Mercurial Hosting > d2o
annotate icf/icf.go @ 11:350589d762a0 default tip
add cgi, remove tls, update docs
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Thu, 19 Mar 2026 20:00:49 +0500 |
| parents | 2ffb8028ccbb |
| children |
| rev | line source |
|---|---|
| 0 | 1 // Package icf implements the Inherited Configuration Format parser. |
| 2 // | |
| 3 // ICF is a rule-based configuration format with variables, abstract blocks | |
| 4 // (mixins), pattern matching with named capture groups, and brace expansion. | |
| 5 package icf | |
| 6 | |
| 7 import ( | |
| 8 "bufio" | |
| 9 "fmt" | |
| 10 "io" | |
| 11 "strings" | |
| 12 ) | |
| 13 | |
| 14 type Directive struct { | |
| 15 Key string | |
| 16 Args []string | |
| 17 } | |
| 18 | |
| 19 type Config struct { | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
20 vars map[string]string |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
21 abstract map[string][]Directive |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
22 abstractMixin map[string]string // mixin name for each abstract block |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
23 Blocks []ParsedBlock |
| 0 | 24 } |
| 25 | |
| 26 type ParsedBlock struct { | |
| 27 ID string | |
| 28 Mixin string | |
| 29 Directives []Directive | |
| 30 } | |
| 31 | |
| 2 | 32 type rawBlock struct { |
| 33 id string | |
| 34 mixin string | |
| 35 directives []rawDirective | |
| 36 } | |
| 37 | |
| 38 type rawDirective struct { | |
| 39 key string | |
| 40 args []string // after brace expansion, before var substitution | |
| 41 } | |
| 42 | |
| 0 | 43 func Parse(r io.Reader) (*Config, error) { |
| 2 | 44 lines, err := readLines(r) |
| 45 if err != nil { | |
| 46 return nil, err | |
| 0 | 47 } |
| 48 | |
| 2 | 49 // --- Pass 1: collect variables --- |
| 50 vars := make(map[string]string) | |
| 4 | 51 subst := makeSubst(vars) |
| 52 | |
| 2 | 53 for _, line := range lines { |
| 54 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { | |
| 55 key := line[:i] | |
| 56 if isVarName(key) { | |
| 4 | 57 vars[key] = subst(strings.TrimSpace(line[i+1:]), nil) |
| 2 | 58 } |
| 59 } | |
| 60 } | |
| 61 | |
| 62 // --- Pass 2: parse blocks (raw, no capture substitution yet) --- | |
| 63 var raws []rawBlock | |
| 64 var cur *rawBlock | |
| 0 | 65 |
| 66 flush := func() { | |
| 67 if cur != nil { | |
| 2 | 68 raws = append(raws, *cur) |
| 0 | 69 cur = nil |
| 70 } | |
| 71 } | |
| 72 | |
| 2 | 73 for _, line := range lines { |
| 0 | 74 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { |
| 2 | 75 if isVarName(line[:i]) { |
| 0 | 76 continue |
| 77 } | |
| 78 } | |
| 79 | |
| 80 if strings.HasPrefix(line, "|>") { | |
| 81 if cur == nil { | |
| 82 return nil, fmt.Errorf("icf: directive outside block: %q", line) | |
| 83 } | |
| 84 parts := strings.Fields(strings.TrimSpace(line[2:])) | |
| 85 if len(parts) == 0 { | |
| 86 continue | |
| 87 } | |
| 2 | 88 cur.directives = append(cur.directives, rawDirective{ |
| 89 key: parts[0], | |
| 90 args: braceExpand(parts[1:]), | |
| 0 | 91 }) |
| 92 continue | |
| 93 } | |
| 94 | |
| 95 flush() | |
| 96 parts := strings.Fields(line) | |
| 2 | 97 rb := rawBlock{id: subst(parts[0], nil)} |
| 0 | 98 if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") { |
| 2 | 99 rb.mixin = subst(parts[1][1:], nil) |
| 0 | 100 } |
| 2 | 101 cur = &rb |
| 0 | 102 } |
| 103 flush() | |
| 104 | |
| 2 | 105 // --- Pass 3: separate abstract from concrete, apply var substitution --- |
| 106 c := &Config{ | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
107 vars: vars, |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
108 abstract: make(map[string][]Directive), |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
109 abstractMixin: make(map[string]string), |
| 0 | 110 } |
| 111 | |
| 2 | 112 for _, rb := range raws { |
| 113 dirs := make([]Directive, len(rb.directives)) | |
| 114 for i, rd := range rb.directives { | |
| 115 args := make([]string, len(rd.args)) | |
| 116 for j, a := range rd.args { | |
| 117 args[j] = subst(a, nil) | |
| 118 } | |
| 119 dirs[i] = Directive{Key: rd.key, Args: args} | |
| 120 } | |
| 121 | |
| 122 if strings.HasPrefix(rb.id, "@") { | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
123 name := rb.id[1:] |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
124 c.abstract[name] = dirs |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
125 if rb.mixin != "" { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
126 c.abstractMixin[name] = rb.mixin |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
127 } |
| 0 | 128 } else { |
| 2 | 129 c.Blocks = append(c.Blocks, ParsedBlock{ |
| 130 ID: rb.id, | |
| 131 Mixin: rb.mixin, | |
| 132 Directives: dirs, | |
| 133 }) | |
| 0 | 134 } |
| 135 } | |
| 136 | |
| 137 return c, nil | |
| 138 } | |
| 139 | |
| 2 | 140 // Abstract returns the directives of a named abstract block. |
| 141 // Returns nil if not found. | |
| 0 | 142 func (c *Config) Abstract(name string) []Directive { |
| 2 | 143 return c.abstract[name] |
| 0 | 144 } |
| 145 | |
| 146 // Match finds the most specific block matching input (e.g. "host/path") and | |
| 147 // returns resolved directives plus named captures. | |
| 2 | 148 // Domain is matched exactly; path uses prefix match. |
| 0 | 149 func (c *Config) Match(input string) ([]Directive, map[string]string) { |
| 150 inHost, inPath, _ := strings.Cut(input, "/") | |
| 151 | |
| 152 type hit struct { | |
| 153 block ParsedBlock | |
| 154 captures map[string]string | |
| 155 score int | |
| 156 } | |
| 157 var best *hit | |
| 158 | |
| 159 for _, b := range c.Blocks { | |
| 160 patHost, patPath, hasPath := strings.Cut(b.ID, "/") | |
| 161 caps := make(map[string]string) | |
| 162 | |
| 163 domScore, ok := matchExact(patHost, inHost, caps) | |
| 164 if !ok { | |
| 165 continue | |
| 166 } | |
| 167 | |
| 168 pathScore := 0 | |
| 169 if hasPath { | |
| 170 pathScore, ok = matchPrefix(patPath, inPath, caps) | |
| 171 if !ok { | |
| 172 continue | |
| 173 } | |
| 174 } | |
| 175 | |
| 176 score := domScore*1000 + pathScore | |
| 177 if best == nil || score > best.score { | |
| 178 best = &hit{block: b, captures: caps, score: score} | |
| 179 } | |
| 180 } | |
| 181 | |
| 182 if best == nil { | |
| 183 return nil, nil | |
| 184 } | |
| 185 | |
| 186 return c.ResolveBlock(best.block, best.captures), best.captures | |
| 187 } | |
| 188 | |
| 2 | 189 // ResolveBlock merges mixin directives (lower priority) with block directives, |
| 190 // then substitutes capture variables. | |
| 0 | 191 func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive { |
| 192 var merged []Directive | |
| 193 if b.Mixin != "" { | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
194 merged = append(merged, c.resolveAbstract(b.Mixin, make(map[string]bool))...) |
| 0 | 195 } |
| 196 merged = append(merged, b.Directives...) | |
| 2 | 197 |
| 198 if len(caps) == 0 { | |
| 199 return merged | |
| 200 } | |
| 0 | 201 |
| 2 | 202 // Substitute capture variables into a copy |
| 203 subst := makeSubst(c.vars) | |
| 204 out := make([]Directive, len(merged)) | |
| 205 for i, d := range merged { | |
| 206 out[i].Key = d.Key | |
| 0 | 207 out[i].Args = make([]string, len(d.Args)) |
| 208 for j, a := range d.Args { | |
| 2 | 209 out[i].Args[j] = subst(a, caps) |
| 0 | 210 } |
| 211 } | |
| 212 return out | |
| 213 } | |
| 214 | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
215 // resolveAbstract recursively expands an abstract block and its mixin chain. |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
216 // visited guards against circular references. |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
217 func (c *Config) resolveAbstract(name string, visited map[string]bool) []Directive { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
218 if visited[name] { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
219 return nil |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
220 } |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
221 visited[name] = true |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
222 |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
223 var out []Directive |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
224 if parent, ok := c.abstractMixin[name]; ok { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
225 out = append(out, c.resolveAbstract(parent, visited)...) |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
226 } |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
227 out = append(out, c.abstract[name]...) |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
228 return out |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
229 } |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
230 |
| 2 | 231 // makeSubst returns a function that substitutes $VAR in s, |
| 232 // checking caps first, then vars. | |
| 233 func makeSubst(vars map[string]string) func(s string, caps map[string]string) string { | |
| 234 return func(s string, caps map[string]string) string { | |
| 235 if !strings.Contains(s, "$") { | |
| 236 return s | |
| 0 | 237 } |
| 2 | 238 var b strings.Builder |
| 239 i := 0 | |
| 240 for i < len(s) { | |
| 241 if s[i] != '$' { | |
| 242 b.WriteByte(s[i]) | |
| 243 i++ | |
| 244 continue | |
| 245 } | |
| 246 j := i + 1 | |
| 247 for j < len(s) && isVarChar(s[j]) { | |
| 248 j++ | |
| 249 } | |
| 250 name := s[i+1 : j] | |
| 251 if caps != nil { | |
| 252 if v, ok := caps[name]; ok { | |
| 253 b.WriteString(v) | |
| 254 i = j | |
| 255 continue | |
| 256 } | |
| 257 } | |
| 258 if v, ok := vars[name]; ok { | |
| 259 b.WriteString(v) | |
| 260 } else { | |
| 261 b.WriteString(s[i:j]) | |
| 262 } | |
| 263 i = j | |
| 0 | 264 } |
| 2 | 265 return b.String() |
| 266 } | |
| 267 } | |
| 268 | |
| 269 func readLines(r io.Reader) ([]string, error) { | |
| 270 var out []string | |
| 271 scanner := bufio.NewScanner(r) | |
| 272 for scanner.Scan() { | |
| 273 line := stripComment(strings.TrimSpace(scanner.Text())) | |
| 274 if line != "" { | |
| 275 out = append(out, line) | |
| 0 | 276 } |
| 277 } | |
| 2 | 278 return out, scanner.Err() |
| 0 | 279 } |
| 280 | |
| 281 func matchExact(pat, s string, caps map[string]string) (int, bool) { | |
| 282 score, rem, ok := matchCaptures(pat, s, caps) | |
| 283 if !ok || rem != "" { | |
| 284 return 0, false | |
| 285 } | |
| 286 return score, true | |
| 287 } | |
| 288 | |
| 289 func matchPrefix(pat, s string, caps map[string]string) (int, bool) { | |
| 290 score, _, ok := matchCaptures(pat, s, caps) | |
| 291 return score, ok | |
| 292 } | |
| 293 | |
| 294 func matchCaptures(pat, inp string, caps map[string]string) (int, string, bool) { | |
| 295 for { | |
| 296 if pat == "" { | |
| 297 return 0, inp, true | |
| 298 } | |
| 299 | |
| 300 if strings.HasPrefix(pat, "<") { | |
| 301 end := strings.Index(pat, ">") | |
| 302 if end == -1 { | |
| 303 return 0, "", false | |
| 304 } | |
| 305 capName := pat[1:end] | |
| 306 rest := pat[end+1:] | |
| 307 | |
|
8
2ffb8028ccbb
add loggingmodes and fix path matching
Atarwn Gard <a@qwa.su>
parents:
6
diff
changeset
|
308 for split := len(inp); split >= 1; split-- { |
| 2 | 309 score, finalRem, ok := matchCaptures(rest, inp[split:], caps) |
| 0 | 310 if ok { |
| 311 if capName != "_" { | |
| 2 | 312 caps[capName] = inp[:split] |
| 0 | 313 } |
| 314 return score, finalRem, true | |
| 315 } | |
| 316 } | |
| 317 return 0, "", false | |
| 318 } | |
| 319 | |
| 320 if inp == "" || pat[0] != inp[0] { | |
| 321 return 0, "", false | |
| 322 } | |
| 323 score, rem, ok := matchCaptures(pat[1:], inp[1:], caps) | |
| 324 return score + 1, rem, ok | |
| 325 } | |
| 326 } | |
| 327 | |
| 328 func braceExpand(args []string) []string { | |
| 329 var out []string | |
| 330 for _, a := range args { | |
| 331 out = append(out, expandOne(a)...) | |
| 332 } | |
| 333 return out | |
| 334 } | |
| 335 | |
| 336 func expandOne(s string) []string { | |
| 337 start := strings.Index(s, "{") | |
| 338 end := strings.Index(s, "}") | |
| 339 if start == -1 || end == -1 || end < start { | |
| 340 return []string{s} | |
| 341 } | |
| 342 prefix := s[:start] | |
| 343 suffix := s[end+1:] | |
| 344 var out []string | |
| 345 for _, v := range strings.Split(s[start+1:end], ",") { | |
| 346 out = append(out, prefix+strings.TrimSpace(v)+suffix) | |
| 347 } | |
| 348 return out | |
| 349 } | |
| 350 | |
| 351 func stripComment(line string) string { | |
| 352 if strings.HasPrefix(line, ";") { | |
| 353 return "" | |
| 354 } | |
| 355 if i := strings.Index(line, " ;"); i != -1 { | |
| 356 return strings.TrimSpace(line[:i]) | |
| 357 } | |
| 358 return line | |
| 359 } | |
| 360 | |
| 361 func isVarName(s string) bool { | |
| 362 if s == "" { | |
| 363 return false | |
| 364 } | |
| 365 for i := 0; i < len(s); i++ { | |
| 366 if !isVarChar(s[i]) { | |
| 367 return false | |
| 368 } | |
| 369 } | |
| 370 return true | |
| 371 } | |
| 372 | |
| 373 func isVarChar(c byte) bool { | |
| 374 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || | |
| 375 (c >= '0' && c <= '9') || c == '_' | |
| 2 | 376 } |
