github.com/tacerus/ldifdiff@v0.0.0-20181030102753-4dccbe38183b/diff.go (about) 1 // Package ldifdiff is a fast library that outputs the difference 2 // between two LDIF files as a valid and importable LDIF (e.g. 3 // by your LDAP server). 4 package ldifdiff 5 6 import ( 7 "bytes" 8 "sort" 9 "strings" 10 "sync" 11 ) 12 13 // Used by the implementation program in the cmd directory. 14 const Version = "v0.2.0" 15 // Used by the implementation program in the cmd directory. 16 const Author = "Claudio Ramirez <pub.claudio@gmail.com>" 17 // Used by the implementation program in the cmd directory. 18 const Repo = "https://github.com/nxadm/ldifdiff" 19 20 type fn func(string, []string) (entries, error) 21 22 var skipDnForDelete map[string]bool 23 24 /* Public functions */ 25 26 // Diff compares two LDIF strings (sourceStr and targetStr) and outputs the 27 // differences as a LDIF string. An array of attributes can be supplied. These 28 // attributes will be ignored when comparing the LDIF strings. 29 // The output is a string, a valid LDIF, and can be added to the "target" 30 // database (the one that created targetStr) in order to make it 31 // equal to the "source" database (the one that created sourceStr). In case of 32 // failure, an error is provided. 33 func Diff(sourceStr, targetStr string, ignoreAttr []string) (string, error) { 34 return genericDiff(sourceStr, targetStr, ignoreAttr, convertLdifStr, nil) 35 } 36 37 // DiffFromFiles compares two LDIF files (sourceFile and targetFile) and 38 // outputs the differences as a LDIF string. An array of attributes can be 39 // supplied. These attributes will be ignored when comparing the LDIF strings. 40 // The output is a string, a valid LDIF, and can be added to the "target" 41 // database (the one that created targetFile) in order to make it equal to the 42 // "source" database (the one that created sourceFile). In case of failure, an 43 // error is provided. 44 func DiffFromFiles(sourceFile, targetFile string, ignoreAttr []string) (string, error) { 45 return genericDiff(sourceFile, targetFile, ignoreAttr, importLdifFile, nil) 46 } 47 48 // ListDiffDn compares two LDIF strings (sourceStr and targetStr) and outputs 49 // the differences as a list of affected DNs (Dintinguished Names). An array of 50 // attributes can be supplied. These attributes will be ignored when comparing 51 // the LDIF strings. 52 // The output is a string slice. In case of failure, an error is provided. 53 func ListDiffDn(sourceStr, targetStr string, ignoreAttr []string) ([]string, error) { 54 dnList := []string{} 55 _, err := genericDiff(sourceStr, targetStr, ignoreAttr, convertLdifStr, &dnList) 56 return dnList, err 57 } 58 59 // ListDiffDnFromFiles compares two LDIF files (sourceFile and targetFileStr) 60 // and outputs the differences as a list of affected DNs (Dintinguished Names). 61 // An array of attributes can be supplied. These attributes will be ignored 62 // when comparing the LDIF strings. 63 // The output is a string slice. In case of failure, an error is provided. 64 func ListDiffDnFromFiles(sourceFile, targetFile string, ignoreAttr []string) ([]string, error) { 65 dnList := []string{} 66 _, err := genericDiff(sourceFile, targetFile, ignoreAttr, importLdifFile, &dnList) 67 return dnList, err 68 } 69 70 /* Package private functions */ 71 72 func arraysEqual(a, b []string) bool { 73 if a == nil && b == nil { 74 return true 75 } 76 if a == nil || b == nil { 77 return false 78 } 79 if len(a) != len(b) { 80 return false 81 } 82 for i := range a { 83 if a[i] != b[i] { 84 return false 85 } 86 } 87 return true 88 } 89 90 // Ordering Logic: 91 // actionAdd: entries from source sorted S -> L. Otherwise is invalid. 92 // Remove: entries from target sorted L -> S. Otherwise is invalid. 93 // actionModify: 94 // - Keep S -> L ordering 95 // - If only 1 instance of attribute with different value on source and target: 96 // update. This way we don't break the applicable LDAP schema. 97 // - extra attribute on source: actionAdd 98 // - extra attribute on target: delete 99 100 func compare(source, target *entries, dnList *[]string) (string, error) { 101 var buffer bytes.Buffer 102 var err error 103 queue := make(chan actionEntry, 10) 104 var wg sync.WaitGroup 105 106 // Find the order in which operation must happen 107 orderedSourceShortToLong := sortDnByDepth(source, false) 108 orderedTargetLongToShort := sortDnByDepth(target, true) 109 110 // Write the file concurrently 111 wg.Add(1) // 1 writer 112 go writeLdif(queue, &buffer, &wg, &err) 113 114 // Dn only on source + removal of identical entries 115 skipDnForDelete = make(map[string]bool) // Keep track of dn to skip at Deletion 116 sendForAddition(&orderedSourceShortToLong, source, target, queue, dnList) 117 118 // Dn only on target 119 sendForDeletion(&orderedTargetLongToShort, source, target, queue, dnList) 120 121 // Dn on source and target 122 sendForModification(&orderedSourceShortToLong, source, target, queue, dnList) 123 124 // Done sending work 125 close(queue) 126 127 // Free some memory 128 *source = entries{} 129 *target = entries{} 130 131 // Wait for the creation of the LDIF 132 wg.Wait() 133 134 // Return the results 135 return buffer.String(), err 136 } 137 138 //func elementInArray(a string, array []string) bool { 139 // for _, b := range array { 140 // if b == a { 141 // return true 142 // } 143 // } 144 // return false 145 //} 146 147 func genericDiff(sourceParam, targetParam string, ignoreAttr []string, fn fn, dnList *[]string) (string, error) { 148 // Read the files in memory as a Map with sorted attributes 149 var source, target entries 150 var sourceErr, targetErr error 151 var wg sync.WaitGroup 152 wg.Add(2) 153 go func(entries *entries, wg *sync.WaitGroup, err *error) { 154 result, e := fn(sourceParam, ignoreAttr) 155 *entries = result 156 *err = e 157 wg.Done() 158 }(&source, &wg, &sourceErr) 159 go func(entries *entries, wg *sync.WaitGroup, err *error) { 160 result, e := fn(targetParam, ignoreAttr) 161 *entries = result 162 *err = e 163 wg.Done() 164 }(&target, &wg, &targetErr) 165 wg.Wait() 166 167 if sourceErr != nil { 168 return "", sourceErr 169 } 170 if targetErr != nil { 171 return "", targetErr 172 } 173 174 // Compare the files 175 return compare(&source, &target, dnList) 176 } 177 178 func sendForAddition( 179 orderedSourceShortToLong *[]string, 180 source, target *entries, 181 queue chan<- actionEntry, 182 dnList *[]string) { 183 for _, dn := range *orderedSourceShortToLong { 184 // Ignore equal entries 185 if arraysEqual((*source)[dn], (*target)[dn]) { 186 delete(*source, dn) 187 delete(*target, dn) 188 continue 189 } 190 // Mark entries for addition if only on source 191 if _, ok := (*target)[dn]; !ok { 192 if dnList == nil { 193 subActionAttr := make(map[subAction][]string) 194 subActionAttr[subActionNone] = (*source)[dn] 195 actionEntry := 196 actionEntry{ 197 Dn: dn, 198 Action: actionAdd, 199 SubActionAttrs: []subActionAttrs{subActionAttr}, 200 } 201 queue <- actionEntry 202 } else { 203 // Always actionAdd (attributes not relevant) 204 *dnList = append(*dnList, dn) 205 } 206 delete(*source, dn) 207 } 208 // Implict else: 209 // It exists on target and it's not equal, so it's a modifyStr 210 skipDnForDelete[dn] = true 211 } 212 } 213 214 func sendForDeletion( 215 orderedTargetLongToShort *[]string, 216 source, target *entries, 217 queue chan<- actionEntry, 218 dnList *[]string) { 219 for _, dn := range *orderedTargetLongToShort { 220 if skipDnForDelete[dn] { // We know it's not a delete operation 221 continue 222 } 223 if _, ok := (*target)[dn]; ok { // It has not been deleted above 224 if _, ok := (*source)[dn]; !ok { // does not exists on source 225 if dnList == nil { 226 subActionAttr := make(map[subAction][]string) 227 subActionAttr[subActionNone] = nil 228 actionEntry := 229 actionEntry{ 230 Dn: dn, 231 Action: actionDelete, 232 SubActionAttrs: []subActionAttrs{subActionAttr}, 233 } 234 queue <- actionEntry 235 } else { 236 // Always remove (attributes are not relevant) 237 *dnList = append(*dnList, dn) 238 } 239 delete(*target, dn) 240 } 241 // Implict else: 242 // It exists on source and it's not equal (tested on sendForAddition), 243 // so it's a modifyStr 244 } 245 } 246 // Free some memory 247 skipDnForDelete = nil 248 } 249 250 func sendForModification( 251 orderedSourceShortToLong *[]string, source, 252 target *entries, 253 queue chan<- actionEntry, 254 dnList *[]string) { 255 for _, dn := range *orderedSourceShortToLong { 256 // DN is present on source and target: 257 // sendForAdd/Remove clean up source and target 258 _, okSource := (*source)[dn] 259 _, okTarget := (*target)[dn] 260 if okSource && okTarget { // it hasn't been deleted 261 if dnList == nil { 262 263 // Store the attributes to be added, deleted or replaced 264 attrToModifyAdd := []string{} 265 attrToModifyDelete := []string{} 266 attrToModifyReplace := []string{} 267 // Put the attributes in a map for easy lookup 268 sourceAttr := make(map[string]bool) 269 targetAttr := make(map[string]bool) 270 for _, attr := range (*source)[dn] { 271 sourceAttr[attr] = true 272 } 273 for _, attr := range (*target)[dn] { 274 targetAttr[attr] = true 275 } 276 277 // Compare attribute values starting from the source 278 for _, attr := range (*source)[dn] { // Keep the order of the attributes 279 // Attribute is not equal on both sides 280 if _, ok := targetAttr[attr]; !ok { 281 // Is the attribute name (not value) unique? 282 switch uniqueAttrName(attr, sourceAttr, targetAttr) { 283 case true: // This is a actionModify-Replace operation 284 attrToModifyReplace = append(attrToModifyReplace, attr) 285 case false: // This is just a actionModify actionAdd (only on source). 286 attrToModifyAdd = append(attrToModifyAdd, attr) 287 } 288 } 289 } 290 291 // Compare attribute values starting from the target. 292 for _, attr := range (*target)[dn] { // Keep the order of the attributes 293 // Looking for unique attributes 294 if !uniqueAttrName(attr, sourceAttr, targetAttr) { 295 if _, ok := sourceAttr[attr]; !ok { 296 attrToModifyDelete = append(attrToModifyDelete, attr) 297 } 298 } 299 } 300 301 // Send it 302 actionEntry := actionEntry{Dn: dn, Action: actionModify} 303 subActionAttrArray := []subActionAttrs{} 304 switch { 305 case len(attrToModifyAdd) > 0: 306 subActionAttrArray = append(subActionAttrArray, subActionAttrs{subActionModifyAdd: attrToModifyAdd}) 307 fallthrough 308 case len(attrToModifyDelete) > 0: 309 subActionAttrArray = append(subActionAttrArray, subActionAttrs{subActionModifyDelete: attrToModifyDelete}) 310 fallthrough 311 case len(attrToModifyReplace) > 0: 312 subActionAttrArray = append(subActionAttrArray, subActionAttrs{subActionModifyReplace: attrToModifyReplace}) 313 } 314 315 actionEntry.SubActionAttrs = subActionAttrArray 316 queue <- actionEntry 317 } else { 318 // There must be something left to modify 319 //if len((*source)[dn]) > 0 || len((*target)[dn]) > 0 { 320 *dnList = append(*dnList, dn) 321 //} 322 } 323 // Clean it up 324 delete(*source, dn) 325 delete(*target, dn) 326 } 327 } 328 } 329 330 func sortDnByDepth(entries *entries, longToShort bool) []string { 331 var sorted []string 332 333 dns := []string{} 334 for dn := range *entries { 335 dns = append(dns, dn) 336 } 337 338 splitByDc := make(map[string][]string) 339 longestDn := 0 340 // Split the components of the dn and remember the longest size 341 for _, dn := range dns { 342 parts := strings.Split(dn, ",") 343 if len(parts) > longestDn { 344 longestDn = len(parts) 345 } 346 splitByDc[dn] = parts 347 } 348 349 // Get the direction of the loop 350 componentSizes := []int{} 351 if longToShort { 352 for i := longestDn; i > 0; i-- { 353 componentSizes = append(componentSizes, i) 354 } 355 } else { 356 for i := 1; i <= longestDn; i++ { 357 componentSizes = append(componentSizes, i) 358 } 359 } 360 361 // Sort by size and alpahbetically within size 362 for _, size := range componentSizes { 363 sameSize := []string{} 364 for dn, components := range splitByDc { 365 if len(components) == size { 366 sameSize = append(sameSize, dn) 367 } 368 } 369 sort.Strings(sameSize) 370 sorted = append(sorted, sameSize...) 371 } 372 373 return sorted 374 } 375 376 func uniqueAttrName(attr string, sourceAttr, targetAttr map[string]bool) bool { 377 378 // Get the attribute name 379 parts := strings.Split(attr, ":") 380 attrName := parts[0] 381 //var base64 bool 382 //if parts[1] == "" { 383 // base64 = true 384 //} 385 sourceCounter := 0 386 for attr := range sourceAttr { 387 if strings.HasPrefix(attr, attrName+":") { 388 sourceCounter++ 389 } 390 } 391 targetCounter := 0 392 for attr := range targetAttr { 393 if strings.HasPrefix(attr, attrName+":") { 394 targetCounter++ 395 } 396 } 397 398 return sourceCounter == 1 && targetCounter == 1 399 }