1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
//! Minimal glob matcher for tool/extension allow- and deny-lists.
//!
//! Patterns and names are dot-delimited segments (e.g. `std.git.status`).
//! Matching operates on whole segments, never on individual characters.
//!
//! Supported segment wildcards:
//! - `*` matches exactly one segment. `std.*` matches `std.read` but
//! not `std.git.status` (greedy within a segment, but not
//! dot-agnostic).
//! - `**` matches one or more segments. `std.**` matches both
//! `std.read` and `std.git.status`.
//!
//! A wildcard is only recognized as a whole segment: `std.*` and `std.**`
//! are wildcards, but `std.rea*` is a literal segment (the `*` has no
//! special meaning mid-segment). Everything else is matched literally.
const std = @import("std");
/// Returns true if `name` matches `pattern` under the segment rules above.
pub fn match(pattern: []const u8, name: []const u8) bool {
var pat_it = std.mem.splitScalar(u8, pattern, '.');
var name_it = std.mem.splitScalar(u8, name, '.');
return matchSegments(&pat_it, &name_it);
}
const SegmentIter = std.mem.SplitIterator(u8, .scalar);
fn matchSegments(pat_it: *SegmentIter, name_it: *SegmentIter) bool {
while (pat_it.next()) |seg| {
if (std.mem.eql(u8, seg, "**")) {
// `**` matches one or more remaining segments. Try the
// shortest match first (one segment), then extend.
const rest_pat = pat_it.*; // pattern after `**`
while (name_it.next()) |_| {
var pat_copy = rest_pat;
var name_copy = name_it.*;
if (matchSegments(&pat_copy, &name_copy)) return true;
}
// `**` requires at least one segment, and the trailing
// pattern must have matched the remainder above.
return false;
}
const name_seg = name_it.next() orelse return false;
if (std.mem.eql(u8, seg, "*")) {
// Matches exactly one (any) segment.
continue;
}
if (!std.mem.eql(u8, seg, name_seg)) return false;
}
// Pattern exhausted: match iff name is also exhausted.
return name_it.next() == null;
}
// ---------------------------------------------------------------------------
// 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"));
try testing.expect(!match("std.read", "std.read.more"));
try testing.expect(!match("std.read.more", "std.read"));
}
test "single star matches exactly one segment" {
try testing.expect(match("std.*", "std.read"));
try testing.expect(match("std.*", "std.write"));
try testing.expect(!match("std.*", "std.git.status"));
try testing.expect(!match("std.*", "std"));
try testing.expect(!match("std.*", "other.read"));
}
test "double star is dot-agnostic, one or more segments" {
try testing.expect(match("std.**", "std.read"));
try testing.expect(match("std.**", "std.git.status"));
try testing.expect(!match("std.**", "std"));
try testing.expect(!match("std.**", "other.read"));
}
test "bare single star matches one segment" {
try testing.expect(match("*", "std"));
try testing.expect(!match("*", "std.read"));
// An empty name is a single empty segment, which `*` matches.
try testing.expect(match("*", ""));
}
test "bare double star matches everything non-empty" {
try testing.expect(match("**", "std"));
try testing.expect(match("**", "std.read"));
try testing.expect(match("**", "std.git.status"));
}
test "star in middle" {
try testing.expect(match("*.read", "std.read"));
try testing.expect(match("*.*", "std.read"));
try testing.expect(!match("*.*", "std.git.status"));
try testing.expect(match("std.*.status", "std.git.status"));
try testing.expect(!match("std.*.status", "std.status"));
}
test "double star in middle" {
try testing.expect(match("std.**.status", "std.git.status"));
try testing.expect(match("std.**.status", "std.a.b.status"));
try testing.expect(!match("std.**.status", "std.status"));
try testing.expect(match("**.status", "std.git.status"));
}
test "wildcard only recognized as whole segment" {
// A `*` embedded in a segment is a literal, not a wildcard.
try testing.expect(match("std.rea*", "std.rea*"));
try testing.expect(!match("std.rea*", "std.read"));
}
test "empty pattern only matches empty name" {
try testing.expect(match("", ""));
try testing.expect(!match("", "x"));
}
|