diff options
| author | t <t@tjp.lol> | 2026-07-01 15:36:20 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-07-01 15:37:04 -0600 |
| commit | 800b2c4b6115845f6bf15d90484b3e80e81a606f (patch) | |
| tree | 914981ad5c49e8941280d015923b7c3143463d64 /src/glob.zig | |
| parent | ef7c37c3423ceb896f20676525307f15d6e927b4 (diff) | |
Load extensions via unified policy, entry/activate, rocks and paths
Replace the split [tools]/[extensions] config sections and the two-stage
load gate with a single model:
- [extensions] is now one policy resolved across all layers. Rules from
every layer are kept (not clobbered) and resolved by last-match-wins
after sorting by (layer, glob specificity, deny-last), so a base layer
can carve one name out of an otherwise-denied group and a higher layer's
broad rule still wins. Default is allow; whitelist via deny=["**"].
- Registration is deferred: a source returns an entry {name, activate}
(or a list, or the sugar tool table), is always eval'd side-effect-free,
and only permitted names get activate()d. Identity is the declared name,
not the filename. Collapses the old pre/post-load two-stage gate.
- The loader runs two passes (eval -> shadow -> filter -> activate) with
precedence project>user>base and, within a layer, rocks<paths<dir.
- extensions.paths adds extra scan dirs; extensions.rocks loads luarocks
packages as extension sources (require-as-entries). Startup installs
only missing rocks (no per-launch network hit); panto update force-
(re)installs every configured rock.
deny is a feature toggle, not a security boundary: rocks is where registry
code enters, so the pin list is the trust boundary. Rock integrity
(first-party GPG signing + --verify) is designed but not yet wired; until
then rocks pins which version, not which bytes.
Breaking: [tools] is removed; extensions must return entries. Built-in
tools and the wc example keep working via the sugar tool form.
Diffstat (limited to 'src/glob.zig')
| -rw-r--r-- | src/glob.zig | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/src/glob.zig b/src/glob.zig index 83d9fcb..7072e95 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -54,11 +54,85 @@ fn matchSegments(pat_it: *SegmentIter, name_it: *SegmentIter) bool { } // --------------------------------------------------------------------------- +// Specificity +// --------------------------------------------------------------------------- + +/// How specific a pattern is, used to rank rules that match the same name in +/// an allow/deny list. `order` gives an *ascending* order: less specific +/// sorts before more specific, so a "last matching rule wins" scan picks the +/// most specific match. Only meaningful when comparing patterns that both +/// match a given name; the metric is a consistent proxy (narrower match set). +pub const Specificity = struct { + /// Count of literal (non-wildcard) segments — the dominant signal. + literals: usize = 0, + /// Total segment count (a longer pattern constrains more positions). + segments: usize = 0, + /// Count of `**` segments (the loosest wildcard). + double_stars: usize = 0, + + /// Ascending order: `.lt` means `a` is *less* specific than `b`. Ranking: + /// 1. more literal segments (a.b.* > a.*, a.b.** > a.**) + /// 2. more total segments (a.*.* > a.*) + /// 3. fewer `**` segments (a.* > a.**) + pub fn order(a: Specificity, b: Specificity) std.math.Order { + if (a.literals != b.literals) return std.math.order(a.literals, b.literals); + if (a.segments != b.segments) return std.math.order(a.segments, b.segments); + // Fewer `**` → more specific (invert: `*` beats `**`). + return std.math.order(b.double_stars, a.double_stars); + } +}; + +/// Compute the specificity of a dot-delimited glob pattern. +pub fn specificity(pattern: []const u8) Specificity { + var s: Specificity = .{}; + var it = std.mem.splitScalar(u8, pattern, '.'); + while (it.next()) |seg| { + s.segments += 1; + if (std.mem.eql(u8, seg, "**")) { + s.double_stars += 1; + } else if (!std.mem.eql(u8, seg, "*")) { + s.literals += 1; + } + } + return s; +} + +// --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- const testing = std.testing; +test "specificity: more literals wins" { + // agent.rules (2 literals) is more specific than agent.* (1 literal). + try testing.expectEqual(std.math.Order.gt, specificity("agent.rules").order(specificity("agent.*"))); + try testing.expectEqual(std.math.Order.lt, specificity("agent.*").order(specificity("agent.rules"))); +} + +test "specificity: single star beats double star" { + // Same literal count and length; `*` is more specific than `**`. + try testing.expectEqual(std.math.Order.gt, specificity("agent.*").order(specificity("agent.**"))); +} + +test "specificity: a longer glob trumps a shorter one" { + // Same literal count; more segments constrains more → more specific. + try testing.expectEqual(std.math.Order.gt, specificity("a.*.*").order(specificity("a.*"))); + try testing.expectEqual(std.math.Order.gt, specificity("a.**.**").order(specificity("a.**"))); + // Literal count still dominates length. + try testing.expectEqual(std.math.Order.gt, specificity("a.b.*").order(specificity("a.*"))); + try testing.expectEqual(std.math.Order.gt, specificity("a.b.**").order(specificity("a.**"))); +} + +test "specificity: literal-heavier pattern beats a broad ** even if longer" { + // agent.rules.detail (3 literals) more specific than agent.** (1 literal). + try testing.expectEqual(std.math.Order.gt, specificity("agent.rules.detail").order(specificity("agent.**"))); +} + +test "specificity: bare ** is the least specific" { + try testing.expectEqual(std.math.Order.lt, specificity("**").order(specificity("agent.rules"))); + try testing.expectEqual(std.math.Order.lt, specificity("**").order(specificity("*"))); +} + test "literal match" { try testing.expect(match("std.read", "std.read")); try testing.expect(!match("std.read", "std.write")); |
