From b4982d22be0c79949046c9d957354209996acb2e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 27 Jul 2026 12:12:19 +0200 Subject: feat(ini): dependency-free INI reader for config and calendar data --- internal/ini/ini.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 internal/ini/ini.go (limited to 'internal/ini/ini.go') diff --git a/internal/ini/ini.go b/internal/ini/ini.go new file mode 100644 index 0000000..b4fc955 --- /dev/null +++ b/internal/ini/ini.go @@ -0,0 +1,75 @@ +// Package ini is a tiny dependency-free reader for the boring INI files lectio +// uses for config and calendar data: [sections] (including "a/b" subsections), +// "key = value" pairs (keys may be dotted, e.g. name.pl), and "#"/";" comments. +// It preserves order and never interprets values beyond trimming. +package ini + +import ( + "bufio" + "bytes" + "fmt" + "strings" +) + +type Pair struct{ Key, Val string } + +type Section struct { + Name string + Pairs []Pair +} + +// Parse reads INI bytes into ordered sections. Pairs before the first [section] +// go into a leading section with an empty Name. +func Parse(data []byte) ([]Section, error) { + secs := []Section{{Name: ""}} + sc := bufio.NewScanner(bytes.NewReader(data)) + sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) + line := 0 + for sc.Scan() { + line++ + raw := stripComment(sc.Text()) + s := strings.TrimSpace(raw) + if s == "" { + continue + } + if strings.HasPrefix(s, "[") { + if !strings.HasSuffix(s, "]") { + return nil, fmt.Errorf("ini: line %d: unclosed section header %q", line, s) + } + name := strings.TrimSpace(s[1 : len(s)-1]) + secs = append(secs, Section{Name: name}) + continue + } + eq := strings.IndexByte(s, '=') + if eq < 0 { + return nil, fmt.Errorf("ini: line %d: expected key = value, got %q", line, s) + } + key := strings.TrimSpace(s[:eq]) + val := strings.TrimSpace(s[eq+1:]) + cur := &secs[len(secs)-1] + cur.Pairs = append(cur.Pairs, Pair{key, val}) + } + return secs, sc.Err() +} + +// stripComment removes a trailing "#" or ";" comment (our values never contain +// an unescaped # or ;). +func stripComment(line string) string { + for i := 0; i < len(line); i++ { + if line[i] == '#' || line[i] == ';' { + return line[:i] + } + } + return line +} + +// List splits a comma-separated value, trimming spaces and dropping empties. +func List(val string) []string { + var out []string + for _, p := range strings.Split(val, ",") { + if t := strings.TrimSpace(p); t != "" { + out = append(out, t) + } + } + return out +} -- cgit v1.3