use anyhow::{Result, anyhow};
use glob::Pattern;
pub struct PathFilter {
raw: Vec<String>,
compiled: Vec<Pattern>,
}
impl PathFilter {
pub fn new(patterns: &[String]) -> Result<Self> {
let compiled = patterns
.iter()
.map(|s| Pattern::new(normalize(s)).map_err(|e| anyhow!("invalid pattern `{s}`: {e}")))
.collect::<Result<Vec<_>>>()?;
Ok(Self {
raw: patterns.to_vec(),
compiled,
})
}
pub fn is_empty(&self) -> bool {
self.raw.is_empty()
}
pub fn matches(&self, path: &str) -> bool {
if self.raw.is_empty() {
return true;
}
let p = normalize(path);
for (raw, glob) in self.raw.iter().zip(&self.compiled) {
let r = normalize(raw);
if p == r || p.starts_with(&format!("{r}/")) || glob.matches(p) {
return true;
}
}
false
}
}
fn normalize(s: &str) -> &str {
s.trim_start_matches("./").trim_end_matches('/')
}
#[cfg(test)]
mod tests {
use super::*;
fn pf(patterns: &[&str]) -> PathFilter {
let owned: Vec<String> = patterns.iter().map(|s| (*s).to_owned()).collect();
PathFilter::new(&owned).unwrap()
}
#[test]
fn empty_matches_everything() {
let f = pf(&[]);
assert!(f.is_empty());
assert!(f.matches("anything"));
assert!(f.matches("./a/b/c"));
}
#[test]
fn exact_match() {
let f = pf(&["src/main.rs"]);
assert!(f.matches("src/main.rs"));
assert!(f.matches("./src/main.rs"));
assert!(!f.matches("src/lib.rs"));
}
#[test]
fn directory_prefix_match() {
let f = pf(&["src/"]);
assert!(f.matches("src/main.rs"));
assert!(f.matches("src/format/toc.rs"));
assert!(f.matches("src"));
assert!(!f.matches("tests/foo.rs"));
}
#[test]
fn glob_match() {
let f = pf(&["*.toml"]);
assert!(f.matches("Cargo.toml"));
assert!(!f.matches("README.md"));
}
#[test]
fn multiple_patterns_or_together() {
let f = pf(&["src/", "*.toml"]);
assert!(f.matches("src/main.rs"));
assert!(f.matches("Cargo.toml"));
assert!(!f.matches("README.md"));
}
#[test]
fn invalid_pattern_errors() {
let bad = "[unclosed".to_owned();
assert!(PathFilter::new(&[bad]).is_err());
}
}