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  }