blob: 66675c70480f96a8e65b07435c92683159da92fb [file] [log] [blame]
// Package ldifdiff is a fast library that outputs the difference
// between two LDIF files as a valid and importable LDIF (e.g.
// by your LDAP server).
package ldifdiff
import (
"bytes"
"sort"
"strings"
"sync"
)
// Used by the implementation program in the cmd directory.
const Version = "v0.2.0"
// Used by the implementation program in the cmd directory.
const Author = "Claudio Ramirez <pub.claudio@gmail.com>"
// Used by the implementation program in the cmd directory.
const Repo = "https://github.com/nxadm/ldifdiff"
type fn func(string, []string) (entries, error)
var skipDnForDelete map[string]bool
/* Public functions */
// Diff compares two LDIF strings (sourceStr and targetStr) and outputs the
// differences as a LDIF string. An array of attributes can be supplied. These
// attributes will be ignored when comparing the LDIF strings.
// The output is a string, a valid LDIF, and can be added to the "target"
// database (the one that created targetStr) in order to make it
// equal to the "source" database (the one that created sourceStr). In case of
// failure, an error is provided.
func Diff(sourceStr, targetStr string, ignoreAttr []string) (string, error) {
return genericDiff(sourceStr, targetStr, ignoreAttr, convertLdifStr, nil)
}
// DiffFromFiles compares two LDIF files (sourceFile and targetFile) and
// outputs the differences as a LDIF string. An array of attributes can be
// supplied. These attributes will be ignored when comparing the LDIF strings.
// The output is a string, a valid LDIF, and can be added to the "target"
// database (the one that created targetFile) in order to make it equal to the
// "source" database (the one that created sourceFile). In case of failure, an
// error is provided.
func DiffFromFiles(sourceFile, targetFile string, ignoreAttr []string) (string, error) {
return genericDiff(sourceFile, targetFile, ignoreAttr, importLdifFile, nil)
}
// ListDiffDn compares two LDIF strings (sourceStr and targetStr) and outputs
// the differences as a list of affected DNs (Dintinguished Names). An array of
// attributes can be supplied. These attributes will be ignored when comparing
// the LDIF strings.
// The output is a string slice. In case of failure, an error is provided.
func ListDiffDn(sourceStr, targetStr string, ignoreAttr []string) ([]string, error) {
dnList := []string{}
_, err := genericDiff(sourceStr, targetStr, ignoreAttr, convertLdifStr, &dnList)
return dnList, err
}
// ListDiffDnFromFiles compares two LDIF files (sourceFile and targetFileStr)
// and outputs the differences as a list of affected DNs (Dintinguished Names).
// An array of attributes can be supplied. These attributes will be ignored
// when comparing the LDIF strings.
// The output is a string slice. In case of failure, an error is provided.
func ListDiffDnFromFiles(sourceFile, targetFile string, ignoreAttr []string) ([]string, error) {
dnList := []string{}
_, err := genericDiff(sourceFile, targetFile, ignoreAttr, importLdifFile, &dnList)
return dnList, err
}
/* Package private functions */
func arraysEqual(a, b []string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// Ordering Logic:
// actionAdd: entries from source sorted S -> L. Otherwise is invalid.
// Remove: entries from target sorted L -> S. Otherwise is invalid.
// actionModify:
// - Keep S -> L ordering
// - If only 1 instance of attribute with different value on source and target:
// update. This way we don't break the applicable LDAP schema.
// - extra attribute on source: actionAdd
// - extra attribute on target: delete
func compare(source, target *entries, dnList *[]string) (string, error) {
var buffer bytes.Buffer
var err error
queue := make(chan actionEntry, 10)
var wg sync.WaitGroup
// Find the order in which operation must happen
orderedSourceShortToLong := sortDnByDepth(source, false)
orderedTargetLongToShort := sortDnByDepth(target, true)
// Write the file concurrently
wg.Add(1) // 1 writer
go writeLdif(queue, &buffer, &wg, &err)
// Dn only on source + removal of identical entries
skipDnForDelete = make(map[string]bool) // Keep track of dn to skip at Deletion
sendForAddition(&orderedSourceShortToLong, source, target, queue, dnList)
// Dn only on target
sendForDeletion(&orderedTargetLongToShort, source, target, queue, dnList)
// Dn on source and target
sendForModification(&orderedSourceShortToLong, source, target, queue, dnList)
// Done sending work
close(queue)
// Free some memory
*source = entries{}
*target = entries{}
// Wait for the creation of the LDIF
wg.Wait()
// Return the results
return buffer.String(), err
}
//func elementInArray(a string, array []string) bool {
// for _, b := range array {
// if b == a {
// return true
// }
// }
// return false
//}
func genericDiff(sourceParam, targetParam string, ignoreAttr []string, fn fn, dnList *[]string) (string, error) {
// Read the files in memory as a Map with sorted attributes
var source, target entries
var sourceErr, targetErr error
var wg sync.WaitGroup
wg.Add(2)
go func(entries *entries, wg *sync.WaitGroup, err *error) {
result, e := fn(sourceParam, ignoreAttr)
*entries = result
*err = e
wg.Done()
}(&source, &wg, &sourceErr)
go func(entries *entries, wg *sync.WaitGroup, err *error) {
result, e := fn(targetParam, ignoreAttr)
*entries = result
*err = e
wg.Done()
}(&target, &wg, &targetErr)
wg.Wait()
if sourceErr != nil {
return "", sourceErr
}
if targetErr != nil {
return "", targetErr
}
// Compare the files
return compare(&source, &target, dnList)
}
func sendForAddition(
orderedSourceShortToLong *[]string,
source, target *entries,
queue chan<- actionEntry,
dnList *[]string) {
for _, dn := range *orderedSourceShortToLong {
// Ignore equal entries
if arraysEqual((*source)[dn], (*target)[dn]) {
delete(*source, dn)
delete(*target, dn)
continue
}
// Mark entries for addition if only on source
if _, ok := (*target)[dn]; !ok {
if dnList == nil {
subActionAttr := make(map[subAction][]string)
subActionAttr[subActionNone] = (*source)[dn]
actionEntry :=
actionEntry{
Dn: dn,
Action: actionAdd,
SubActionAttrs: []subActionAttrs{subActionAttr},
}
queue <- actionEntry
} else {
// Always actionAdd (attributes not relevant)
*dnList = append(*dnList, dn)
}
delete(*source, dn)
}
// Implict else:
// It exists on target and it's not equal, so it's a modifyStr
skipDnForDelete[dn] = true
}
}
func sendForDeletion(
orderedTargetLongToShort *[]string,
source, target *entries,
queue chan<- actionEntry,
dnList *[]string) {
for _, dn := range *orderedTargetLongToShort {
if skipDnForDelete[dn] { // We know it's not a delete operation
continue
}
if _, ok := (*target)[dn]; ok { // It has not been deleted above
if _, ok := (*source)[dn]; !ok { // does not exists on source
if dnList == nil {
subActionAttr := make(map[subAction][]string)
subActionAttr[subActionNone] = nil
actionEntry :=
actionEntry{
Dn: dn,
Action: actionDelete,
SubActionAttrs: []subActionAttrs{subActionAttr},
}
queue <- actionEntry
} else {
// Always remove (attributes are not relevant)
*dnList = append(*dnList, dn)
}
delete(*target, dn)
}
// Implict else:
// It exists on source and it's not equal (tested on sendForAddition),
// so it's a modifyStr
}
}
// Free some memory
skipDnForDelete = nil
}
func sendForModification(
orderedSourceShortToLong *[]string, source,
target *entries,
queue chan<- actionEntry,
dnList *[]string) {
for _, dn := range *orderedSourceShortToLong {
// DN is present on source and target:
// sendForAdd/Remove clean up source and target
_, okSource := (*source)[dn]
_, okTarget := (*target)[dn]
if okSource && okTarget { // it hasn't been deleted
if dnList == nil {
// Store the attributes to be added, deleted or replaced
attrToModifyAdd := []string{}
attrToModifyDelete := []string{}
attrToModifyReplace := []string{}
// Put the attributes in a map for easy lookup
sourceAttr := make(map[string]bool)
targetAttr := make(map[string]bool)
for _, attr := range (*source)[dn] {
sourceAttr[attr] = true
}
for _, attr := range (*target)[dn] {
targetAttr[attr] = true
}
// Compare attribute values starting from the source
for _, attr := range (*source)[dn] { // Keep the order of the attributes
// Attribute is not equal on both sides
if _, ok := targetAttr[attr]; !ok {
// Is the attribute name (not value) unique?
switch uniqueAttrName(attr, sourceAttr, targetAttr) {
case true: // This is a actionModify-Replace operation
attrToModifyReplace = append(attrToModifyReplace, attr)
case false: // This is just a actionModify actionAdd (only on source).
attrToModifyAdd = append(attrToModifyAdd, attr)
}
}
}
// Compare attribute values starting from the target.
for _, attr := range (*target)[dn] { // Keep the order of the attributes
// Looking for unique attributes
if !uniqueAttrName(attr, sourceAttr, targetAttr) {
if _, ok := sourceAttr[attr]; !ok {
attrToModifyDelete = append(attrToModifyDelete, attr)
}
}
}
// Send it
actionEntry := actionEntry{Dn: dn, Action: actionModify}
subActionAttrArray := []subActionAttrs{}
switch {
case len(attrToModifyAdd) > 0:
subActionAttrArray = append(subActionAttrArray, subActionAttrs{subActionModifyAdd: attrToModifyAdd})
fallthrough
case len(attrToModifyDelete) > 0:
subActionAttrArray = append(subActionAttrArray, subActionAttrs{subActionModifyDelete: attrToModifyDelete})
fallthrough
case len(attrToModifyReplace) > 0:
subActionAttrArray = append(subActionAttrArray, subActionAttrs{subActionModifyReplace: attrToModifyReplace})
}
actionEntry.SubActionAttrs = subActionAttrArray
queue <- actionEntry
} else {
// There must be something left to modify
//if len((*source)[dn]) > 0 || len((*target)[dn]) > 0 {
*dnList = append(*dnList, dn)
//}
}
// Clean it up
delete(*source, dn)
delete(*target, dn)
}
}
}
func sortDnByDepth(entries *entries, longToShort bool) []string {
var sorted []string
dns := []string{}
for dn := range *entries {
dns = append(dns, dn)
}
splitByDc := make(map[string][]string)
longestDn := 0
// Split the components of the dn and remember the longest size
for _, dn := range dns {
parts := strings.Split(dn, ",")
if len(parts) > longestDn {
longestDn = len(parts)
}
splitByDc[dn] = parts
}
// Get the direction of the loop
componentSizes := []int{}
if longToShort {
for i := longestDn; i > 0; i-- {
componentSizes = append(componentSizes, i)
}
} else {
for i := 1; i <= longestDn; i++ {
componentSizes = append(componentSizes, i)
}
}
// Sort by size and alpahbetically within size
for _, size := range componentSizes {
sameSize := []string{}
for dn, components := range splitByDc {
if len(components) == size {
sameSize = append(sameSize, dn)
}
}
sort.Strings(sameSize)
sorted = append(sorted, sameSize...)
}
return sorted
}
func uniqueAttrName(attr string, sourceAttr, targetAttr map[string]bool) bool {
// Get the attribute name
parts := strings.Split(attr, ":")
attrName := parts[0]
//var base64 bool
//if parts[1] == "" {
// base64 = true
//}
sourceCounter := 0
for attr := range sourceAttr {
if strings.HasPrefix(attr, attrName+":") {
sourceCounter++
}
}
targetCounter := 0
for attr := range targetAttr {
if strings.HasPrefix(attr, attrName+":") {
targetCounter++
}
}
return sourceCounter == 1 && targetCounter == 1
}