relay_cabi/
codeowners.rs

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
10/// LRU cache for [`Regex`]s in relation to the provided string pattern.
11static 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    // Special case backslash can match a backslash file or directory
18    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    // patterns ending with "/*" are special. They only match items directly in the directory
39    // not deeper
40    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        // Anchored paths may or may not start with a slash
47        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            // Prevents everything after the * in the pattern from being skipped
55            if num_to_skip > Some(0) {
56                continue;
57            }
58        }
59
60        if ch == '*' {
61            // Handle double star (**) case properly
62            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                    // Allows the trailing slash after ** to be optional
72                    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 += &regex::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(&regex).ok()
95}
96
97/// Returns `true` if the codeowners path matches the value, `false` otherwise.
98#[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        // Matches "*.txt" anywhere it appears
127        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        // Matches leading "/dir/<x>.txt", where the leading / is optional and <x> is a single dir or filename
134        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        // Matches "apps/" anywhere it appears
141        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        // Matches leading "docs/<x>", where <x> is a single dir or filename
149        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        // Matches leading "<x>/docs/", where <x> is a single directory
156        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        // Matches leading "docs/<x>/something/", where <x> is a single dir
163        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        // Matches leading "/docs/", where the leading / is optional
170        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        // Matches leading "/red/<x>/file.py", where the leading / is optional and <x> is 0 or more dirs
178        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        // Matches leading "red/<x>/file.py", where <x> is 0 or more dirs
186        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        // Matches "<x>/yellow/file.py", where the leading / is optional and <x> is 0 or more dirs
193        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        // Matches "/red/orange/**"", where the leading / is optional and <x> is 0 or more dirs/filenames
200        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        // Matches any ".css" file
207        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}