1use std::num::NonZeroUsize;
2use std::sync::{LazyLock, Mutex};
3
4use lru::LruCache;
5use regex::bytes::Regex;
6
7use crate::core::{RelayBuf, RelayStr};
8
9static CODEOWNERS_CACHE: LazyLock<Mutex<LruCache<String, Regex>>> =
11 LazyLock::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(500).unwrap())));
12
13fn translate_codeowners_pattern(pattern: &str) -> Option<Regex> {
14 let mut regex = String::new();
15
16 if pattern.starts_with('\\') {
18 return Regex::new(r"\\(?:\z|/)").ok();
19 }
20
21 let anchored = pattern
22 .find('/')
23 .is_some_and(|pos| pos != pattern.len() - 1);
24
25 if anchored {
26 regex += r"\A";
27 } else {
28 regex += r"(?:\A|/)";
29 }
30
31 let matches_dir = pattern.ends_with('/');
32 let mut pattern = pattern;
33 if matches_dir {
34 pattern = pattern.trim_end_matches('/');
35 }
36
37 let trailing_slash_star = pattern.len() > 1 && pattern.ends_with("/*");
40
41 let pattern_vec: Vec<char> = pattern.chars().collect();
42
43 let mut num_to_skip = None;
44 for (i, &ch) in pattern_vec.iter().enumerate() {
45 if i == 0 && anchored && pattern.starts_with('/') {
47 regex += r"/?";
48 continue;
49 }
50
51 if let Some(skip_amount) = num_to_skip {
52 num_to_skip = Some(skip_amount - 1);
53 if num_to_skip > Some(0) {
55 continue;
56 }
57 }
58
59 if ch == '*' {
60 if pattern_vec.get(i + 1) == Some(&'*') {
62 let left_anchored = i == 0;
63 let leading_slash = i > 0 && pattern_vec.get(i - 1) == Some(&'/');
64 let right_anchored = i + 2 == pattern.len();
65 let trailing_slash = pattern_vec.get(i + 2) == Some(&'/');
66
67 if (left_anchored || leading_slash) && (right_anchored || trailing_slash) {
68 if trailing_slash {
69 regex += "(?:.*/)?";
71 num_to_skip = Some(3);
72 } else {
73 regex += ".*";
75 num_to_skip = Some(2);
76 }
77 continue;
78 }
79 }
80 regex += r"[^/]*";
81 } else if ch == '?' {
82 regex += r"[^/]";
83 } else {
84 regex += ®ex::escape(ch.to_string().as_str());
85 }
86 }
87
88 if matches_dir {
89 regex += "/";
90 } else if trailing_slash_star {
91 regex += r"\z";
92 } else {
93 regex += r"(?:\z|/)";
94 }
95 Regex::new(®ex).ok()
96}
97
98#[unsafe(no_mangle)]
100#[relay_ffi::catch_unwind]
101pub unsafe extern "C" fn relay_is_codeowners_path_match(
102 value: *const RelayBuf,
103 pattern: *const RelayStr,
104) -> bool {
105 let value = unsafe { (*value).as_bytes() };
106 let pat = unsafe { (*pattern).as_str() };
107
108 let mut cache = CODEOWNERS_CACHE.lock().unwrap();
109
110 if let Some(pattern) = cache.get(pat) {
111 pattern.is_match(value)
112 } else if let Some(pattern) = translate_codeowners_pattern(pat) {
113 let result = pattern.is_match(value);
114 cache.put(pat.to_owned(), pattern);
115 result
116 } else {
117 false
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_translate_codeowners_pattern() {
127 let pattern = "*.txt";
129 let regex = translate_codeowners_pattern(pattern).unwrap();
130 assert!(regex.is_match(b"file.txt"));
131 assert!(regex.is_match(b"file.txt/"));
132 assert!(regex.is_match(b"dir/file.txt"));
133
134 let pattern = "/dir/*.txt";
136 let regex = translate_codeowners_pattern(pattern).unwrap();
137 assert!(regex.is_match(b"/dir/file.txt"));
138 assert!(regex.is_match(b"dir/file.txt"));
139 assert!(!regex.is_match(b"/dir/subdir/file.txt"));
140
141 let pattern = "apps/";
143 let regex = translate_codeowners_pattern(pattern).unwrap();
144 assert!(regex.is_match(b"apps/file.txt"));
145 assert!(regex.is_match(b"/apps/file.txt"));
146 assert!(regex.is_match(b"/dir/apps/file.txt"));
147 assert!(regex.is_match(b"/dir/subdir/apps/file.txt"));
148
149 let pattern = "docs/*";
151 let regex = translate_codeowners_pattern(pattern).unwrap();
152 assert!(regex.is_match(b"docs/getting-started.md"));
153 assert!(!regex.is_match(b"docs/build-app/troubleshooting.md"));
154 assert!(!regex.is_match(b"something/docs/build-app/troubleshooting.md"));
155
156 let pattern = "*/docs/";
158 let regex = translate_codeowners_pattern(pattern).unwrap();
159 assert!(regex.is_match(b"first/docs/troubleshooting.md"));
160 assert!(!regex.is_match(b"docs/getting-started.md"));
161 assert!(!regex.is_match(b"first/second/docs/troubleshooting.md"));
162
163 let pattern = "docs/*/something/";
165 let regex = translate_codeowners_pattern(pattern).unwrap();
166 assert!(regex.is_match(b"docs/first/something/troubleshooting.md"));
167 assert!(!regex.is_match(b"something/docs/first/something/troubleshooting.md"));
168 assert!(!regex.is_match(b"docs/first/second/something/getting-started.md"));
169
170 let pattern = "/docs/";
172 let regex = translate_codeowners_pattern(pattern).unwrap();
173 assert!(regex.is_match(b"/docs/file.txt"));
174 assert!(regex.is_match(b"/docs/subdir/file.txt"));
175 assert!(regex.is_match(b"docs/subdir/file.txt"));
176 assert!(!regex.is_match(b"app/docs/file.txt"));
177
178 let pattern = "/red/**/file.py";
180 let regex = translate_codeowners_pattern(pattern).unwrap();
181 assert!(regex.is_match(b"red/orange/yellow/green/file.py"));
182 assert!(regex.is_match(b"/red/orange/file.py"));
183 assert!(regex.is_match(b"red/file.py"));
184 assert!(!regex.is_match(b"yellow/file.py"));
185
186 let pattern = "red/**/file.py";
188 let regex = translate_codeowners_pattern(pattern).unwrap();
189 assert!(regex.is_match(b"red/orange/yellow/green/file.py"));
190 assert!(regex.is_match(b"red/file.py"));
191 assert!(!regex.is_match(b"something/red/file.py"));
192
193 let pattern = "**/yellow/file.py";
195 let regex = translate_codeowners_pattern(pattern).unwrap();
196 assert!(regex.is_match(b"red/orange/yellow/file.py"));
197 assert!(regex.is_match(b"yellow/file.py"));
198 assert!(!regex.is_match(b"/docs/file.py"));
199
200 let pattern = "/red/orange/**";
202 let regex = translate_codeowners_pattern(pattern).unwrap();
203 assert!(regex.is_match(b"red/orange/yellow/file.py"));
204 assert!(regex.is_match(b"red/orange/file.py"));
205 assert!(!regex.is_match(b"blue/red/orange/file.py"));
206
207 let pattern = "/**/*.css";
209 let regex = translate_codeowners_pattern(pattern).unwrap();
210 assert!(regex.is_match(b"/docs/subdir/file.css"));
211 assert!(regex.is_match(b"file.css"));
212 assert!(!regex.is_match(b"/docs/file.txt"));
213
214 let pattern = "/**/foo.py";
216 let regex = translate_codeowners_pattern(pattern).unwrap();
217 assert!(regex.is_match(b"foo.py"));
218 assert!(regex.is_match(b"dir/foo.py"));
219 assert!(regex.is_match(b"dir/subdir/foo.py"));
220 assert!(!regex.is_match(b"not_foo.py"));
221 assert!(!regex.is_match(b"dir/not_foo.py"));
222 }
223}