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