1// Package config reads bounded transactional flat key=value configuration.
2package config
3
4import (
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "strings"
11
12 "github.com/bnema/kvconf"
13 "github.com/bnema/neferafk/internal/ports"
14)
15
16const MaxBytes = 64 << 10
17const MaxLine = 4 << 10
18
19func DefaultPath() string {
20 base := os.Getenv("XDG_CONFIG_HOME")
21 if base == "" {
22 if home, err := os.UserHomeDir(); err == nil {
23 base = filepath.Join(home, ".config")
24 }
25 }
26 return filepath.Join(base, "neferafk", "config")
27}
28
29func Load(path string) (ports.Config, error) {
30 data, err := kvconf.ReadFile(path, options)
31 if err != nil {
32 if errors.Is(err, kvconf.ErrTooLarge) {
33 return ports.Config{}, errTooLarge
34 }
35 return ports.Config{}, err
36 }
37 return parse(data)
38}
39
40// Parse never returns partial configuration and never resolves PIN references.
41func Parse(r io.Reader) (ports.Config, error) {
42 data, err := io.ReadAll(io.LimitReader(r, MaxBytes+1))
43 if err != nil {
44 return ports.Config{}, err
45 }
46 return parse(data)
47}
48
49// options: values are never unquoted, and parse cuts each one at its first
50// "#", even inside quotes. parse rejects the whole file on any problem.
51var options = kvconf.Options{Comments: kvconf.CommentLine, RawValues: true, MaxBytes: MaxBytes, MaxLine: MaxLine}
52
53var errTooLarge = errors.New("config exceeds 64KiB")
54
55// parse fails on the first problem in line order. Strict mode would report
56// a line without "=" or a duplicate before an earlier bad value, so the file
57// is decoded leniently and its problems compared; a syntax problem wins a tie.
58func parse(data []byte) (ports.Config, error) {
59 entries, syntax, err := kvconf.ParseBytes(data, options)
60 if err != nil {
61 return ports.Config{}, reject(err)
62 }
63 for i := range entries {
64 if v, _, cut := strings.Cut(entries[i].Value, "#"); cut {
65 entries[i].Value = strings.TrimSpace(v)
66 }
67 }
68 cfg := ports.Defaults()
69 values, err := kvconf.Decode(entries, &cfg, options)
70 switch {
71 case len(syntax) > 0 && (len(values) == 0 || syntax[0].Line <= values[0].Line):
72 return ports.Config{}, lineError(syntax[0])
73 case len(values) > 0:
74 return ports.Config{}, lineError(values[0])
75 case err != nil:
76 return ports.Config{}, err
77 }
78 return cfg, nil
79}
80
81func reject(err error) error {
82 switch {
83 case errors.Is(err, kvconf.ErrTooLarge):
84 return errTooLarge
85 case errors.Is(err, kvconf.ErrNotUTF8):
86 return errors.New("config is not valid UTF-8")
87 }
88 return err
89}
90
91// lineError carries a line number and a fixed message, never the key or
92// value, which may be secret.
93func lineError(w kvconf.Warning) error {
94 return fmt.Errorf("line %d: %s", w.Line, w.Msg)
95}