summaryrefslogtreecommitdiff
path: root/src/glob.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/glob.zig')
-rw-r--r--src/glob.zig74
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"));