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 }