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