Mercurial Hosting > d2o
view icf/icf.go @ 8:2ffb8028ccbb
add loggingmodes and fix path matching
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Tue, 17 Mar 2026 19:55:07 +0500 |
| parents | 54ab94198677 |
| children |
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. package icf import ( "bufio" "fmt" "io" "strings" ) type Directive struct { Key string Args []string } type Config struct { vars map[string]string abstract map[string][]Directive abstractMixin map[string]string // mixin name for each abstract block Blocks []ParsedBlock } type ParsedBlock struct { ID string Mixin string Directives []Directive } type rawBlock struct { id string mixin string directives []rawDirective } type rawDirective struct { key string args []string // after brace expansion, before var substitution } func Parse(r io.Reader) (*Config, error) { lines, err := readLines(r) if err != nil { return nil, err } // --- Pass 1: collect variables --- vars := make(map[string]string) subst := makeSubst(vars) for _, line := range lines { if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { key := line[:i] if isVarName(key) { vars[key] = subst(strings.TrimSpace(line[i+1:]), nil) } } } // --- Pass 2: parse blocks (raw, no capture substitution yet) --- var raws []rawBlock var cur *rawBlock flush := func() { if cur != nil { raws = append(raws, *cur) cur = nil } } for _, line := range lines { if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { if isVarName(line[:i]) { continue } } 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, rawDirective{ key: parts[0], args: braceExpand(parts[1:]), }) continue } flush() parts := strings.Fields(line) rb := rawBlock{id: subst(parts[0], nil)} if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") { rb.mixin = subst(parts[1][1:], nil) } cur = &rb } flush() // --- Pass 3: separate abstract from concrete, apply var substitution --- c := &Config{ vars: vars, abstract: make(map[string][]Directive), abstractMixin: make(map[string]string), } for _, rb := range raws { dirs := make([]Directive, len(rb.directives)) for i, rd := range rb.directives { args := make([]string, len(rd.args)) for j, a := range rd.args { args[j] = subst(a, nil) } dirs[i] = Directive{Key: rd.key, Args: args} } if strings.HasPrefix(rb.id, "@") { name := rb.id[1:] c.abstract[name] = dirs if rb.mixin != "" { c.abstractMixin[name] = rb.mixin } } else { c.Blocks = append(c.Blocks, ParsedBlock{ ID: rb.id, Mixin: rb.mixin, Directives: dirs, }) } } return c, nil } // Abstract returns the directives of a named abstract block. // Returns nil if not found. func (c *Config) Abstract(name string) []Directive { return c.abstract[name] } // Match finds the most specific block matching input (e.g. "host/path") and // returns resolved directives plus named captures. // Domain is matched exactly; path 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 directives (lower priority) with block directives, // then substitutes capture variables. func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive { var merged []Directive if b.Mixin != "" { merged = append(merged, c.resolveAbstract(b.Mixin, make(map[string]bool))...) } merged = append(merged, b.Directives...) if len(caps) == 0 { return merged } // Substitute capture variables into a copy subst := makeSubst(c.vars) out := make([]Directive, len(merged)) for i, d := range merged { out[i].Key = d.Key out[i].Args = make([]string, len(d.Args)) for j, a := range d.Args { out[i].Args[j] = subst(a, caps) } } return out } // resolveAbstract recursively expands an abstract block and its mixin chain. // visited guards against circular references. func (c *Config) resolveAbstract(name string, visited map[string]bool) []Directive { if visited[name] { return nil } visited[name] = true var out []Directive if parent, ok := c.abstractMixin[name]; ok { out = append(out, c.resolveAbstract(parent, visited)...) } out = append(out, c.abstract[name]...) return out } // makeSubst returns a function that substitutes $VAR in s, // checking caps first, then vars. func makeSubst(vars map[string]string) func(s string, caps map[string]string) string { return func(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 caps != nil { if v, ok := caps[name]; ok { b.WriteString(v) i = j continue } } if v, ok := vars[name]; ok { b.WriteString(v) } else { b.WriteString(s[i:j]) } i = j } return b.String() } } func readLines(r io.Reader) ([]string, error) { var out []string scanner := bufio.NewScanner(r) for scanner.Scan() { line := stripComment(strings.TrimSpace(scanner.Text())) if line != "" { out = append(out, line) } } return out, scanner.Err() } 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 := len(inp); split >= 1; split-- { score, finalRem, ok := matchCaptures(rest, inp[split:], caps) if ok { if capName != "_" { caps[capName] = inp[:split] } 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 == '_' }
