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 regex += ".*";
69 num_to_skip = Some(2);
70 if trailing_slash {
72 regex += "/?";
73 num_to_skip = Some(3);
74 }
75 continue;
76 }
77 }
78 regex += r"[^/]*";
79 } else if ch == '?' {
80 regex += r"[^/]";
81 } else {
82 regex += ®ex::escape(ch.to_string().as_str());
83 }
84 }
85
86 if matches_dir {
87 regex += "/";
88 } else if trailing_slash_star {
89 regex += r"\z";
90 } else {
91 regex += r"(?:\z|/)";
92 }
93 Regex::new(®ex).ok()
94}
95
96#[unsafe(no_mangle)]
98#[relay_ffi::catch_unwind]
99pub unsafe extern "C" fn relay_is_codeowners_path_match(
100 value: *const RelayBuf,
101 pattern: *const RelayStr,
102) -> bool {
103 let value = unsafe { (*value).as_bytes() };
104 let pat = unsafe { (*pattern).as_str() };
105
106 let mut cache = CODEOWNERS_CACHE.lock().unwrap();
107
108 if let Some(pattern) = cache.get(pat) {
109 pattern.is_match(value)
110 } else if let Some(pattern) = translate_codeowners_pattern(pat) {
111 let result = pattern.is_match(value);
112 cache.put(pat.to_owned(), pattern);
113 result
114 } else {
115 false
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_translate_codeowners_pattern() {
125 let pattern = "*.txt";
127 let regex = translate_codeowners_pattern(pattern).unwrap();
128 assert!(regex.is_match(b"file.txt"));
129 assert!(regex.is_match(b"file.txt/"));
130 assert!(regex.is_match(b"dir/file.txt"));
131
132 let pattern = "/dir/*.txt";
134 let regex = translate_codeowners_pattern(pattern).unwrap();
135 assert!(regex.is_match(b"/dir/file.txt"));
136 assert!(regex.is_match(b"dir/file.txt"));
137 assert!(!regex.is_match(b"/dir/subdir/file.txt"));
138
139 let pattern = "apps/";
141 let regex = translate_codeowners_pattern(pattern).unwrap();
142 assert!(regex.is_match(b"apps/file.txt"));
143 assert!(regex.is_match(b"/apps/file.txt"));
144 assert!(regex.is_match(b"/dir/apps/file.txt"));
145 assert!(regex.is_match(b"/dir/subdir/apps/file.txt"));
146
147 let pattern = "docs/*";
149 let regex = translate_codeowners_pattern(pattern).unwrap();
150 assert!(regex.is_match(b"docs/getting-started.md"));
151 assert!(!regex.is_match(b"docs/build-app/troubleshooting.md"));
152 assert!(!regex.is_match(b"something/docs/build-app/troubleshooting.md"));
153
154 let pattern = "*/docs/";
156 let regex = translate_codeowners_pattern(pattern).unwrap();
157 assert!(regex.is_match(b"first/docs/troubleshooting.md"));
158 assert!(!regex.is_match(b"docs/getting-started.md"));
159 assert!(!regex.is_match(b"first/second/docs/troubleshooting.md"));
160
161 let pattern = "docs/*/something/";
163 let regex = translate_codeowners_pattern(pattern).unwrap();
164 assert!(regex.is_match(b"docs/first/something/troubleshooting.md"));
165 assert!(!regex.is_match(b"something/docs/first/something/troubleshooting.md"));
166 assert!(!regex.is_match(b"docs/first/second/something/getting-started.md"));
167
168 let pattern = "/docs/";
170 let regex = translate_codeowners_pattern(pattern).unwrap();
171 assert!(regex.is_match(b"/docs/file.txt"));
172 assert!(regex.is_match(b"/docs/subdir/file.txt"));
173 assert!(regex.is_match(b"docs/subdir/file.txt"));
174 assert!(!regex.is_match(b"app/docs/file.txt"));
175
176 let pattern = "/red/**/file.py";
178 let regex = translate_codeowners_pattern(pattern).unwrap();
179 assert!(regex.is_match(b"red/orange/yellow/green/file.py"));
180 assert!(regex.is_match(b"/red/orange/file.py"));
181 assert!(regex.is_match(b"red/file.py"));
182 assert!(!regex.is_match(b"yellow/file.py"));
183
184 let pattern = "red/**/file.py";
186 let regex = translate_codeowners_pattern(pattern).unwrap();
187 assert!(regex.is_match(b"red/orange/yellow/green/file.py"));
188 assert!(regex.is_match(b"red/file.py"));
189 assert!(!regex.is_match(b"something/red/file.py"));
190
191 let pattern = "**/yellow/file.py";
193 let regex = translate_codeowners_pattern(pattern).unwrap();
194 assert!(regex.is_match(b"red/orange/yellow/file.py"));
195 assert!(regex.is_match(b"yellow/file.py"));
196 assert!(!regex.is_match(b"/docs/file.py"));
197
198 let pattern = "/red/orange/**";
200 let regex = translate_codeowners_pattern(pattern).unwrap();
201 assert!(regex.is_match(b"red/orange/yellow/file.py"));
202 assert!(regex.is_match(b"red/orange/file.py"));
203 assert!(!regex.is_match(b"blue/red/orange/file.py"));
204
205 let pattern = "/**/*.css";
207 let regex = translate_codeowners_pattern(pattern).unwrap();
208 assert!(regex.is_match(b"/docs/subdir/file.css"));
209 assert!(regex.is_match(b"file.css"));
210 assert!(!regex.is_match(b"/docs/file.txt"));
211 }
212}