1package ldif 2 3import ( 4 "encoding/base64" 5 "errors" 6 "fmt" 7 "io" 8 9 "github.com/go-ldap/ldap/v3" 10) 11 12var foldWidth = 76 13 14// ErrMixed is the error, that we cannot mix change records and content 15// records in one LDIF 16var ErrMixed = errors.New("cannot mix change records and content records") 17 18// Marshal returns an LDIF string from the given LDIF. 19// 20// The default line lenght is 76 characters. This can be changed by setting 21// the fw parameter to something else than 0. 22// For a fold width < 0, no folding will be done, with 0, the default is used. 23func Marshal(l *LDIF) (data string, err error) { 24 hasEntry := false 25 hasChange := false 26 27 if l.Version > 0 { 28 data = "version: 1\n" 29 } 30 31 fw := l.FoldWidth 32 if fw == 0 { 33 fw = foldWidth 34 } 35 36 for _, e := range l.Entries { 37 switch { 38 case e.Add != nil: 39 hasChange = true 40 if hasEntry { 41 return "", ErrMixed 42 } 43 data += foldLine("dn: "+e.Add.DN, fw) + "\n" 44 data += "changetype: add\n" 45 for _, add := range e.Add.Attributes { 46 if len(add.Vals) == 0 { 47 return "", errors.New("changetype 'add' requires non empty value list") 48 } 49 for _, v := range add.Vals { 50 ev, t := encodeValue(v) 51 col := ": " 52 if t { 53 col = ":: " 54 } 55 data += foldLine(add.Type+col+ev, fw) + "\n" 56 } 57 } 58 59 case e.Del != nil: 60 hasChange = true 61 if hasEntry { 62 return "", ErrMixed 63 } 64 data += foldLine("dn: "+e.Del.DN, fw) + "\n" 65 data += "changetype: delete\n" 66 67 case e.Modify != nil: 68 hasChange = true 69 if hasEntry { 70 return "", ErrMixed 71 } 72 data += foldLine("dn: "+e.Modify.DN, fw) + "\n" 73 data += "changetype: modify\n" 74 for _, mod := range e.Modify.Changes { 75 switch mod.Operation { 76 // add operation - https://tools.ietf.org/html/rfc4511#section-4.6 77 case 0: 78 if len(mod.Modification.Vals) == 0 { 79 return "", errors.New("changetype 'modify', op 'add' requires non empty value list") 80 } 81 82 data += "add: " + mod.Modification.Type + "\n" 83 for _, v := range mod.Modification.Vals { 84 ev, t := encodeValue(v) 85 col := ": " 86 if t { 87 col = ":: " 88 } 89 data += foldLine(mod.Modification.Type+col+ev, fw) + "\n" 90 } 91 data += "-\n" 92 // delete operation - https://tools.ietf.org/html/rfc4511#section-4.6 93 case 1: 94 data += "delete: " + mod.Modification.Type + "\n" 95 for _, v := range mod.Modification.Vals { 96 ev, t := encodeValue(v) 97 col := ": " 98 if t { 99 col = ":: " 100 } 101 data += foldLine(mod.Modification.Type+col+ev, fw) + "\n" 102 } 103 data += "-\n" 104 // replace operation - https://tools.ietf.org/html/rfc4511#section-4.6 105 case 2: 106 if len(mod.Modification.Vals) == 0 { 107 return "", errors.New("changetype 'modify', op 'replace' requires non empty value list") 108 } 109 data += "replace: " + mod.Modification.Type + "\n" 110 for _, v := range mod.Modification.Vals { 111 ev, t := encodeValue(v) 112 col := ": " 113 if t { 114 col = ":: " 115 } 116 data += foldLine(mod.Modification.Type+col+ev, fw) + "\n" 117 } 118 data += "-\n" 119 default: 120 return "", fmt.Errorf("invalid type %s in modify request", mod.Modification.Type) 121 } 122 } 123 default: 124 hasEntry = true 125 if hasChange { 126 return "", ErrMixed 127 } 128 data += foldLine("dn: "+e.Entry.DN, fw) + "\n" 129 for _, av := range e.Entry.Attributes { 130 for _, v := range av.Values { 131 ev, t := encodeValue(v) 132 col := ": " 133 if t { 134 col = ":: " 135 } 136 data += foldLine(av.Name+col+ev, fw) + "\n" 137 } 138 } 139 } 140 data += "\n" 141 } 142 return data, nil 143} 144 145func encodeValue(value string) (string, bool) { 146 required := false 147 for _, r := range value { 148 if r < ' ' || r > '~' { // ~ = 0x7E, <DEL> = 0x7F 149 required = true 150 break 151 } 152 } 153 if !required { 154 return value, false 155 } 156 return base64.StdEncoding.EncodeToString([]byte(value)), true 157} 158 159func foldLine(line string, fw int) (folded string) { 160 if fw < 0 { 161 return line 162 } 163 if len(line) <= fw { 164 return line 165 } 166 167 folded = line[:fw] + "\n" 168 line = line[fw:] 169 170 for len(line) > fw-1 { 171 folded += " " + line[:fw-1] + "\n" 172 line = line[fw-1:] 173 } 174 175 if len(line) > 0 { 176 folded += " " + line 177 } 178 return 179} 180 181// Dump writes the given entries to the io.Writer. 182// 183// The entries argument can be *ldap.Entry or a mix of *ldap.AddRequest, 184// *ldap.DelRequest, *ldap.ModifyRequest and *ldap.ModifyDNRequest or slices 185// of any of those. 186// 187// See Marshal() for the fw argument. 188func Dump(fh io.Writer, fw int, entries ...interface{}) error { 189 l, err := ToLDIF(entries...) 190 if err != nil { 191 return err 192 } 193 l.FoldWidth = fw 194 str, err := Marshal(l) 195 if err != nil { 196 return err 197 } 198 _, err = fh.Write([]byte(str)) 199 return err 200} 201 202// ToLDIF puts the given arguments in an LDIF struct and returns it. 203// 204// The entries argument can be *ldap.Entry or a mix of *ldap.AddRequest, 205// *ldap.DelRequest, *ldap.ModifyRequest and *ldap.ModifyDNRequest or slices 206// of any of those. 207func ToLDIF(entries ...interface{}) (*LDIF, error) { 208 l := &LDIF{} 209 for _, e := range entries { 210 switch e.(type) { 211 case []*ldap.Entry: 212 for _, en := range e.([]*ldap.Entry) { 213 l.Entries = append(l.Entries, &Entry{Entry: en}) 214 } 215 216 case *ldap.Entry: 217 l.Entries = append(l.Entries, &Entry{Entry: e.(*ldap.Entry)}) 218 219 case []*ldap.AddRequest: 220 for _, en := range e.([]*ldap.AddRequest) { 221 l.Entries = append(l.Entries, &Entry{Add: en}) 222 } 223 224 case *ldap.AddRequest: 225 l.Entries = append(l.Entries, &Entry{Add: e.(*ldap.AddRequest)}) 226 227 case []*ldap.DelRequest: 228 for _, en := range e.([]*ldap.DelRequest) { 229 l.Entries = append(l.Entries, &Entry{Del: en}) 230 } 231 232 case *ldap.DelRequest: 233 l.Entries = append(l.Entries, &Entry{Del: e.(*ldap.DelRequest)}) 234 235 case []*ldap.ModifyRequest: 236 for _, en := range e.([]*ldap.ModifyRequest) { 237 l.Entries = append(l.Entries, &Entry{Modify: en}) 238 } 239 case *ldap.ModifyRequest: 240 l.Entries = append(l.Entries, &Entry{Modify: e.(*ldap.ModifyRequest)}) 241 242 default: 243 return nil, fmt.Errorf("unsupported type %T", e) 244 } 245 } 246 return l, nil 247} 248