Mercurial Hosting > d2o
changeset 2:d19133be91ba
ndex and smarter parser
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 09 Mar 2026 01:55:11 +0500 |
| parents | 3e7247db5c6e |
| children | eb705d4cdcd7 |
| files | icf/icf.go main.go |
| diffstat | 2 files changed, 152 insertions(+), 116 deletions(-) [+] |
line wrap: on
line diff
diff -r 3e7247db5c6e -r d19133be91ba icf/icf.go --- a/icf/icf.go Mon Mar 09 01:04:16 2026 +0500 +++ b/icf/icf.go Mon Mar 09 01:55:11 2026 +0500 @@ -2,16 +2,6 @@ // // 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 ( @@ -21,13 +11,11 @@ "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 @@ -40,40 +28,54 @@ Directives []Directive } -// Parse reads an ICF document and returns a ready Config. +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) { - c := &Config{ - vars: make(map[string]string), - abstract: make(map[string][]Directive), + lines, err := readLines(r) + if err != nil { + return nil, err } - var raw []ParsedBlock - var cur *ParsedBlock + // --- Pass 1: collect variables --- + vars := make(map[string]string) + for _, line := range lines { + if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { + key := line[:i] + if isVarName(key) { + vars[key] = strings.TrimSpace(line[i+1:]) + } + } + } + + subst := makeSubst(vars) + + // --- Pass 2: parse blocks (raw, no capture substitution yet) --- + var raws []rawBlock + var cur *rawBlock flush := func() { if cur != nil { - raw = append(raw, *cur) + raws = append(raws, *cur) cur = nil } } - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := stripComment(strings.TrimSpace(scanner.Text())) - if line == "" { - continue - } - - // Variable assignment: KEY=value + for _, line := range lines { if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { - key := line[:i] - if isVarName(key) { - c.vars[key] = strings.TrimSpace(line[i+1:]) + if isVarName(line[:i]) { continue } } - // Directive: |> key args... if strings.HasPrefix(line, "|>") { if cur == nil { return nil, fmt.Errorf("icf: directive outside block: %q", line) @@ -82,53 +84,62 @@ if len(parts) == 0 { continue } - cur.Directives = append(cur.Directives, Directive{ - Key: parts[0], - Args: braceExpand(parts[1:]), + cur.directives = append(cur.directives, rawDirective{ + key: parts[0], + args: braceExpand(parts[1:]), }) continue } - // Block header: id [@mixin] flush() parts := strings.Fields(line) - pb := ParsedBlock{ID: parts[0]} + rb := rawBlock{id: subst(parts[0], nil)} if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") { - pb.Mixin = parts[1][1:] + rb.mixin = subst(parts[1][1:], nil) } - cur = &pb + cur = &rb } flush() - if err := scanner.Err(); err != nil { - return nil, err + // --- Pass 3: separate abstract from concrete, apply var substitution --- + c := &Config{ + vars: vars, + abstract: make(map[string][]Directive), } - // Separate abstract from concrete blocks - for _, b := range raw { - if strings.HasPrefix(b.ID, "@") { - c.abstract[b.ID[1:]] = b.Directives + 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, "@") { + c.abstract[rb.id[1:]] = dirs } else { - c.Blocks = append(c.Blocks, b) + 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 with variables -// substituted. Returns nil if not found. +// Abstract returns the directives of a named abstract block. +// 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) + return c.abstract[name] } // 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. +// Domain is matched exactly; path uses prefix match. func (c *Config) Match(input string) ([]Directive, map[string]string) { inHost, inPath, _ := strings.Cut(input, "/") @@ -141,7 +152,6 @@ for _, b := range c.Blocks { patHost, patPath, hasPath := strings.Cut(b.ID, "/") - caps := make(map[string]string) domScore, ok := matchExact(patHost, inHost, caps) @@ -170,55 +180,80 @@ return c.ResolveBlock(best.block, best.captures), best.captures } -// ResolveBlock merges mixin then block directives, substituting vars+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.abstract[b.Mixin]...) } merged = append(merged, b.Directives...) - return c.applyVars(merged, caps) -} + + if len(caps) == 0 { + return merged + } -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) + // 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] = c.subst(a, caps) + out[i].Args[j] = 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 +// 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 } - j := i + 1 - for j < len(s) && isVarChar(s[j]) { - j++ + 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 } - 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]) + 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) } - i = j } - return b.String() + return out, scanner.Err() } func matchExact(pat, s string, caps map[string]string) (int, bool) { @@ -249,12 +284,10 @@ rest := pat[end+1:] for split := 1; split <= len(inp); split++ { - candidate := inp[:split] - remaining := inp[split:] - score, finalRem, ok := matchCaptures(rest, remaining, caps) + score, finalRem, ok := matchCaptures(rest, inp[split:], caps) if ok { if capName != "_" { - caps[capName] = candidate + caps[capName] = inp[:split] } return score, finalRem, true } @@ -318,4 +351,4 @@ func isVarChar(c byte) bool { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' -} +} \ No newline at end of file
diff -r 3e7247db5c6e -r d19133be91ba main.go --- a/main.go Mon Mar 09 01:04:16 2026 +0500 +++ b/main.go Mon Mar 09 01:55:11 2026 +0500 @@ -160,29 +160,23 @@ func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { var ( - rootDir string - rootIndex []string - fcgiAddr string - fcgiPat string - rprxAddr string + rootDir string + rootShow bool + ndex []string + fcgiAddr string + fcgiPat string + rprxAddr string + rdirCode int + rdirURL string ) for _, d := range dirs { switch d.Key { case "root": rootDir = safeArg(d.Args, 0) - switch safeArg(d.Args, 1) { - case "show": - if len(d.Args) >= 3 { - rootIndex = d.Args[2:] - } else { - rootIndex = []string{"index.html"} - } - case "hide", "": - rootIndex = nil - default: - log.Printf("d2o: root: unknown mode %q (want show|hide)", safeArg(d.Args, 1)) - } + rootShow = safeArg(d.Args, 1) == "show" + case "ndex": + ndex = d.Args case "fcgi": fcgiAddr = safeArg(d.Args, 0) fcgiPat = safeArg(d.Args, 1) @@ -191,9 +185,19 @@ } case "rprx": rprxAddr = safeArg(d.Args, 0) + case "rdir": + rdirCode, _ = strconv.Atoi(safeArg(d.Args, 0)) + rdirURL = safeArg(d.Args, 1) } } + if rdirURL != "" { + if rdirCode == 0 { + rdirCode = http.StatusFound + } + http.Redirect(w, r, rdirURL, rdirCode) + return + } if rprxAddr != "" { serveReverseProxy(w, r, rprxAddr) return @@ -206,7 +210,7 @@ return } if rootDir != "" { - serveStatic(w, r, rootDir, rootIndex) + serveStatic(w, r, rootDir, rootShow, ndex) return } @@ -214,8 +218,7 @@ } // --- Static ----------------------------------------------------------------- - -func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, rootIndex []string) { +func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string) { fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) info, err := os.Stat(fpath) @@ -229,17 +232,17 @@ } if info.IsDir() { - if rootIndex == nil { - http.Error(w, "forbidden", http.StatusForbidden) - return - } - for _, idx := range rootIndex { + for _, idx := range ndex { idxPath := filepath.Join(fpath, idx) if _, err := os.Stat(idxPath); err == nil { http.ServeFile(w, r, idxPath) return } } + if !show { + http.Error(w, "forbidden", http.StatusForbidden) + return + } listDir(w, r, fpath, r.URL.Path) return } @@ -362,4 +365,4 @@ regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$" matched, err := regexp.MatchString(regPat, s) return err == nil && matched -} +} \ No newline at end of file
