Mercurial Hosting > d2o
diff icf/icf.go @ 0:48bdab3eec8a
Initial
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 09 Mar 2026 00:37:49 +0500 |
| parents | |
| children | d19133be91ba |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/icf/icf.go Mon Mar 09 00:37:49 2026 +0500 @@ -0,0 +1,321 @@ +// Package icf implements the Inherited Configuration Format parser. +// +// ICF is a rule-based configuration format with variables, abstract blocks +// (mixins), pattern matching with named capture groups, and brace expansion. +// +// Syntax: +// +// ; comment +// KEY=value variable +// @name abstract block (mixin) +// |> directive arg {a,b} directive with optional brace expansion +// block.id @mixin concrete block inheriting a mixin +// <cap>.example.com block with named capture group +// <_>.example.com anonymous wildcard +package icf + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// Directive is a single key + arguments line inside a block. +type Directive struct { + Key string + Args []string +} + +// Config holds the parsed and fully resolved state of an ICF file. +type Config struct { + vars map[string]string + abstract map[string][]Directive + Blocks []ParsedBlock +} + +type ParsedBlock struct { + ID string + Mixin string + Directives []Directive +} + +// Parse reads an ICF document and returns a ready Config. +func Parse(r io.Reader) (*Config, error) { + c := &Config{ + vars: make(map[string]string), + abstract: make(map[string][]Directive), + } + + var raw []ParsedBlock + var cur *ParsedBlock + + flush := func() { + if cur != nil { + raw = append(raw, *cur) + cur = nil + } + } + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := stripComment(strings.TrimSpace(scanner.Text())) + if line == "" { + continue + } + + // Variable assignment: KEY=value + if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { + key := line[:i] + if isVarName(key) { + c.vars[key] = strings.TrimSpace(line[i+1:]) + continue + } + } + + // Directive: |> key args... + if strings.HasPrefix(line, "|>") { + if cur == nil { + return nil, fmt.Errorf("icf: directive outside block: %q", line) + } + parts := strings.Fields(strings.TrimSpace(line[2:])) + if len(parts) == 0 { + continue + } + cur.Directives = append(cur.Directives, Directive{ + Key: parts[0], + Args: braceExpand(parts[1:]), + }) + continue + } + + // Block header: id [@mixin] + flush() + parts := strings.Fields(line) + pb := ParsedBlock{ID: parts[0]} + if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") { + pb.Mixin = parts[1][1:] + } + cur = &pb + } + flush() + + if err := scanner.Err(); err != nil { + return nil, err + } + + // Separate abstract from concrete blocks + for _, b := range raw { + if strings.HasPrefix(b.ID, "@") { + c.abstract[b.ID[1:]] = b.Directives + } else { + c.Blocks = append(c.Blocks, b) + } + } + + return c, nil +} + +// Abstract returns the directives of a named abstract block with variables +// substituted. Returns nil if not found. +func (c *Config) Abstract(name string) []Directive { + dirs, ok := c.abstract[name] + if !ok { + return nil + } + return c.applyVars(dirs, nil) +} + +// Match finds the most specific block matching input (e.g. "host/path") and +// returns resolved directives plus named captures. +// Domain part is matched exactly (with captures); path part uses prefix match. +func (c *Config) Match(input string) ([]Directive, map[string]string) { + inHost, inPath, _ := strings.Cut(input, "/") + + type hit struct { + block ParsedBlock + captures map[string]string + score int + } + var best *hit + + for _, b := range c.Blocks { + patHost, patPath, hasPath := strings.Cut(b.ID, "/") + + caps := make(map[string]string) + + domScore, ok := matchExact(patHost, inHost, caps) + if !ok { + continue + } + + pathScore := 0 + if hasPath { + pathScore, ok = matchPrefix(patPath, inPath, caps) + if !ok { + continue + } + } + + score := domScore*1000 + pathScore + if best == nil || score > best.score { + best = &hit{block: b, captures: caps, score: score} + } + } + + if best == nil { + return nil, nil + } + + return c.ResolveBlock(best.block, best.captures), best.captures +} + +// ResolveBlock merges mixin then block directives, substituting vars+captures. +func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive { + var merged []Directive + if b.Mixin != "" { + merged = append(merged, c.abstract[b.Mixin]...) + } + merged = append(merged, b.Directives...) + return c.applyVars(merged, caps) +} + +func (c *Config) applyVars(dirs []Directive, caps map[string]string) []Directive { + out := make([]Directive, len(dirs)) + for i, d := range dirs { + out[i].Key = c.subst(d.Key, caps) + out[i].Args = make([]string, len(d.Args)) + for j, a := range d.Args { + out[i].Args[j] = c.subst(a, caps) + } + } + return out +} + +func (c *Config) subst(s string, caps map[string]string) string { + if !strings.Contains(s, "$") { + return s + } + var b strings.Builder + i := 0 + for i < len(s) { + if s[i] != '$' { + b.WriteByte(s[i]) + i++ + continue + } + j := i + 1 + for j < len(s) && isVarChar(s[j]) { + j++ + } + name := s[i+1 : j] + if v, ok := caps[name]; ok { + b.WriteString(v) + } else if v, ok := c.vars[name]; ok { + b.WriteString(v) + } else { + b.WriteString(s[i:j]) + } + i = j + } + return b.String() +} + +func matchExact(pat, s string, caps map[string]string) (int, bool) { + score, rem, ok := matchCaptures(pat, s, caps) + if !ok || rem != "" { + return 0, false + } + return score, true +} + +func matchPrefix(pat, s string, caps map[string]string) (int, bool) { + score, _, ok := matchCaptures(pat, s, caps) + return score, ok +} + +func matchCaptures(pat, inp string, caps map[string]string) (int, string, bool) { + for { + if pat == "" { + return 0, inp, true + } + + if strings.HasPrefix(pat, "<") { + end := strings.Index(pat, ">") + if end == -1 { + return 0, "", false + } + capName := pat[1:end] + rest := pat[end+1:] + + for split := 1; split <= len(inp); split++ { + candidate := inp[:split] + remaining := inp[split:] + score, finalRem, ok := matchCaptures(rest, remaining, caps) + if ok { + if capName != "_" { + caps[capName] = candidate + } + return score, finalRem, true + } + } + return 0, "", false + } + + if inp == "" || pat[0] != inp[0] { + return 0, "", false + } + score, rem, ok := matchCaptures(pat[1:], inp[1:], caps) + return score + 1, rem, ok + } +} + +func braceExpand(args []string) []string { + var out []string + for _, a := range args { + out = append(out, expandOne(a)...) + } + return out +} + +func expandOne(s string) []string { + start := strings.Index(s, "{") + end := strings.Index(s, "}") + if start == -1 || end == -1 || end < start { + return []string{s} + } + prefix := s[:start] + suffix := s[end+1:] + var out []string + for _, v := range strings.Split(s[start+1:end], ",") { + out = append(out, prefix+strings.TrimSpace(v)+suffix) + } + return out +} + +func stripComment(line string) string { + if strings.HasPrefix(line, ";") { + return "" + } + if i := strings.Index(line, " ;"); i != -1 { + return strings.TrimSpace(line[:i]) + } + return line +} + +func isVarName(s string) bool { + if s == "" { + return false + } + for i := 0; i < len(s); i++ { + if !isVarChar(s[i]) { + return false + } + } + return true +} + +func isVarChar(c byte) bool { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' +}
