diff icf/icf.go @ 0:48bdab3eec8a

Initial
author Atarwn Gard <a@qwa.su>
date Mon, 09 Mar 2026 00:37:49 +0500
parents
children d19133be91ba
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/icf/icf.go	Mon Mar 09 00:37:49 2026 +0500
@@ -0,0 +1,321 @@
+// 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.
+//
+// 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 (
+	"bufio"
+	"fmt"
+	"io"
+	"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
+	Blocks   []ParsedBlock
+}
+
+type ParsedBlock struct {
+	ID         string
+	Mixin      string
+	Directives []Directive
+}
+
+// Parse reads an ICF document and returns a ready Config.
+func Parse(r io.Reader) (*Config, error) {
+	c := &Config{
+		vars:     make(map[string]string),
+		abstract: make(map[string][]Directive),
+	}
+
+	var raw []ParsedBlock
+	var cur *ParsedBlock
+
+	flush := func() {
+		if cur != nil {
+			raw = append(raw, *cur)
+			cur = nil
+		}
+	}
+
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		line := stripComment(strings.TrimSpace(scanner.Text()))
+		if line == "" {
+			continue
+		}
+
+		// Variable assignment: KEY=value
+		if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") {
+			key := line[:i]
+			if isVarName(key) {
+				c.vars[key] = strings.TrimSpace(line[i+1:])
+				continue
+			}
+		}
+
+		// Directive: |> key args...
+		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, Directive{
+				Key:  parts[0],
+				Args: braceExpand(parts[1:]),
+			})
+			continue
+		}
+
+		// Block header: id [@mixin]
+		flush()
+		parts := strings.Fields(line)
+		pb := ParsedBlock{ID: parts[0]}
+		if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") {
+			pb.Mixin = parts[1][1:]
+		}
+		cur = &pb
+	}
+	flush()
+
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	// Separate abstract from concrete blocks
+	for _, b := range raw {
+		if strings.HasPrefix(b.ID, "@") {
+			c.abstract[b.ID[1:]] = b.Directives
+		} else {
+			c.Blocks = append(c.Blocks, b)
+		}
+	}
+
+	return c, nil
+}
+
+// Abstract returns the directives of a named abstract block with variables
+// substituted. 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)
+}
+
+// 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.
+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 then block directives, substituting vars+captures.
+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)
+}
+
+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)
+		out[i].Args = make([]string, len(d.Args))
+		for j, a := range d.Args {
+			out[i].Args[j] = c.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
+		}
+		j := i + 1
+		for j < len(s) && isVarChar(s[j]) {
+			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])
+		}
+		i = j
+	}
+	return b.String()
+}
+
+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 := 1; split <= len(inp); split++ {
+				candidate := inp[:split]
+				remaining := inp[split:]
+				score, finalRem, ok := matchCaptures(rest, remaining, caps)
+				if ok {
+					if capName != "_" {
+						caps[capName] = candidate
+					}
+					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 == '_'
+}