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