comparison icf/icf.go @ 2:d19133be91ba

ndex and smarter parser
author Atarwn Gard <a@qwa.su>
date Mon, 09 Mar 2026 01:55:11 +0500
parents 48bdab3eec8a
children dacc92aae6d5
comparison
equal deleted inserted replaced
1:3e7247db5c6e 2:d19133be91ba
1 // Package icf implements the Inherited Configuration Format parser. 1 // Package icf implements the Inherited Configuration Format parser.
2 // 2 //
3 // ICF is a rule-based configuration format with variables, abstract blocks 3 // ICF is a rule-based configuration format with variables, abstract blocks
4 // (mixins), pattern matching with named capture groups, and brace expansion. 4 // (mixins), pattern matching with named capture groups, and brace expansion.
5 //
6 // Syntax:
7 //
8 // ; comment
9 // KEY=value variable
10 // @name abstract block (mixin)
11 // |> directive arg {a,b} directive with optional brace expansion
12 // block.id @mixin concrete block inheriting a mixin
13 // <cap>.example.com block with named capture group
14 // <_>.example.com anonymous wildcard
15 package icf 5 package icf
16 6
17 import ( 7 import (
18 "bufio" 8 "bufio"
19 "fmt" 9 "fmt"
20 "io" 10 "io"
21 "strings" 11 "strings"
22 ) 12 )
23 13
24 // Directive is a single key + arguments line inside a block.
25 type Directive struct { 14 type Directive struct {
26 Key string 15 Key string
27 Args []string 16 Args []string
28 } 17 }
29 18
30 // Config holds the parsed and fully resolved state of an ICF file.
31 type Config struct { 19 type Config struct {
32 vars map[string]string 20 vars map[string]string
33 abstract map[string][]Directive 21 abstract map[string][]Directive
34 Blocks []ParsedBlock 22 Blocks []ParsedBlock
35 } 23 }
38 ID string 26 ID string
39 Mixin string 27 Mixin string
40 Directives []Directive 28 Directives []Directive
41 } 29 }
42 30
43 // Parse reads an ICF document and returns a ready Config. 31 type rawBlock struct {
32 id string
33 mixin string
34 directives []rawDirective
35 }
36
37 type rawDirective struct {
38 key string
39 args []string // after brace expansion, before var substitution
40 }
41
44 func Parse(r io.Reader) (*Config, error) { 42 func Parse(r io.Reader) (*Config, error) {
45 c := &Config{ 43 lines, err := readLines(r)
46 vars: make(map[string]string), 44 if err != nil {
47 abstract: make(map[string][]Directive), 45 return nil, err
48 } 46 }
49 47
50 var raw []ParsedBlock 48 // --- Pass 1: collect variables ---
51 var cur *ParsedBlock 49 vars := make(map[string]string)
52 50 for _, line := range lines {
53 flush := func() {
54 if cur != nil {
55 raw = append(raw, *cur)
56 cur = nil
57 }
58 }
59
60 scanner := bufio.NewScanner(r)
61 for scanner.Scan() {
62 line := stripComment(strings.TrimSpace(scanner.Text()))
63 if line == "" {
64 continue
65 }
66
67 // Variable assignment: KEY=value
68 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { 51 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") {
69 key := line[:i] 52 key := line[:i]
70 if isVarName(key) { 53 if isVarName(key) {
71 c.vars[key] = strings.TrimSpace(line[i+1:]) 54 vars[key] = strings.TrimSpace(line[i+1:])
55 }
56 }
57 }
58
59 subst := makeSubst(vars)
60
61 // --- Pass 2: parse blocks (raw, no capture substitution yet) ---
62 var raws []rawBlock
63 var cur *rawBlock
64
65 flush := func() {
66 if cur != nil {
67 raws = append(raws, *cur)
68 cur = nil
69 }
70 }
71
72 for _, line := range lines {
73 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") {
74 if isVarName(line[:i]) {
72 continue 75 continue
73 } 76 }
74 } 77 }
75 78
76 // Directive: |> key args...
77 if strings.HasPrefix(line, "|>") { 79 if strings.HasPrefix(line, "|>") {
78 if cur == nil { 80 if cur == nil {
79 return nil, fmt.Errorf("icf: directive outside block: %q", line) 81 return nil, fmt.Errorf("icf: directive outside block: %q", line)
80 } 82 }
81 parts := strings.Fields(strings.TrimSpace(line[2:])) 83 parts := strings.Fields(strings.TrimSpace(line[2:]))
82 if len(parts) == 0 { 84 if len(parts) == 0 {
83 continue 85 continue
84 } 86 }
85 cur.Directives = append(cur.Directives, Directive{ 87 cur.directives = append(cur.directives, rawDirective{
86 Key: parts[0], 88 key: parts[0],
87 Args: braceExpand(parts[1:]), 89 args: braceExpand(parts[1:]),
88 }) 90 })
89 continue 91 continue
90 } 92 }
91 93
92 // Block header: id [@mixin]
93 flush() 94 flush()
94 parts := strings.Fields(line) 95 parts := strings.Fields(line)
95 pb := ParsedBlock{ID: parts[0]} 96 rb := rawBlock{id: subst(parts[0], nil)}
96 if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") { 97 if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") {
97 pb.Mixin = parts[1][1:] 98 rb.mixin = subst(parts[1][1:], nil)
98 } 99 }
99 cur = &pb 100 cur = &rb
100 } 101 }
101 flush() 102 flush()
102 103
103 if err := scanner.Err(); err != nil { 104 // --- Pass 3: separate abstract from concrete, apply var substitution ---
104 return nil, err 105 c := &Config{
105 } 106 vars: vars,
106 107 abstract: make(map[string][]Directive),
107 // Separate abstract from concrete blocks 108 }
108 for _, b := range raw { 109
109 if strings.HasPrefix(b.ID, "@") { 110 for _, rb := range raws {
110 c.abstract[b.ID[1:]] = b.Directives 111 dirs := make([]Directive, len(rb.directives))
112 for i, rd := range rb.directives {
113 args := make([]string, len(rd.args))
114 for j, a := range rd.args {
115 args[j] = subst(a, nil)
116 }
117 dirs[i] = Directive{Key: rd.key, Args: args}
118 }
119
120 if strings.HasPrefix(rb.id, "@") {
121 c.abstract[rb.id[1:]] = dirs
111 } else { 122 } else {
112 c.Blocks = append(c.Blocks, b) 123 c.Blocks = append(c.Blocks, ParsedBlock{
124 ID: rb.id,
125 Mixin: rb.mixin,
126 Directives: dirs,
127 })
113 } 128 }
114 } 129 }
115 130
116 return c, nil 131 return c, nil
117 } 132 }
118 133
119 // Abstract returns the directives of a named abstract block with variables 134 // Abstract returns the directives of a named abstract block.
120 // substituted. Returns nil if not found. 135 // Returns nil if not found.
121 func (c *Config) Abstract(name string) []Directive { 136 func (c *Config) Abstract(name string) []Directive {
122 dirs, ok := c.abstract[name] 137 return c.abstract[name]
123 if !ok {
124 return nil
125 }
126 return c.applyVars(dirs, nil)
127 } 138 }
128 139
129 // Match finds the most specific block matching input (e.g. "host/path") and 140 // Match finds the most specific block matching input (e.g. "host/path") and
130 // returns resolved directives plus named captures. 141 // returns resolved directives plus named captures.
131 // Domain part is matched exactly (with captures); path part uses prefix match. 142 // Domain is matched exactly; path uses prefix match.
132 func (c *Config) Match(input string) ([]Directive, map[string]string) { 143 func (c *Config) Match(input string) ([]Directive, map[string]string) {
133 inHost, inPath, _ := strings.Cut(input, "/") 144 inHost, inPath, _ := strings.Cut(input, "/")
134 145
135 type hit struct { 146 type hit struct {
136 block ParsedBlock 147 block ParsedBlock
139 } 150 }
140 var best *hit 151 var best *hit
141 152
142 for _, b := range c.Blocks { 153 for _, b := range c.Blocks {
143 patHost, patPath, hasPath := strings.Cut(b.ID, "/") 154 patHost, patPath, hasPath := strings.Cut(b.ID, "/")
144
145 caps := make(map[string]string) 155 caps := make(map[string]string)
146 156
147 domScore, ok := matchExact(patHost, inHost, caps) 157 domScore, ok := matchExact(patHost, inHost, caps)
148 if !ok { 158 if !ok {
149 continue 159 continue
168 } 178 }
169 179
170 return c.ResolveBlock(best.block, best.captures), best.captures 180 return c.ResolveBlock(best.block, best.captures), best.captures
171 } 181 }
172 182
173 // ResolveBlock merges mixin then block directives, substituting vars+captures. 183 // ResolveBlock merges mixin directives (lower priority) with block directives,
184 // then substitutes capture variables.
174 func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive { 185 func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive {
175 var merged []Directive 186 var merged []Directive
176 if b.Mixin != "" { 187 if b.Mixin != "" {
177 merged = append(merged, c.abstract[b.Mixin]...) 188 merged = append(merged, c.abstract[b.Mixin]...)
178 } 189 }
179 merged = append(merged, b.Directives...) 190 merged = append(merged, b.Directives...)
180 return c.applyVars(merged, caps) 191
181 } 192 if len(caps) == 0 {
182 193 return merged
183 func (c *Config) applyVars(dirs []Directive, caps map[string]string) []Directive { 194 }
184 out := make([]Directive, len(dirs)) 195
185 for i, d := range dirs { 196 // Substitute capture variables into a copy
186 out[i].Key = c.subst(d.Key, caps) 197 subst := makeSubst(c.vars)
198 out := make([]Directive, len(merged))
199 for i, d := range merged {
200 out[i].Key = d.Key
187 out[i].Args = make([]string, len(d.Args)) 201 out[i].Args = make([]string, len(d.Args))
188 for j, a := range d.Args { 202 for j, a := range d.Args {
189 out[i].Args[j] = c.subst(a, caps) 203 out[i].Args[j] = subst(a, caps)
190 } 204 }
191 } 205 }
192 return out 206 return out
193 } 207 }
194 208
195 func (c *Config) subst(s string, caps map[string]string) string { 209 // makeSubst returns a function that substitutes $VAR in s,
196 if !strings.Contains(s, "$") { 210 // checking caps first, then vars.
197 return s 211 func makeSubst(vars map[string]string) func(s string, caps map[string]string) string {
198 } 212 return func(s string, caps map[string]string) string {
199 var b strings.Builder 213 if !strings.Contains(s, "$") {
200 i := 0 214 return s
201 for i < len(s) { 215 }
202 if s[i] != '$' { 216 var b strings.Builder
203 b.WriteByte(s[i]) 217 i := 0
204 i++ 218 for i < len(s) {
205 continue 219 if s[i] != '$' {
206 } 220 b.WriteByte(s[i])
207 j := i + 1 221 i++
208 for j < len(s) && isVarChar(s[j]) { 222 continue
209 j++ 223 }
210 } 224 j := i + 1
211 name := s[i+1 : j] 225 for j < len(s) && isVarChar(s[j]) {
212 if v, ok := caps[name]; ok { 226 j++
213 b.WriteString(v) 227 }
214 } else if v, ok := c.vars[name]; ok { 228 name := s[i+1 : j]
215 b.WriteString(v) 229 if caps != nil {
216 } else { 230 if v, ok := caps[name]; ok {
217 b.WriteString(s[i:j]) 231 b.WriteString(v)
218 } 232 i = j
219 i = j 233 continue
220 } 234 }
221 return b.String() 235 }
236 if v, ok := vars[name]; ok {
237 b.WriteString(v)
238 } else {
239 b.WriteString(s[i:j])
240 }
241 i = j
242 }
243 return b.String()
244 }
245 }
246
247 func readLines(r io.Reader) ([]string, error) {
248 var out []string
249 scanner := bufio.NewScanner(r)
250 for scanner.Scan() {
251 line := stripComment(strings.TrimSpace(scanner.Text()))
252 if line != "" {
253 out = append(out, line)
254 }
255 }
256 return out, scanner.Err()
222 } 257 }
223 258
224 func matchExact(pat, s string, caps map[string]string) (int, bool) { 259 func matchExact(pat, s string, caps map[string]string) (int, bool) {
225 score, rem, ok := matchCaptures(pat, s, caps) 260 score, rem, ok := matchCaptures(pat, s, caps)
226 if !ok || rem != "" { 261 if !ok || rem != "" {
247 } 282 }
248 capName := pat[1:end] 283 capName := pat[1:end]
249 rest := pat[end+1:] 284 rest := pat[end+1:]
250 285
251 for split := 1; split <= len(inp); split++ { 286 for split := 1; split <= len(inp); split++ {
252 candidate := inp[:split] 287 score, finalRem, ok := matchCaptures(rest, inp[split:], caps)
253 remaining := inp[split:]
254 score, finalRem, ok := matchCaptures(rest, remaining, caps)
255 if ok { 288 if ok {
256 if capName != "_" { 289 if capName != "_" {
257 caps[capName] = candidate 290 caps[capName] = inp[:split]
258 } 291 }
259 return score, finalRem, true 292 return score, finalRem, true
260 } 293 }
261 } 294 }
262 return 0, "", false 295 return 0, "", false