Raw
  1package config
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"fmt"
  7	"reflect"
  8	"regexp"
  9	"sort"
 10	"strconv"
 11	"strings"
 12
 13	"github.com/pelletier/go-toml/v2"
 14	"github.com/pelletier/go-toml/v2/unstable"
 15)
 16
 17var (
 18	indexPattern    = regexp.MustCompile(`\[\d+\]`)
 19	bareKeyPattern  = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
 20	conflictPattern = regexp.MustCompile(`( is already defined| already exists| already exists as an? [a-z ]+| should be a table, not an? [a-z ]+|, but should be an array table)$`)
 21)
 22
 23const (
 24	kindTable = "table"
 25	kindArray = "array"
 26	kindValue = "value"
 27)
 28
 29func renderSegment(s string) string {
 30	if bareKeyPattern.MatchString(s) {
 31		return s
 32	}
 33	return strconv.Quote(s)
 34}
 35
 36func schemaPaths(t reflect.Type, prefix string, out map[string]string) {
 37	for t.Kind() == reflect.Pointer {
 38		t = t.Elem()
 39	}
 40	for i := 0; i < t.NumField(); i++ {
 41		f := t.Field(i)
 42		name, _, _ := strings.Cut(f.Tag.Get("toml"), ",")
 43		if name == "" || name == "-" {
 44			continue
 45		}
 46		path := name
 47		if prefix != "" {
 48			path = prefix + "." + name
 49		}
 50		out[path] = kindValue
 51		ft := f.Type
 52		for ft.Kind() == reflect.Pointer {
 53			ft = ft.Elem()
 54		}
 55		switch {
 56		case ft.Kind() == reflect.Struct:
 57			out[path] = kindTable
 58			schemaPaths(ft, path, out)
 59		case ft.Kind() == reflect.Slice && ft.Elem().Kind() == reflect.Struct:
 60			out[path] = kindArray
 61			out[path+"[]"] = kindTable
 62			schemaPaths(ft.Elem(), path+"[]", out)
 63		}
 64	}
 65}
 66
 67func allowedPaths() map[string]string {
 68	out := map[string]string{}
 69	schemaPaths(reflect.TypeOf(rawFile{}), "", out)
 70	return out
 71}
 72
 73func pathPattern(path string) string {
 74	return indexPattern.ReplaceAllString(path, "[]")
 75}
 76
 77type rangeEntry struct {
 78	path       string
 79	start, end int
 80	line       int
 81}
 82
 83type keyIndex struct {
 84	present    map[string]bool
 85	kinds      map[string]string
 86	entries    []rangeEntry
 87	lineStarts []int
 88	header     string
 89}
 90
 91func (k *keyIndex) has(path string) bool {
 92	return k.present[path]
 93}
 94
 95func (k *keyIndex) line(path string) int {
 96	best := 0
 97	for _, e := range k.entries {
 98		if e.path == path {
 99			return e.line
100		}
101		if (strings.HasPrefix(e.path, path+".") || strings.HasPrefix(e.path, path+"[")) && (best == 0 || e.line < best) {
102			best = e.line
103		}
104	}
105	return best
106}
107
108func (k *keyIndex) duplicateUnder(key string) (string, int) {
109	seen := map[string]int{}
110	best, line := "", 0
111	for _, e := range k.entries {
112		seen[e.path]++
113		if seen[e.path] == 2 && (e.path == key || strings.HasPrefix(e.path, key+".") || strings.HasPrefix(e.path, key+"[")) && len(e.path) > len(best) {
114			best, line = e.path, e.line
115		}
116	}
117	return best, line
118}
119
120func (k *keyIndex) offset(row, col int) int {
121	if row < 1 || row > len(k.lineStarts) {
122		return -1
123	}
124	return k.lineStarts[row-1] + col - 1
125}
126
127func (k *keyIndex) keyAt(row, col int) string {
128	off := k.offset(row, col)
129	best, bestLen := "", -1
130	for _, e := range k.entries {
131		if off >= e.start && off < e.end && (bestLen < 0 || e.end-e.start < bestLen) {
132			best, bestLen = e.path, e.end-e.start
133		}
134	}
135	return best
136}
137
138func lineStarts(data []byte) []int {
139	starts := []int{0}
140	for i, b := range data {
141		if b == '\n' {
142			starts = append(starts, i+1)
143		}
144	}
145	return starts
146}
147
148func buildRangeMap(data []byte) *keyIndex {
149	idx := &keyIndex{present: map[string]bool{}, kinds: map[string]string{}, lineStarts: lineStarts(data)}
150	arrays := map[string]int{}
151	var p unstable.Parser
152	p.Reset(data)
153	record := func(path string, n *unstable.Node) {
154		start := int(n.Raw.Offset)
155		end := start + int(n.Raw.Length)
156		idx.entries = append(idx.entries, rangeEntry{path: path, start: start, end: end, line: p.Shape(n.Raw).Start.Line})
157	}
158	var keyValue func(prefix string, kv *unstable.Node)
159	var value func(path string, v *unstable.Node)
160	keyValue = func(prefix string, kv *unstable.Node) {
161		segs, _ := keySegments(kv.Key())
162		path := strings.Join(segs, ".")
163		if prefix != "" {
164			path = prefix + "." + path
165		}
166		record(path, kv)
167		value(path, kv.Value())
168	}
169	value = func(path string, v *unstable.Node) {
170		if v == nil {
171			return
172		}
173		switch v.Kind {
174		case unstable.InlineTable:
175			it := v.Children()
176			for it.Next() {
177				if c := it.Node(); c.Kind == unstable.KeyValue {
178					keyValue(path, c)
179				}
180			}
181		case unstable.Array:
182			it := v.Children()
183			i := 0
184			for it.Next() {
185				if c := it.Node(); c.Kind == unstable.InlineTable {
186					value(fmt.Sprintf("%s[%d]", path, i), c)
187				}
188				i++
189			}
190		}
191	}
192	for p.NextExpression() {
193		e := p.Expression()
194		switch e.Kind {
195		case unstable.Table, unstable.ArrayTable:
196			segs, first := keySegments(e.Key())
197			idx.header = indexedHeader(segs, arrays, e.Kind == unstable.ArrayTable)
198			if first != nil {
199				record(idx.header, first)
200			}
201		case unstable.KeyValue:
202			keyValue(idx.header, e)
203		}
204	}
205	return idx
206}
207
208func keySegments(it unstable.Iterator) ([]string, *unstable.Node) {
209	var segs []string
210	var first *unstable.Node
211	for it.Next() {
212		n := it.Node()
213		if first == nil {
214			first = n
215		}
216		segs = append(segs, renderSegment(string(n.Data)))
217	}
218	return segs, first
219}
220
221func indexedHeader(segs []string, arrays map[string]int, isArray bool) string {
222	parts := make([]string, 0, len(segs))
223	for i, s := range segs {
224		counter := strings.Join(append(parts[:i:i], s), ".")
225		if isArray && i == len(segs)-1 {
226			arrays[counter]++
227		}
228		if n := arrays[counter]; n > 0 {
229			s = fmt.Sprintf("%s[%d]", s, n-1)
230		}
231		parts = append(parts, s)
232	}
233	return strings.Join(parts, ".")
234}
235
236func documentKind(v any) string {
237	switch t := v.(type) {
238	case map[string]any:
239		return kindTable
240	case []any:
241		for _, e := range t {
242			if _, ok := e.(map[string]any); !ok {
243				return kindValue
244			}
245		}
246		return kindArray
247	}
248	return kindValue
249}
250
251func walkDocument(prefix string, v any, idx *keyIndex) {
252	switch t := v.(type) {
253	case map[string]any:
254		for key, val := range t {
255			path := renderSegment(key)
256			if prefix != "" {
257				path = prefix + "." + path
258			}
259			idx.present[path] = true
260			idx.kinds[path] = documentKind(val)
261			walkDocument(path, val, idx)
262		}
263	case []any:
264		for i, e := range t {
265			if _, ok := e.(map[string]any); ok {
266				path := fmt.Sprintf("%s[%d]", prefix, i)
267				idx.present[path] = true
268				idx.kinds[path] = kindTable
269				walkDocument(path, e, idx)
270			}
271		}
272	}
273}
274
275func decodeProblem(file string, err error, idx *keyIndex, typed bool) Problem {
276	p := Problem{File: file, Reason: reasonSyntax}
277	var de *toml.DecodeError
278	if !errors.As(err, &de) {
279		return p
280	}
281	row, col := de.Position()
282	p.Line = row
283	msg := de.Error()
284	switch {
285	case typed && (strings.HasPrefix(msg, "toml: cannot decode TOML ") || strings.HasPrefix(msg, "toml: cannot store ")):
286		p.Reason = reasonWrongType
287	case !typed && conflictPattern.MatchString(msg):
288		p.Reason = reasonDuplicate
289	}
290	p.Key = idx.keyAt(row, col)
291	if p.Reason == reasonDuplicate {
292		if dup, line := idx.duplicateUnder(p.Key); dup != "" {
293			p.Key, p.Line = dup, line
294		}
295	}
296	if p.Key == "" && p.Reason == reasonSyntax {
297		p.Key = idx.header
298	}
299	return p
300}
301
302func indexKeys(file string, data []byte) (*keyIndex, []Problem) {
303	idx := buildRangeMap(data)
304	var doc map[string]any
305	if err := toml.NewDecoder(bytes.NewReader(data)).Decode(&doc); err != nil {
306		return nil, []Problem{decodeProblem(file, err, idx, false)}
307	}
308	walkDocument("", doc, idx)
309
310	allowed := allowedPaths()
311	paths := make([]string, 0, len(idx.present))
312	for path := range idx.present {
313		paths = append(paths, path)
314	}
315	sort.Strings(paths)
316	var problems []Problem
317	var rejected []string
318	for _, path := range paths {
319		pattern := pathPattern(path)
320		if underRejected(path, rejected) {
321			continue
322		}
323		if want, ok := allowed[pattern]; ok {
324			got := idx.kinds[path]
325			if (want == kindTable || want == kindArray) && got != want && !strings.HasSuffix(path, "]") {
326				rejected = append(rejected, path)
327				problems = append(problems, Problem{File: file, Line: idx.line(path), Key: path, Reason: reasonWrongType})
328			}
329			continue
330		}
331		if strings.HasSuffix(path, "]") {
332			continue
333		}
334		rejected = append(rejected, path)
335		reason := reasonUnknownKey
336		for a := range allowed {
337			if strings.EqualFold(a, pattern) {
338				reason = reasonUnknownKeyCase
339				break
340			}
341		}
342		problems = append(problems, Problem{File: file, Line: idx.line(path), Key: path, Reason: reason})
343	}
344	return idx, problems
345}
346
347func underRejected(path string, rejected []string) bool {
348	for _, r := range rejected {
349		if strings.HasPrefix(path, r+".") || strings.HasPrefix(path, r+"[") {
350			return true
351		}
352	}
353	return false
354}