relay_cabi/
codeowners.rs

1use std::num::NonZeroUsize;
2use std::sync::{LazyLock, Mutex};
3
4use lru::LruCache;
5use regex::bytes::Regex;
6
7use crate::core::{RelayBuf, RelayStr};
8
9/// LRU cache for [`Regex`]s in relation to the provided string pattern.
10static 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    // Special case backslash can match a backslash file or directory
17    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    // patterns ending with "/*" are special. They only match items directly in the directory
38    // not deeper
39    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        // Anchored paths may or may not start with a slash
46        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            // Prevents everything after the * in the pattern from being skipped
54            if num_to_skip > Some(0) {
55                continue;
56            }
57        }
58
59        if ch == '*' {
60            // Handle double star (**) case properly
61            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                        // **/ matches zero or more complete path segments
70                        regex += "(?:.*/)?";
71                        num_to_skip = Some(3);
72                    } else {
73                        // ** at end matches anything
74                        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 += &regex::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(&regex).ok()
96}
97
98/// Returns `true` if the codeowners path matches the value, `false` otherwise.
99#[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        // Matches "*.txt" anywhere it appears
128        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        // Matches leading "/dir/<x>.txt", where the leading / is optional and <x> is a single dir or filename
135        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        // Matches "apps/" anywhere it appears
142        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        // Matches leading "docs/<x>", where <x> is a single dir or filename
150        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        // Matches leading "<x>/docs/", where <x> is a single directory
157        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        // Matches leading "docs/<x>/something/", where <x> is a single dir
164        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        // Matches leading "/docs/", where the leading / is optional
171        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        // Matches leading "/red/<x>/file.py", where the leading / is optional and <x> is 0 or more dirs
179        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        // Matches leading "red/<x>/file.py", where <x> is 0 or more dirs
187        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        // Matches "<x>/yellow/file.py", where the leading / is optional and <x> is 0 or more dirs
194        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        // Matches "/red/orange/**"", where the leading / is optional and <x> is 0 or more dirs/filenames
201        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        // Matches any ".css" file
208        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        // /**/<filename> should match <filename> exactly anywhere it appears
215        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}