use std::num::NonZeroUsize;
use std::sync::Mutex;
use lru::LruCache;
use once_cell::sync::Lazy;
use regex::bytes::Regex;
use crate::core::{RelayBuf, RelayStr};
static CODEOWNERS_CACHE: Lazy<Mutex<LruCache<String, Regex>>> =
Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(500).unwrap())));
fn translate_codeowners_pattern(pattern: &str) -> Option<Regex> {
let mut regex = String::new();
if pattern.starts_with('\\') {
return Regex::new(r"\\(?:\z|/)").ok();
}
let anchored = pattern
.find('/')
.map_or(false, |pos| pos != pattern.len() - 1);
if anchored {
regex += r"\A";
} else {
regex += r"(?:\A|/)";
}
let matches_dir = pattern.ends_with('/');
let mut pattern = pattern;
if matches_dir {
pattern = pattern.trim_end_matches('/');
}
let trailing_slash_star = pattern.len() > 1 && pattern.ends_with("/*");
let pattern_vec: Vec<char> = pattern.chars().collect();
let mut num_to_skip = None;
for (i, &ch) in pattern_vec.iter().enumerate() {
if i == 0 && anchored && pattern.starts_with('/') {
regex += r"/?";
continue;
}
if let Some(skip_amount) = num_to_skip {
num_to_skip = Some(skip_amount - 1);
if num_to_skip > Some(0) {
continue;
}
}
if ch == '*' {
if pattern_vec.get(i + 1) == Some(&'*') {
let left_anchored = i == 0;
let leading_slash = i > 0 && pattern_vec.get(i - 1) == Some(&'/');
let right_anchored = i + 2 == pattern.len();
let trailing_slash = pattern_vec.get(i + 2) == Some(&'/');
if (left_anchored || leading_slash) && (right_anchored || trailing_slash) {
regex += ".*";
num_to_skip = Some(2);
if trailing_slash {
regex += "/?";
num_to_skip = Some(3);
}
continue;
}
}
regex += r"[^/]*";
} else if ch == '?' {
regex += r"[^/]";
} else {
regex += ®ex::escape(ch.to_string().as_str());
}
}
if matches_dir {
regex += "/";
} else if trailing_slash_star {
regex += r"\z";
} else {
regex += r"(?:\z|/)";
}
Regex::new(®ex).ok()
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_is_codeowners_path_match(
value: *const RelayBuf,
pattern: *const RelayStr,
) -> bool {
let value = (*value).as_bytes();
let pat = (*pattern).as_str();
let mut cache = CODEOWNERS_CACHE.lock().unwrap();
if let Some(pattern) = cache.get(pat) {
pattern.is_match(value)
} else if let Some(pattern) = translate_codeowners_pattern(pat) {
let result = pattern.is_match(value);
cache.put(pat.to_owned(), pattern);
result
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_translate_codeowners_pattern() {
let pattern = "*.txt";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"file.txt"));
assert!(regex.is_match(b"file.txt/"));
assert!(regex.is_match(b"dir/file.txt"));
let pattern = "/dir/*.txt";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"/dir/file.txt"));
assert!(regex.is_match(b"dir/file.txt"));
assert!(!regex.is_match(b"/dir/subdir/file.txt"));
let pattern = "apps/";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"apps/file.txt"));
assert!(regex.is_match(b"/apps/file.txt"));
assert!(regex.is_match(b"/dir/apps/file.txt"));
assert!(regex.is_match(b"/dir/subdir/apps/file.txt"));
let pattern = "docs/*";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"docs/getting-started.md"));
assert!(!regex.is_match(b"docs/build-app/troubleshooting.md"));
assert!(!regex.is_match(b"something/docs/build-app/troubleshooting.md"));
let pattern = "*/docs/";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"first/docs/troubleshooting.md"));
assert!(!regex.is_match(b"docs/getting-started.md"));
assert!(!regex.is_match(b"first/second/docs/troubleshooting.md"));
let pattern = "docs/*/something/";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"docs/first/something/troubleshooting.md"));
assert!(!regex.is_match(b"something/docs/first/something/troubleshooting.md"));
assert!(!regex.is_match(b"docs/first/second/something/getting-started.md"));
let pattern = "/docs/";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"/docs/file.txt"));
assert!(regex.is_match(b"/docs/subdir/file.txt"));
assert!(regex.is_match(b"docs/subdir/file.txt"));
assert!(!regex.is_match(b"app/docs/file.txt"));
let pattern = "/red/**/file.py";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"red/orange/yellow/green/file.py"));
assert!(regex.is_match(b"/red/orange/file.py"));
assert!(regex.is_match(b"red/file.py"));
assert!(!regex.is_match(b"yellow/file.py"));
let pattern = "red/**/file.py";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"red/orange/yellow/green/file.py"));
assert!(regex.is_match(b"red/file.py"));
assert!(!regex.is_match(b"something/red/file.py"));
let pattern = "**/yellow/file.py";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"red/orange/yellow/file.py"));
assert!(regex.is_match(b"yellow/file.py"));
assert!(!regex.is_match(b"/docs/file.py"));
let pattern = "/red/orange/**";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"red/orange/yellow/file.py"));
assert!(regex.is_match(b"red/orange/file.py"));
assert!(!regex.is_match(b"blue/red/orange/file.py"));
let pattern = "/**/*.css";
let regex = translate_codeowners_pattern(pattern).unwrap();
assert!(regex.is_match(b"/docs/subdir/file.css"));
assert!(regex.is_match(b"file.css"));
assert!(!regex.is_match(b"/docs/file.txt"));
}
}