lib/coz/src/filter.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub fn pathHasPrefix(path: []const u8, prefix: []const u8) bool {
4 if (prefix.len == 0) return false;
5 if (prefix.len > path.len) return false;
6 if (!std.mem.eql(u8, path[0..prefix.len], prefix)) return false;
7 return path.len == prefix.len or path[prefix.len] == '/';
8 }
9
10 pub fn isSystemPath(normalized: []const u8) bool {
11 const prefixes = [_][]const u8{
12 "/usr/include",
13 "/usr/lib",
14 "/usr/local/include",
15 "/usr/local/lib",
16 "/lib",
17 "/lib64",
18 };
19
20 for (prefixes) |prefix| {
21 if (pathHasPrefix(normalized, prefix)) return true;
22 }
23 return false;
24 }
25
26 pub fn isCozHeader(path: []const u8) bool {
27 return std.mem.endsWith(u8, path, "/coz.h");
28 }
29
30 pub fn wildcardMatch(subject: []const u8, pattern: []const u8) bool {
31 return wildcardMatchRange(subject, pattern);
32 }
33
34 pub fn inScopeNormalized(normalized: []const u8, scope: []const []const u8) bool {
35 for (scope) |pattern| {
36 if (wildcardMatch(normalized, pattern)) return true;
37 }
38 return false;
39 }
40
41 pub fn fileMatchesScopeNormalized(
42 normalized: []const u8,
43 scope: []const []const u8,
44 allow_system_sources: bool,
45 ) bool {
46 if (normalized.len == 0) return false;
47 if (isCozHeader(normalized)) return false;
48 if (!allow_system_sources and isSystemPath(normalized)) return false;
49 if (scope.len == 0) return true;
50 return inScopeNormalized(normalized, scope);
51 }
52
53 fn wildcardMatchRange(subject: []const u8, pattern: []const u8) bool {
54 if ((pattern.len == 0) != (subject.len == 0)) return false;
55 if (pattern.len == 0 and subject.len == 0) return true;
56
57 if (pattern[0] == '%') {
58 var match_end = subject.len;
59 while (true) {
60 if (wildcardMatchRange(subject[match_end..], pattern[1..])) return true;
61 if (match_end == 0) break;
62 match_end -= 1;
63 }
64 return false;
65 }
66
67 var index: usize = 0;
68 while (index < subject.len and index < pattern.len and pattern[index] != '%') : (index += 1) {
69 if (pattern[index] != subject[index]) return false;
70 }
71
72 return wildcardMatchRange(subject[index..], pattern[index..]);
73 }
74
75 test "system include and library paths are detected" {
76 try std.testing.expect(isSystemPath("/usr/include/stdio.h"));
77 try std.testing.expect(isSystemPath("/usr/include/c++/11/string"));
78 try std.testing.expect(isSystemPath("/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h"));
79 try std.testing.expect(isSystemPath("/usr/local/include/boost/asio.hpp"));
80 try std.testing.expect(isSystemPath("/usr/local/lib/libfoo.so"));
81 try std.testing.expect(isSystemPath("/lib/x86_64-linux-gnu/libc.so.6"));
82 try std.testing.expect(isSystemPath("/lib64/ld-linux-x86-64.so.2"));
83 }
84
85 test "user paths are not system paths" {
86 try std.testing.expect(!isSystemPath("/home/user/project/src/main.cpp"));
87 try std.testing.expect(!isSystemPath("/opt/app/lib/mylib.so"));
88 try std.testing.expect(!isSystemPath("/tmp/build/output.o"));
89 }
90
91 test "coz header paths are detected" {
92 try std.testing.expect(isCozHeader("/usr/include/coz.h"));
93 try std.testing.expect(isCozHeader("/home/user/git/coz/include/coz.h"));
94 try std.testing.expect(isCozHeader("/coz.h"));
95 }
96
97 test "non-coz header paths are not detected" {
98 try std.testing.expect(!isCozHeader("/usr/include/coz_utils.h"));
99 try std.testing.expect(!isCozHeader("/home/user/coz.cpp"));
100 try std.testing.expect(!isCozHeader("/home/user/mycoz.h"));
101 }
102
103 test "path prefix matching requires a component boundary" {
104 try std.testing.expect(pathHasPrefix("/usr/include/stdio.h", "/usr/include"));
105 try std.testing.expect(pathHasPrefix("/usr/include", "/usr/include"));
106 try std.testing.expect(!pathHasPrefix("/usr/includes/foo.h", "/usr/include"));
107 try std.testing.expect(!pathHasPrefix("/usr", "/usr/include"));
108 try std.testing.expect(!pathHasPrefix("", "/usr"));
109 try std.testing.expect(!pathHasPrefix("/usr/include", ""));
110 }
111
112 test "wildcard matching follows upstream percent semantics" {
113 try std.testing.expect(wildcardMatch("/usr/include/stdio.h", "/usr/%/stdio.h"));
114 try std.testing.expect(wildcardMatch("/home/user/project/src/main.rs", "%"));
115 try std.testing.expect(wildcardMatch("/opt/app/file.zig", "/opt/%/file.zig"));
116 try std.testing.expect(!wildcardMatch("", "%"));
117 try std.testing.expect(!wildcardMatch("/opt", "/opt/%"));
118 try std.testing.expect(!wildcardMatch("/usr/includes/foo.h", "/usr/include/%"));
119 }
120
121 test "scope wildcard admits non-system user source paths" {
122 const scope = [_][]const u8{"%"};
123
124 try std.testing.expect(fileMatchesScopeNormalized("/home/user/project/src/main.rs", &scope, true));
125 try std.testing.expect(fileMatchesScopeNormalized("/home/user/project/src/main.cpp", &scope, true));
126 }
127
128 test "coz headers are filtered in every scope" {
129 const default_scope = [_][]const u8{"%"};
130 const explicit_scope = [_][]const u8{"/usr/include/%"};
131
132 try std.testing.expect(!fileMatchesScopeNormalized("/usr/include/coz.h", &default_scope, true));
133 try std.testing.expect(!fileMatchesScopeNormalized("/usr/include/coz.h", &explicit_scope, true));
134 }
135
136 test "empty scope allows nonempty non-header paths" {
137 const empty_scope = [_][]const u8{};
138
139 try std.testing.expect(fileMatchesScopeNormalized("/opt/app/file.zig", &empty_scope, true));
140 try std.testing.expect(fileMatchesScopeNormalized("/home/user/project/src/main.rs", &empty_scope, true));
141 }
142
143 test "system source filtering honors the allow flag" {
144 const scope = [_][]const u8{"%"};
145
146 try std.testing.expect(!fileMatchesScopeNormalized("/usr/include/stdio.h", &scope, false));
147 try std.testing.expect(fileMatchesScopeNormalized("/usr/include/stdio.h", &scope, true));
148 }