1package kvconf
2
3import (
4 "encoding"
5 "errors"
6 "fmt"
7 "maps"
8 "reflect"
9 "slices"
10 "strconv"
11 "strings"
12 "time"
13 "unicode/utf8"
14 "unsafe"
15)
16
17// Quote returns v as it must be written so Parse reads it back unchanged:
18// as is when possible, else as a Go "..." string.
19func Quote(v string) string {
20 if needsQuote(v) {
21 return strconv.Quote(v)
22 }
23 return v
24}
25
26func needsQuote(v string) bool {
27 if v == "" {
28 return true
29 }
30 first, last := v[0], v[len(v)-1]
31 if first >= utf8.RuneSelf || last >= utf8.RuneSelf {
32 if v != strings.TrimSpace(v) {
33 return true
34 }
35 } else if asciiSpace(first) || asciiSpace(last) {
36 return true
37 }
38 if first == '"' || first == '\'' {
39 return true // it would be unquoted on reading
40 }
41 hash := false
42 for i := 0; i < len(v); i++ {
43 switch v[i] {
44 case '\n', '\r':
45 return true
46 case '#':
47 hash = true
48 }
49 }
50 // A '#' after a space ends the value unless it is inside a quoted word,
51 // so quote any value where Parse would cut it.
52 return hash && stripComment(v, CommentSpaced) != v
53}
54
55// asciiSpace reports the ASCII bytes that strings.TrimSpace removes.
56func asciiSpace(c byte) bool {
57 return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'
58}
59
60// validKey reports whether key can be written and read back as one key.
61// A leading U+FEFF would be read as the byte order mark of a first line.
62func validKey(key string) bool {
63 return key != "" && key[0] != '#' && key == strings.TrimSpace(key) &&
64 !strings.HasPrefix(key, "\ufeff") && !strings.ContainsAny(key, "=\n\r")
65}
66
67func appendQuoted(b []byte, v string) []byte {
68 if needsQuote(v) {
69 return strconv.AppendQuote(b, v)
70 }
71 return append(b, v...)
72}
73
74// Marshal writes the tagged fields of v (a struct or a pointer to one) as
75// `key = value` lines in declaration order; map entries are sorted by name.
76func Marshal(v any) ([]byte, error) {
77 rv := reflect.Indirect(reflect.ValueOf(v))
78 if rv.Kind() != reflect.Struct {
79 return nil, errors.New("kvconf: Marshal needs a struct")
80 }
81 s, err := schemaOf(rv.Type())
82 if err != nil {
83 return nil, err
84 }
85 // Size the output once: about 24 bytes of value per line.
86 size, maxLen := 0, 0
87 for _, f := range s.order {
88 if f.elem == nil && f.val == nil {
89 size += len(f.key) + 24
90 continue
91 }
92 n := rv.FieldByIndex(f.index).Len()
93 maxLen = max(maxLen, n)
94 if f.elem == nil {
95 size += n * (2*len(f.key) + 24)
96 continue
97 }
98 for _, sub := range f.elem.order {
99 size += n * (len(f.key) + len(sub.key) + 32)
100 }
101 }
102 m := marshaler{b: make([]byte, 0, size), tmp: make([]byte, 0, 64)}
103 // Map names and their sort order, reused for every map.
104 names := make([]string, 0, maxLen)
105 order := make([]int, 0, maxLen)
106 for _, f := range s.order {
107 fv := rv.FieldByIndex(f.index)
108 if f.elem == nil && f.val == nil {
109 if err := m.line(f.key, "", "", fv, f); err != nil {
110 return nil, err
111 }
112 continue
113 }
114 // Copy the map out once, into reused values, then sort by name.
115 n := fv.Len()
116 names, order = names[:0], order[:0]
117 vals := reflect.MakeSlice(reflect.SliceOf(f.typ.Elem()), n, n)
118 key := reflect.New(f.typ.Key()).Elem()
119 for it, i := fv.MapRange(), 0; it.Next(); i++ {
120 key.SetIterKey(it)
121 names = append(names, key.String())
122 order = append(order, i)
123 vals.Index(i).SetIterValue(it)
124 }
125 slices.SortFunc(order, func(a, b int) int { return strings.Compare(names[a], names[b]) })
126 for _, i := range order {
127 name, ev := names[i], vals.Index(i)
128 // Checked in name order, so the error is the same on every run.
129 if strings.ContainsAny(name, "=\n\r") || name != strings.TrimSpace(name) {
130 return nil, fmt.Errorf("kvconf: %s: invalid map name %q", f.key, name)
131 }
132 if f.elem == nil {
133 if err := m.line(f.key, name, "", ev, f.val); err != nil {
134 return nil, err
135 }
136 continue
137 }
138 for _, sub := range f.elem.order {
139 if err := m.line(f.key, name, sub.key, ev.FieldByIndex(sub.index), sub); err != nil {
140 return nil, err
141 }
142 }
143 }
144 }
145 if s.catchAll != nil {
146 fv := rv.FieldByIndex(s.catchAll.index)
147 if es, ok := fv.Interface().([]Entry); ok {
148 for _, e := range es {
149 if err := m.raw(e.Key, e.Value); err != nil {
150 return nil, err
151 }
152 }
153 } else {
154 kv := fv.Interface().(map[string]string)
155 for _, k := range slices.Sorted(maps.Keys(kv)) {
156 if err := m.raw(k, kv[k]); err != nil {
157 return nil, err
158 }
159 }
160 }
161 }
162 return m.b, nil
163}
164
165type marshaler struct {
166 b []byte
167 tmp []byte // the value being formatted, reused
168}
169
170func (m *marshaler) raw(key, value string) error {
171 if !validKey(key) {
172 return fmt.Errorf("kvconf: invalid key %q", key)
173 }
174 m.b = append(m.b, key...)
175 m.b = append(m.b, " = "...)
176 m.b = appendQuoted(m.b, value)
177 m.b = append(m.b, '\n')
178 return nil
179}
180
181// line writes prefix+name[.sub] = value.
182func (m *marshaler) line(prefix, name, sub string, v reflect.Value, f *field) error {
183 var err error
184 if m.tmp, err = appendValue(m.tmp[:0], v, f); err != nil {
185 key := prefix + name
186 if sub != "" {
187 key += "." + sub
188 }
189 return fmt.Errorf("kvconf: %s: %w", key, err)
190 }
191 m.b = append(m.b, prefix...)
192 m.b = append(m.b, name...)
193 if sub != "" {
194 m.b = append(m.b, '.')
195 m.b = append(m.b, sub...)
196 }
197 m.b = append(m.b, " = "...)
198 // tmp is only read here, before its next reuse.
199 m.b = appendQuoted(m.b, unsafe.String(unsafe.SliceData(m.tmp), len(m.tmp)))
200 m.b = append(m.b, '\n')
201 return nil
202}
203
204func appendValue(b []byte, v reflect.Value, f *field) ([]byte, error) {
205 if f.off && v.IsZero() {
206 return append(b, "off"...), nil
207 }
208 if f.marshal {
209 if !v.CanAddr() { // a map element: copy it to call a pointer method
210 p := reflect.New(v.Type())
211 p.Elem().Set(v)
212 v = p.Elem()
213 }
214 var tm encoding.TextMarshaler
215 if v.Type().Implements(marshalType) {
216 tm = v.Interface().(encoding.TextMarshaler)
217 } else {
218 tm = v.Addr().Interface().(encoding.TextMarshaler)
219 }
220 text, err := tm.MarshalText()
221 return append(b, text...), err
222 }
223 switch v.Kind() {
224 case reflect.String:
225 return append(b, v.String()...), nil
226 case reflect.Bool:
227 if v.Bool() {
228 return append(b, "on"...), nil
229 }
230 return append(b, "off"...), nil
231 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
232 if f.duration {
233 return append(b, time.Duration(v.Int()).String()...), nil
234 }
235 return strconv.AppendInt(b, v.Int(), 10), nil
236 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
237 return strconv.AppendUint(b, v.Uint(), 10), nil
238 case reflect.Float32, reflect.Float64:
239 return strconv.AppendFloat(b, v.Float(), 'f', -1, v.Type().Bits()), nil
240 case reflect.Slice:
241 for i := range v.Len() {
242 if i > 0 {
243 b = append(b, f.sep...)
244 }
245 var err error
246 if b, err = appendValue(b, v.Index(i), f.item); err != nil {
247 return b, err
248 }
249 }
250 return b, nil
251 }
252 return b, fmt.Errorf("unsupported type %s", v.Type())
253}