#[must_use]
pub fn matches_any(path: &str, patterns: &[String]) -> bool {
let path = path.strip_prefix("./").unwrap_or(path);
patterns.iter().any(|p| matches(path, p))
}
#[must_use]
pub fn matches(path: &str, pattern: &str) -> bool {
if pattern.is_empty() {
return false;
}
let expanded;
let pattern = if let Some(dir) = pattern.strip_suffix('/') {
expanded = format!("{dir}/**");
&expanded
} else {
pattern
};
match_here(path.as_bytes(), pattern.as_bytes())
}
fn match_here(path: &[u8], pat: &[u8]) -> bool {
let (Some(&p0), Some(rest)) = (pat.first(), pat.get(1..)) else {
return path.is_empty();
};
match p0 {
b'*' => {
if rest.first() == Some(&b'*') {
let tail = &rest[1..];
if let Some(after_slash) = tail.strip_prefix(b"/")
&& match_here(path, after_slash)
{
return true;
}
(0..=path.len()).any(|i| match_here(&path[i..], tail))
} else {
let limit = path.iter().position(|&c| c == b'/').unwrap_or(path.len());
(0..=limit).any(|i| match_here(&path[i..], rest))
}
}
b'?' => match path.first() {
Some(&c) if c != b'/' => match_here(&path[1..], rest),
_ => false,
},
lit => match path.first() {
Some(&c) if c == lit => match_here(&path[1..], rest),
_ => false,
},
}
}
#[cfg(test)]
mod tests {
use super::{matches, matches_any};
#[test]
fn a_literal_pattern_matches_only_itself() {
assert!(matches("scratch/cut.ink", "scratch/cut.ink"));
assert!(!matches("scratch/cut.ink", "scratch/cut.brink"));
assert!(!matches("scratch/cut.ink", "scratch/cut"));
assert!(!matches("a/scratch/cut.ink", "scratch/cut.ink"));
}
#[test]
fn a_star_stops_at_a_separator() {
assert!(matches("scratch/cut.ink", "scratch/*.ink"));
assert!(matches("scratch/cut.ink", "*/cut.ink"));
assert!(!matches("scratch/deep/cut.ink", "scratch/*.ink"));
assert!(!matches("scratch/cut.ink", "*.ink"));
}
#[test]
fn a_double_star_crosses_separators() {
assert!(matches("scratch/deep/cut.ink", "scratch/**"));
assert!(matches("scratch/deep/cut.ink", "**/cut.ink"));
assert!(matches("scratch/deep/cut.ink", "**.ink"));
assert!(matches("scratch/cut.ink", "scratch/**/cut.ink"));
assert!(matches("scratch/cut.ink", "**/scratch/cut.ink"));
assert!(!matches("notes/cut.ink", "scratch/**"));
}
#[test]
fn a_trailing_slash_is_sugar_for_everything_under_it() {
assert!(matches("scratch/cut.ink", "scratch/"));
assert!(matches("scratch/deep/cut.ink", "scratch/"));
assert!(!matches("scratch", "scratch/"));
assert!(!matches("scratchpad/cut.ink", "scratch/"));
}
#[test]
fn a_bare_directory_name_does_not_cover_its_contents() {
assert!(!matches("scratch/cut.ink", "scratch"));
assert!(matches("scratch", "scratch"));
}
#[test]
fn a_question_mark_is_one_non_separator_character() {
assert!(matches("act1.ink", "act?.ink"));
assert!(!matches("act10.ink", "act?.ink"));
assert!(!matches("a/b", "a?b"));
}
#[test]
fn matching_is_case_sensitive() {
assert!(!matches("Scratch/cut.ink", "scratch/**"));
}
#[test]
fn an_empty_pattern_matches_nothing() {
assert!(!matches("", ""));
assert!(!matches("cut.ink", ""));
}
#[test]
fn matches_any_is_the_or_of_its_patterns_and_tolerates_a_dot_slash() {
let pats = vec!["scratch/**".to_owned(), "*.draft.ink".to_owned()];
assert!(matches_any("scratch/cut.ink", &pats));
assert!(matches_any("./scratch/cut.ink", &pats));
assert!(matches_any("aside.draft.ink", &pats));
assert!(!matches_any("main.ink", &pats));
assert!(!matches_any("main.ink", &[]));
}
}