summaryrefslogtreecommitdiff
path: root/src/glob.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-01 08:21:00 -0600
committert <t@tjp.lol>2026-06-01 11:20:56 -0600
commit1beefefc69beee214430eb5bd2528a4f5692d2a8 (patch)
treea459dbde5da17babe7d872999341d2006ebfe02e /src/glob.zig
parent5c016cfdbcb122e2b4f0496ef946642d68953c4b (diff)
Rename system extension layer to base for clarity
Replace all references to the "system" layer with "base" to better reflect its role as the foundational extension/tool layer in panto's hierarchy. The layer hierarchy now consistently uses: project > user > base. Includes: - Update agent README and build.zig documentation - Refactor tool registry and config module to support layered lookups - Add TestHarness abstraction for cleaner test setup - Improve JSON serialization with wire-encoded tool names - Add glob pattern matching for tool/extension discovery
Diffstat (limited to 'src/glob.zig')
-rw-r--r--src/glob.zig121
1 files changed, 121 insertions, 0 deletions
diff --git a/src/glob.zig b/src/glob.zig
new file mode 100644
index 0000000..83d9fcb
--- /dev/null
+++ b/src/glob.zig
@@ -0,0 +1,121 @@
+//! 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;
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+const testing = std.testing;
+
+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"));
+}