ldif 0.0.1 → 0.0.2
raw patch · 14 files changed
+577/−222 lines, 14 filesnew-component:exe:diffLDIFPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Text.LDIF: AttrValRecord :: DN -> [AttrValue] -> Record
- Text.LDIF: ChangeAdd :: [AttrValue] -> Change
- Text.LDIF: ChangeDelete :: Change
- Text.LDIF: ChangeModDN :: Change
- Text.LDIF: ChangeModify :: [Modify] -> Change
- Text.LDIF: ChangeRecord :: DN -> Change -> Record
- Text.LDIF: LDIFChanges :: Maybe String -> [Record] -> LDIF
- Text.LDIF: LDIFContent :: Maybe String -> [Record] -> LDIF
- Text.LDIF: ModAdd :: Attribute -> [AttrValue] -> Modify
- Text.LDIF: ModDelete :: Attribute -> [AttrValue] -> Modify
- Text.LDIF: ModReplace :: Attribute -> [AttrValue] -> Modify
- Text.LDIF: chAttrVals :: Change -> [AttrValue]
- Text.LDIF: chMods :: Change -> [Modify]
- Text.LDIF: data Change
- Text.LDIF: data LDIF
- Text.LDIF: data Modify
- Text.LDIF: data Record
- Text.LDIF: instance Show Change
- Text.LDIF: instance Show LDIF
- Text.LDIF: instance Show Modify
- Text.LDIF: instance Show Record
- Text.LDIF: lcEntries :: LDIF -> [Record]
- Text.LDIF: lcVersion :: LDIF -> Maybe String
- Text.LDIF: modAttr :: Modify -> Attribute
- Text.LDIF: modAttrVals :: Modify -> [AttrValue]
- Text.LDIF: parseLDIFFile :: String -> IO (Either ParseError LDIF)
- Text.LDIF: parseLDIFStr :: String -> Either ParseError LDIF
- Text.LDIF: recAttrVals :: Record -> [AttrValue]
- Text.LDIF: recDN :: Record -> DN
- Text.LDIF: recOp :: Record -> Change
- Text.LDIF: type AttrValue = (Attribute, Value)
- Text.LDIF: type Attribute = String
- Text.LDIF: type DN = String
- Text.LDIF: type Value = String
+ Text.LDIF.Parser: parseDNStr :: String -> Either ParseError DN
+ Text.LDIF.Parser: parseLDIFFile :: String -> IO (Either ParseError LDIF)
+ Text.LDIF.Parser: parseLDIFStr :: String -> Either ParseError LDIF
+ Text.LDIF.Printer: change2Str :: ChangeRecord -> String
+ Text.LDIF.Printer: content2Str :: ContentRecord -> String
+ Text.LDIF.Printer: dn2Str :: DN -> String
+ Text.LDIF.Printer: ldif2Str :: LDIF -> String
+ Text.LDIF.Proc: diffLDIF :: LDIF -> LDIF -> Maybe LDIF
+ Text.LDIF.Proc: diffRecord :: ContentRecord -> ContentRecord -> Maybe ChangeRecord
+ Text.LDIF.Proc: findChangesByDN :: LDIF -> DN -> [ChangeRecord]
+ Text.LDIF.Proc: findContentByDN :: LDIF -> DN -> Maybe ContentRecord
+ Text.LDIF.Proc: findContentsByDN :: LDIF -> DN -> [ContentRecord]
+ Text.LDIF.Types: ChangeAdd :: [AttrValue] -> Change
+ Text.LDIF.Types: ChangeDelete :: Change
+ Text.LDIF.Types: ChangeModDN :: Change
+ Text.LDIF.Types: ChangeModify :: [Modify] -> Change
+ Text.LDIF.Types: ChangeRecord :: DN -> Change -> ChangeRecord
+ Text.LDIF.Types: ContentRecord :: DN -> [AttrValue] -> ContentRecord
+ Text.LDIF.Types: DN :: [AttrValue] -> DN
+ Text.LDIF.Types: LDIFChanges :: Maybe String -> [ChangeRecord] -> LDIF
+ Text.LDIF.Types: LDIFContent :: Maybe String -> [ContentRecord] -> LDIF
+ Text.LDIF.Types: ModAdd :: Attribute -> [Value] -> Modify
+ Text.LDIF.Types: ModDelete :: Attribute -> [Value] -> Modify
+ Text.LDIF.Types: ModReplace :: Attribute -> [Value] -> Modify
+ Text.LDIF.Types: chAttrVals :: Change -> [AttrValue]
+ Text.LDIF.Types: chDN :: ChangeRecord -> DN
+ Text.LDIF.Types: chMods :: Change -> [Modify]
+ Text.LDIF.Types: chOp :: ChangeRecord -> Change
+ Text.LDIF.Types: coAttrVals :: ContentRecord -> [AttrValue]
+ Text.LDIF.Types: coDN :: ContentRecord -> DN
+ Text.LDIF.Types: data Change
+ Text.LDIF.Types: data ChangeRecord
+ Text.LDIF.Types: data ContentRecord
+ Text.LDIF.Types: data DN
+ Text.LDIF.Types: data LDIF
+ Text.LDIF.Types: data Modify
+ Text.LDIF.Types: dnAttrVals :: DN -> [AttrValue]
+ Text.LDIF.Types: instance Eq Change
+ Text.LDIF.Types: instance Eq ChangeRecord
+ Text.LDIF.Types: instance Eq ContentRecord
+ Text.LDIF.Types: instance Eq DN
+ Text.LDIF.Types: instance Eq LDIF
+ Text.LDIF.Types: instance Eq Modify
+ Text.LDIF.Types: instance Show Change
+ Text.LDIF.Types: instance Show ChangeRecord
+ Text.LDIF.Types: instance Show ContentRecord
+ Text.LDIF.Types: instance Show DN
+ Text.LDIF.Types: instance Show LDIF
+ Text.LDIF.Types: instance Show Modify
+ Text.LDIF.Types: lcChanges :: LDIF -> [ChangeRecord]
+ Text.LDIF.Types: lcEntries :: LDIF -> [ContentRecord]
+ Text.LDIF.Types: lcVersion :: LDIF -> Maybe String
+ Text.LDIF.Types: modAttr :: Modify -> Attribute
+ Text.LDIF.Types: modAttrVals :: Modify -> [Value]
+ Text.LDIF.Types: type AttrValue = (Attribute, Value)
+ Text.LDIF.Types: type Attribute = String
+ Text.LDIF.Types: type Value = String
Files
- TODO +13/−0
- ldif.cabal +30/−8
- run_tests.sh +3/−0
- src/Text/LDIF.hs +9/−208
- src/Text/LDIF/Parser.hs +232/−0
- src/Text/LDIF/Printer.hs +49/−0
- src/Text/LDIF/Proc.hs +62/−0
- src/Text/LDIF/Types.hs +43/−0
- src/diffLDIF.hs +16/−0
- tests/TestMain.hs +13/−6
- tests/data/OK_diff01.content.ldif +10/−0
- tests/data/OK_diff02.content.ldif +4/−0
- tests/data/OK_simpleComment.modify.ldif +48/−0
- tests/data/OK_simpleWrap.modify.ldif +45/−0
+ TODO view
@@ -0,0 +1,13 @@+* DONE replace error call in mkMod with parser error+ (Not needed)+* DONE create preprocessor for concat split long lines & remove comments+* DONE refactoring of packages, create Types, Parser and Proc+* TODO update parseDNStr to parse all kind of DN notations+* TODO error handling, modify add: <attrX> and than <attrY>:<value>,+ need to be asserted that <attrX> and <attrY> are the same.+* TODO Implement the base64 parsing+* TODO Implement modifyDN parsing+* TODO update serializers back to LDIF for base64 & all DN notations+* TODO add module tests for versioned LDIFs+* TODO add module tests for base64 parsing+
ldif.cabal view
@@ -1,28 +1,42 @@ Name: ldif-Version: 0.0.1+Version: 0.0.2 License: BSD3 License-File: LICENSE Synopsis: The LDAP Data Interchange Format (LDIF) parser Author: Radoslav Dorcik <radoslav.dorcik@gmail.com> Maintainer: radoslav.dorcik@gmail.com Description: - LDIF files parser implementation using Parsec and based- on RFC 2849 - The LDAP Data Interchange Format (LDIF).-+ LDIF files parser implementation using Parsec and based+ on RFC 2849 - The LDAP Data Interchange Format (LDIF).+ .+ Current implementation is unfinished and need to be enhanced+ for base64 encoded values and various DN escaping. + .+ It includes following tool:+ .+ - diffLDIF command generates change LDIF between two + content LDIF files. + . Category: Text Stability: experimental Build-Type: Simple Cabal-Version: >= 1.6 Extra-Source-Files:+ run_tests.sh+ TODO doc/rfc2253.txt doc/rfc2849.txt tests/TestMain.hs- tests/data/OK_simple03.modify.ldif- tests/data/OK_simple01.modify.ldif- tests/data/OK_simple02.content.ldif+ tests/data/OK_diff01.content.ldif+ tests/data/OK_diff02.content.ldif tests/data/OK_multivalue.modify.ldif tests/data/OK_simple01.content.ldif-+ tests/data/OK_simple01.modify.ldif+ tests/data/OK_simple02.content.ldif+ tests/data/OK_simple03.modify.ldif+ tests/data/OK_simpleComment.modify.ldif+ tests/data/OK_simpleWrap.modify.ldif+ Source-Repository head type: darcs location: http://rampa.sk/repo/ldif@@ -44,6 +58,14 @@ Exposed-modules: Text.LDIF+ Text.LDIF.Types+ Text.LDIF.Parser+ Text.LDIF.Printer+ Text.LDIF.Proc++Executable diffLDIF+ Hs-Source-Dirs: src+ Main-Is: diffLDIF.hs Executable test Hs-Source-Dirs: src, tests
+ run_tests.sh view
@@ -0,0 +1,3 @@+#!/bin/sh++cabal clean && cabal configure -ftest && cabal build && cd tests && ../dist/build/test/test
src/Text/LDIF.hs view
@@ -1,213 +1,14 @@ module Text.LDIF (- parseLDIFStr,- parseLDIFFile,- LDIF(..), - Record(..),- Change(..),- Modify(..), - DN, Attribute, Value, AttrValue+ module Text.LDIF.Types,+ module Text.LDIF.Parser,+ module Text.LDIF.Printer,+ module Text.LDIF.Proc ) where-import Text.ParserCombinators.Parsec-import Data.Either-import Data.Char -type Attribute = String-type Value = String-type AttrValue = (Attribute, Value)-type DN = String---- | Represents LDIF structure, it can be either simply LDIF data dump or--- | changes LDIF with LDAP operations -data LDIF = LDIFContent { lcVersion :: Maybe String, lcEntries :: [Record] }- | LDIFChanges { lcVersion :: Maybe String, lcEntries :: [Record] } deriving Show---- | Represents one record or entry within LDIF file with DN and content-data Record = AttrValRecord { recDN :: DN, recAttrVals :: [AttrValue] } - | ChangeRecord { recDN :: DN, recOp :: Change } deriving Show---- | Represents one LDAP operation within changes LDIF-data Change = ChangeAdd { chAttrVals :: [AttrValue] }- | ChangeDelete - | ChangeModify { chMods :: [Modify] }- | ChangeModDN deriving Show---- | Represents ChangeModify operations upon one entry within given DN-data Modify = ModAdd { modAttr :: Attribute, modAttrVals :: [AttrValue] }- | ModDelete { modAttr :: Attribute, modAttrVals :: [AttrValue] }- | ModReplace { modAttr :: Attribute, modAttrVals :: [AttrValue] } deriving Show---- | Parse string as LDIF content and return LDIF or ParseError-parseLDIFStr :: String -> Either ParseError LDIF-parseLDIFStr = parse pLdif "(param)" ---- | Read and parse provided file and return LDIF or ParseError-parseLDIFFile :: String -> IO (Either ParseError LDIF)-parseLDIFFile name = do- input <- readFile name- return $ parse pLdif name input---- | Parsec ldif parser-pLdif :: CharParser st LDIF-pLdif = try pLdifChanges <|> pLdifContent--pLdifChanges :: CharParser st LDIF-pLdifChanges = do- ver <- optionMaybe pVersionSpec- recs <- sepEndBy1 pChangeRec pSEPs- return $ LDIFChanges ver recs--pLdifContent :: CharParser st LDIF-pLdifContent = do- ver <- optionMaybe pVersionSpec- recs <- sepEndBy1 pAttrValRec pSEPs- return $ LDIFContent ver recs--pAttrValRec :: CharParser st Record-pAttrValRec = do- dn <- pDNSpec- pSEP- attrVals <- sepEndBy1 pAttrValSpec pSEP- return $ AttrValRecord dn attrVals--pChangeRec :: CharParser st Record-pChangeRec = try pChangeAdd- <|> try pChangeDel- <|> try pChangeMod- <|> pChangeModDN--pChangeAdd :: CharParser st Record-pChangeAdd = do- dn <- pDNSpec- pSEP- string "changetype:"- pFILL- string "add"- pSEP- vals <- sepEndBy1 pAttrValSpec pSEP- return $ ChangeRecord dn (ChangeAdd vals)--pChangeDel :: CharParser st Record-pChangeDel = do- dn <- pDNSpec- pSEP- string "changetype:"- pFILL- string "delete"- pSEP- return $ ChangeRecord dn ChangeDelete--pChangeMod :: CharParser st Record-pChangeMod = do- dn <- pDNSpec- pSEP- string "changetype:"- pFILL- string "modify"- pSEP- mods <- sepEndBy1 pModSpec (char '-' >> pSEP)- return $ ChangeRecord dn (ChangeModify mods)--pChangeModDN :: CharParser st Record-pChangeModDN = do- dn <- pDNSpec- pSEP- string "changetype:"- pFILL- string "modrdn" - pSEP- string "newrdn:"- pFILL - pRDN- pSEP- string "deleteoldrdn:"- pFILL- oneOf "01"- pSEP- return $ ChangeRecord dn ChangeModDN--pRDN :: CharParser st String-pRDN = pSafeString--pDNSpec :: CharParser st DN-pDNSpec = do- string "dn:"- pFILL- pSafeString--pVersionSpec :: CharParser st String-pVersionSpec = do- string "version:"- pFILL- many1 digit--pModSpec :: CharParser st Modify-pModSpec = do- modType <- pModType- pFILL- att <- pAttributeDescription - pSEP - vals <- sepEndBy pAttrValSpec pSEP- return $ mkMod modType att vals---- TODO: Use something safe instead of error-mkMod :: String -> String -> [AttrValue] -> Modify-mkMod modType att vals | modType == "add:" = ModAdd att vals- | modType == "delete:" = ModDelete att vals- | modType == "replace:" = ModReplace att vals- | otherwise = error $ "unexpected mod:" ++ modType--pModType :: CharParser st String-pModType = try (string "add:")- <|> try (string "delete:")- <|> string "replace:"--pAttributeDescription :: CharParser st String-pAttributeDescription = pAttributeType--pAttributeType :: CharParser st String-pAttributeType = try pLdapOid- <|> (do { l <- letter; o <- pAttrTypeChars; return $ l:o } )--pAttrValSpec :: CharParser st AttrValue-pAttrValSpec = do- name <- pAttributeDescription- val <- pValueSpec- return (name, val)--pValueSpec :: CharParser st Value-pValueSpec = try (char ':' >> char ':' >> pFILL >> pBase64String)- <|> try (char ':' >> pFILL >> pSafeString) - <|> (char ':' >> char '<' >> pFILL >> pURL)--pURL :: CharParser st String-pURL = pSafeString--pSafeString :: CharParser st String-pSafeString = do- c <- noneOf "\n\r :<"- r <- many (noneOf "\n\r")- return $ c:r- -pBase64String :: CharParser st String-pBase64String = pSafeString--pAttrTypeChars :: CharParser st String-pAttrTypeChars = many (satisfy (\x -> isAlphaNum x || x == '-'))--pLdapOid :: CharParser st String-pLdapOid = do- num <- many1 digit- rest <- many (do { string "."; n <- many1 digit; return $ '.':n})- return $ num ++ concat rest--pFILL :: CharParser st ()-pFILL = spaces--pSEP :: CharParser st ()-pSEP = try (char '\r' >> char '\n' >> return () )- <|> (char '\n' >> return () )--pSEPs :: CharParser st ()-pSEPs = many pSEP >> return ()+import Text.LDIF.Types+import Text.LDIF.Parser+import Text.LDIF.Printer+import Text.LDIF.Proc +
+ src/Text/LDIF/Parser.hs view
@@ -0,0 +1,232 @@+module Text.LDIF.Parser (+ parseLDIFStr,+ parseLDIFFile,+ parseDNStr+)+where+import Text.LDIF.Types+import Text.ParserCombinators.Parsec+import Data.Either+import Data.Char+import Data.List (isPrefixOf)++-- | Parse string as LDIF content and return LDIF or ParseError+parseLDIFStr :: String -> Either ParseError LDIF+parseLDIFStr = parse pLdif "(param)" . preproc ++-- | Read and parse provided file and return LDIF or ParseError+parseLDIFFile :: String -> IO (Either ParseError LDIF)+parseLDIFFile name = do+ input <- readFile name+ return $ parse pLdif name (preproc input)++-- | Parse string as DN and return DN type or ParseError+parseDNStr :: String -> Either ParseError DN+parseDNStr = parse pDN "(param)" ++-- | Preprocessing for concat wrapped lines and remove comment lines+preproc :: String -> String+preproc = unwrap . stripComments++-- | Remove Comment Lines+stripComments :: String -> String+stripComments input = unlines $ filter (not . isPrefixOf "#") $ lines input++-- | Unwrap lines, lines with space at begin is continue of previous line +unwrap :: String -> String+unwrap input = unlines $ preprocLines $ lines input+ where + preprocLines xs = unbox $ foldl (preprocLine) ([],Nothing) xs+ preprocLine (ys,r) [] = (addLineMaybe ys r,Just []) + preprocLine (ys,r) (l:lx) | l == ' ' = (ys,concatLineMaybe r lx)+ | otherwise = (addLineMaybe ys r, Just $ l:lx)+ concatLineMaybe Nothing x = Just x+ concatLineMaybe (Just y) x = Just (y++x)+ addLineMaybe xs Nothing = xs+ addLineMaybe xs (Just x) = xs++[x]+ unbox (ys,Nothing) = ys+ unbox (ys,Just x) = ys++[x]++-- | Parsec ldif parser+pLdif :: CharParser st LDIF+pLdif = try pLdifChanges <|> pLdifContent++pLdifChanges :: CharParser st LDIF+pLdifChanges = do+ ver <- optionMaybe pVersionSpec+ recs <- sepEndBy1 pChangeRec pSEPs+ return $ LDIFChanges ver recs++pLdifContent :: CharParser st LDIF+pLdifContent = do+ ver <- optionMaybe pVersionSpec+ recs <- sepEndBy1 pAttrValRec pSEPs+ return $ LDIFContent ver recs++pAttrValRec :: CharParser st ContentRecord+pAttrValRec = do+ dn <- pDNSpec+ pSEP+ attrVals <- sepEndBy1 pAttrValSpec pSEP+ return $ ContentRecord dn attrVals++pChangeRec :: CharParser st ChangeRecord+pChangeRec = try pChangeAdd+ <|> try pChangeDel+ <|> try pChangeMod+ <|> pChangeModDN++pChangeAdd :: CharParser st ChangeRecord+pChangeAdd = do+ dn <- pDNSpec+ pSEP+ string "changetype:"+ pFILL+ string "add"+ pSEP+ vals <- sepEndBy1 pAttrValSpec pSEP+ return $ ChangeRecord dn (ChangeAdd vals)++pChangeDel :: CharParser st ChangeRecord+pChangeDel = do+ dn <- pDNSpec+ pSEP+ string "changetype:"+ pFILL+ string "delete"+ pSEP+ return $ ChangeRecord dn ChangeDelete++pChangeMod :: CharParser st ChangeRecord+pChangeMod = do+ dn <- pDNSpec+ pSEP+ string "changetype:"+ pFILL+ string "modify"+ pSEP+ mods <- sepEndBy1 pModSpec (char '-' >> pSEP)+ return $ ChangeRecord dn (ChangeModify mods)++pChangeModDN :: CharParser st ChangeRecord+pChangeModDN = do+ dn <- pDNSpec+ pSEP+ string "changetype:"+ pFILL+ string "modrdn" + pSEP+ string "newrdn:"+ pFILL + pRDN+ pSEP+ string "deleteoldrdn:"+ pFILL+ oneOf "01"+ pSEP+ return $ ChangeRecord dn ChangeModDN++pRDN :: CharParser st String+pRDN = pSafeString++pDNSpec :: CharParser st DN+pDNSpec = do+ string "dn:"+ pDN++pDN :: CharParser st DN+pDN = do+ pFILL+ avals <- sepEndBy pAttrEqValue (char ',') + return $ DN avals++pAttrEqValue :: CharParser st AttrValue+pAttrEqValue = do+ att <- pAttributeType+ char '='+ val <- pAttrValueDN+ return (att,val)++pAttrValueDN :: CharParser st Value+pAttrValueDN = do+ many (noneOf stringChars)+ where + specialChars = [',','=','+','<','>','#',';','\n','\r']+ stringChars = '\\':'"':specialChars ++pVersionSpec :: CharParser st String+pVersionSpec = do+ string "version:"+ pFILL+ many1 digit++pModSpec :: CharParser st Modify+pModSpec = do+ modType <- pModType+ pFILL+ att <- pAttributeDescription + pSEP + vals <- sepEndBy pAttrValSpec pSEP+ return $ mkMod modType att vals++mkMod :: String -> String -> [AttrValue] -> Modify+mkMod modType att vals | modType == "add:" = ModAdd att (map (snd) vals)+ | modType == "delete:" = ModDelete att (map (snd) vals)+ | modType == "replace:" = ModReplace att (map (snd) vals)+ | otherwise = error $ "unexpected mod:" ++ modType + -- error can not be reached because pModType++pModType :: CharParser st String+pModType = try (string "add:")+ <|> try (string "delete:")+ <|> string "replace:"++pAttributeDescription :: CharParser st String+pAttributeDescription = pAttributeType++pAttributeType :: CharParser st String+pAttributeType = try pLdapOid+ <|> (do { l <- letter; o <- pAttrTypeChars; return $ l:o } )++pAttrValSpec :: CharParser st AttrValue+pAttrValSpec = do+ name <- pAttributeDescription+ val <- pValueSpec+ return (name, val)++pValueSpec :: CharParser st Value+pValueSpec = try (char ':' >> char ':' >> pFILL >> pBase64String)+ <|> try (char ':' >> pFILL >> pSafeString) + <|> (char ':' >> char '<' >> pFILL >> pURL)++pURL :: CharParser st String+pURL = pSafeString++pSafeString :: CharParser st String+pSafeString = do+ c <- noneOf "\n\r :<"+ r <- many (noneOf "\n\r")+ return $ c:r+ +pBase64String :: CharParser st String+pBase64String = pSafeString++pAttrTypeChars :: CharParser st String+pAttrTypeChars = many (satisfy (\x -> isAlphaNum x || x == '-'))++pLdapOid :: CharParser st String+pLdapOid = do+ num <- many1 digit+ rest <- many (do { string "."; n <- many1 digit; return $ '.':n})+ return $ num ++ concat rest++pFILL :: CharParser st ()+pFILL = spaces++pSEP :: CharParser st ()+pSEP = try (char '\r' >> char '\n' >> return () )+ <|> (char '\n' >> return () )++pSEPs :: CharParser st ()+pSEPs = many pSEP >> return ()+
+ src/Text/LDIF/Printer.hs view
@@ -0,0 +1,49 @@+-- | LDIF serializers+module Text.LDIF.Printer (+ ldif2Str,+ dn2Str,+ change2Str,+ content2Str+)+where+import Text.LDIF.Types+import Data.List++-- | Serialize LDIF in LDIF Format+ldif2Str :: LDIF -> String+ldif2Str (LDIFContent v xs) = unlines $ (ver2Str v) ++ (map (content2Str) xs)+ldif2Str (LDIFChanges v xs) = unlines $ (ver2Str v) ++ (map (change2Str) xs)++-- | Serialize version to LDIF Format Lines+ver2Str :: Maybe String -> [String]+ver2Str Nothing = []+ver2Str (Just v) = ["version: "++v]++-- | Serialize DN to LDIF Format+dn2Str :: DN -> String+dn2Str (DN xs) = intercalate "," $ map (\(n,v) -> n++"="++v) xs++-- | Serialize Content Record in LDIF Format+content2Str :: ContentRecord -> String+content2Str (ContentRecord dn xs) = unlines $ [ "dn: "++(dn2Str dn) ] ++ (attrVals2Ln xs)++-- | Serialize Change Record in LDIF Format+change2Str :: ChangeRecord -> String+change2Str (ChangeRecord dn (ChangeDelete)) = unlines [ "dn: "++(dn2Str dn), "changetype: delete" ]+change2Str (ChangeRecord dn (ChangeAdd xs)) = unlines $ [ "dn: "++(dn2Str dn), "changetype: add" ] ++ (attrVals2Ln xs)+change2Str (ChangeRecord dn (ChangeModify xs)) = unlines $ [ "dn: "++(dn2Str dn), "changetype: modify" ] ++ (mods2Ln xs)+change2Str (ChangeRecord dn (ChangeModDN)) = unlines $ [ "dn: "++(dn2Str dn), "changetype: moddn" ]++attrVals2Ln :: [AttrValue] -> [String]+attrVals2Ln xs = map (attrVal2Ln) xs++attrVal2Ln :: AttrValue -> String+attrVal2Ln (n,v) = n ++ ": "++v++mods2Ln :: [Modify] -> [String]+mods2Ln xs = intercalate ["-"] $ map (mod2Ln) xs++mod2Ln :: Modify -> [String]+mod2Ln (ModAdd nm xs) = [ attrVal2Ln ("add",nm) ] ++ (map (\v -> attrVal2Ln (nm,v)) xs) +mod2Ln (ModDelete nm xs) = [ attrVal2Ln ("delete",nm) ] ++ (map (\v -> attrVal2Ln (nm,v)) xs)+mod2Ln (ModReplace nm xs) = [ attrVal2Ln ("replace",nm) ] ++ (map (\v -> attrVal2Ln (nm,v)) xs)
+ src/Text/LDIF/Proc.hs view
@@ -0,0 +1,62 @@+-- | LDIF related operations+module Text.LDIF.Proc (+ findChangesByDN,+ findContentsByDN,+ findContentByDN,+ diffLDIF,+ diffRecord +)+where+import Text.LDIF.Types+import Data.Maybe++-- | Find all Changes with given DN+findChangesByDN :: LDIF -> DN -> [ChangeRecord]+findChangesByDN _ _ = error "not implemented"++-- | Find all Contents with given DN+findContentsByDN :: LDIF -> DN -> [ContentRecord]+findContentsByDN (LDIFContent _ entries) dn = filter (\x -> (coDN x) == dn) entries+findContentsByDN _ _ = []++-- | Find first Content with given DN+findContentByDN :: LDIF -> DN -> Maybe ContentRecord+findContentByDN ldif dn = case findContentsByDN ldif dn of+ [] -> Nothing+ xs -> Just (head xs)++-- | Create Change LDIF between to LDIF contents. If any+-- | of input argument is not LDIFContent it returns Nothing. +-- | If there is not difference the Change LDIF with empty+-- | change list is returned.+-- |+-- | Unsing following strategy: +-- | 1. Iterate over L1 DN's and Modify / Remove Content +-- | 2. Iterate over L2 and Add Content not in L1+diffLDIF :: LDIF -> LDIF -> Maybe LDIF+diffLDIF l1@(LDIFContent v1 c1) l2@(LDIFContent v2 c2) = Just (LDIFChanges v2 changes) + where + changes = filter (not . isDummyChangeRecord) $ foldl (processEntry) [] c1+ processEntry xs e1 = let me2 = findContentByDN l2 (coDN e1) + change = case me2 of+ Nothing -> ChangeRecord (coDN e1) ChangeDelete+ Just e2 -> fromJust $ diffRecord e1 e2+ in xs ++ [change]+diffLDIF _ _ = Nothing++-- | Diff two AttrVal Records if any of provided. +-- | Implementation uses inefficient algorithm for large count of attributes within ContentRecord.+diffRecord :: ContentRecord -> ContentRecord -> Maybe ChangeRecord+diffRecord r1 r2 | (coDN r1) /= (coDN r2) = Nothing+ | otherwise = Just (ChangeRecord (coDN r1) (ChangeModify mods))+ where+ mods = delMods ++ addMods+ addMods = map (\x -> ModAdd (fst x) [(snd x)]) addVals+ delMods = map (\x -> ModDelete (fst x) [(snd x)]) delVals+ addVals = filter (\x -> not $ elem x (coAttrVals r1)) (coAttrVals r2) :: [AttrValue]+ delVals = filter (\x -> not $ elem x (coAttrVals r2)) (coAttrVals r1) :: [AttrValue]++-- | Change record without any impact+isDummyChangeRecord :: ChangeRecord -> Bool+isDummyChangeRecord (ChangeRecord _ (ChangeModify [])) = True +isDummyChangeRecord _ = False
+ src/Text/LDIF/Types.hs view
@@ -0,0 +1,43 @@+-- | LDIF related types+module Text.LDIF.Types (+ LDIF(..), + ContentRecord(..),+ ChangeRecord(..),+ Change(..),+ Modify(..), + DN(..), + Attribute, Value, AttrValue+)+where+import Data.Char++type Attribute = String+type Value = String+type AttrValue = (Attribute, Value)++-- | Represents LDIF structure, it can be either simply LDIF data dump or+-- | changes LDIF with LDAP operations +data LDIF = LDIFContent { lcVersion :: Maybe String, lcEntries :: [ContentRecord] }+ | LDIFChanges { lcVersion :: Maybe String, lcChanges :: [ChangeRecord] } deriving (Show, Eq)++-- | Represents one data record within LDIF file with DN and content+data ContentRecord =ContentRecord { coDN :: DN, coAttrVals :: [AttrValue] } deriving (Show, Eq)++-- | Represents one change record within LDIF file with DN and content+data ChangeRecord = ChangeRecord { chDN :: DN, chOp :: Change } deriving (Show, Eq)++-- | Represents one LDAP operation within changes LDIF+data Change = ChangeAdd { chAttrVals :: [AttrValue] }+ | ChangeDelete + | ChangeModify { chMods :: [Modify] }+ | ChangeModDN deriving (Show, Eq)++-- | Represents ChangeModify operations upon one entry within given DN+data Modify = ModAdd { modAttr :: Attribute, modAttrVals :: [Value] }+ | ModDelete { modAttr :: Attribute, modAttrVals :: [Value] }+ | ModReplace { modAttr :: Attribute, modAttrVals :: [Value] } deriving (Show, Eq)++-- | Represents Distinguished Name (DN)+data DN = DN { dnAttrVals :: [AttrValue] } deriving (Show, Eq)++
+ src/diffLDIF.hs view
@@ -0,0 +1,16 @@+-- | Make Change LDIF based on two Content LDIFs+import Data.List+import Data.Either+import Data.Maybe+import Control.Monad +import System.FilePath +import System.Environment+import Text.LDIF++main = do+ args <- getArgs+ ml1 <- parseLDIFFile (args !! 0)+ ml2 <- parseLDIFFile (args !! 1)+ case rights [ml1,ml2] of + [l1,l2] -> putStrLn $ ldif2Str $ fromJust $ diffLDIF l1 l2 + _ -> print $ lefts [ml1,ml2]
tests/TestMain.hs view
@@ -12,6 +12,17 @@ ls <- getLDIFs ldifDir runTestTT (tests ls) +tests ls = TestList $ (testCasesParseOK ls) ++ (testCasesDIFF)++--+-- Test Cases+--+testCasesDIFF = [TestCase (assertEqual "dummy" True True)]+testCasesParseOK ls = map (\x -> TestCase (assertParsedOK x)) $ filter (isOK) ls++--+-- Support Methods+-- getLDIFs :: String -> IO [String] getLDIFs dr = do liftM (map (dr </>)) $ liftM (filter isLDIF) $ getDirectoryContents dr@@ -19,10 +30,6 @@ isOK x = isPrefixOf "OK" (takeFileName x) isLDIF x = isSuffixOf ".ldif" x -tests ls = TestList (testCasesParseOK ls)--testCasesParseOK ls = map (\x -> TestCase (assertParsedOK x)) $ filter (isOK) ls- assertParsedOK filename = do ret <- parseLDIFFile filename either (\e -> assertFailure (show e)) (\ldif -> assertParsedType filename ldif) ret@@ -31,9 +38,9 @@ | (isSuffixOf ".content.ldif" name) = assertTypeContent name ldif | otherwise = assertFailure $ "Unexpected filename: (not .modify.ldif or .content.ldif " ++ name -assertTypeContent n l@(LDIFContent _ _) = assertBool "Valid Content Type" True >> (putStrLn $ "\n\n" ++ n ++ "\n\n" ++ (show l))+assertTypeContent n l@(LDIFContent _ _) = assertBool "Valid Content Type" True -- >> (putStrLn $ "\n\n" ++ n ++ "\n\n" ++ (show l)) assertTypeContent n x = assertFailure $ n ++ " is not type of LDIFContent" -assertTypeChanges n l@(LDIFChanges _ _) = assertBool "Valid Changes Type" True >> (putStrLn $ "\n\n" ++ n ++ "\n\n" ++ (show l))+assertTypeChanges n l@(LDIFChanges _ _) = assertBool "Valid Changes Type" True -- >> (putStrLn $ "\n\n" ++ n ++ "\n\n" ++ (show l)) assertTypeChanges n x = assertFailure $ n ++ " is not type of LDIFChanges"
+ tests/data/OK_diff01.content.ldif view
@@ -0,0 +1,10 @@+dn: cn=The Postmaster,dc=example,dc=com+objectClass: organizationalRole+cn: The Postmaster+oldAttr: attrValue1+oldAttr: attrValue2++dn: cn=The Postmaster Remove,dc=example,dc=com+objectClass: organizationalRole+cn: The Postmaster Remove+
+ tests/data/OK_diff02.content.ldif view
@@ -0,0 +1,4 @@+dn: cn=The Postmaster,dc=example,dc=com+objectClass: organizationalRole+cn: The Postmaster+newAttribute: newValue
+ tests/data/OK_simpleComment.modify.ldif view
@@ -0,0 +1,48 @@+#Hello World+dn: CN=John Smith,OU=Legal,DC=example,DC=com+changetype: modify+# In middle+replace:employeeID+employeeID: 1234+-+# Here+replace:employeeNumber+employeeNumber: 98722+-+replace: extensionAttribute6+extensionAttribute6: JSmith98+-++# Test+# coment is the # comment+dn: CN=Jane Smith,OU=Accounting,DC=example,DC=com+changetype: modify+replace:employeeID+employeeID: 5678+-+replace:employeeNumber+employeeNumber: 76543+-+replace: extensionAttribute6+extensionAttribute6: JSmith14+-+++dn: CN=Jane Smith,OU=Accounting,DC=example,DC=com+changetype: modify+add:employeeID+employeeID: 5678+-+delete:employeeNumber+employeeNumber: 76543+-+replace: extensionAttribute6+extensionAttribute6: JSmith14+extensionAttribute6: JSmith18+-+add: extensionAttribute6+extensionAttribute6: JSmith14+extensionAttribute6: JSmith15+-+delete:employeeNumber+-
+ tests/data/OK_simpleWrap.modify.ldif view
@@ -0,0 +1,45 @@+dn: CN=John Smith,OU=Legal,DC=example,+ DC=com+change+ type: modify+replace:employeeID+employeeID: 1234+-+replace:employeeNumber+employeeNumber: 98722+-+replace: extensionAttribute6+extensionAttribute6: JSmith98+-++dn: CN=Jane Smith,OU=Accounting,DC=example,DC=com+changetype: modify+replace:employeeID+employeeID: 5678+-+replace:employeeNumber+employeeNumber: 76543+-+replace: extensionAttribute6+extensionAttribute6: JSmith14+-+++dn: CN=Jane Smith,OU=Accounting,DC=example,DC=com+changetype: modify+add:employeeID+employeeID: 5678+-+delete:employeeNumber+employeeNumber: 76543+-+replace: extensionAttribute6+extensionAttribute6: JSmith14+extensionAttribute6: JSmith18+-+add: extensionAttribute6+extensionAttribute6: JSmith14+extensionAttribute6: JSmith15+-+delete:employeeNumber+-