main b4521f0

marshal.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close