packages feed

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 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+-