Mercurial Hosting > d2o
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 |
