Mercurial Hosting > d2o
view 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 source
// 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 == '_' }
