use std::ffi::OsStr; use anyhow::{Result, anyhow}; use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; use memchr::memmem; use regex::bytes::{RegexSet, RegexSetBuilder}; use crate::regex_helper::{self, pattern_matches_strings_with_leading_dot}; pub trait Matcher { fn matches_path(&self, path: &OsStr) -> bool; } pub type Patterns = Box; #[derive(Eq, PartialEq, Copy, Clone)] pub enum PatternType { Regex, Fixed, Glob, Exact, } impl Matcher for RegexSet { fn matches_path(&self, path: &OsStr) -> bool { let haystack = path.as_encoded_bytes(); let matches = self.matches(haystack); // Return true if the number of regexes that matched // equals the total number of regexes. matches.iter().count() == self.len() } } impl Matcher for GlobSet { fn matches_path(&self, path: &OsStr) -> bool { self.matches_all(path) } } /// In the common case a single glob, it is simpler, and /// faster to just use a single Glob instead of a GlobSet impl Matcher for GlobMatcher { fn matches_path(&self, path: &OsStr) -> bool { self.is_match(path) } } /// Matcher that matches fixed strings pub struct FixedStrings(pub Vec); impl Matcher for FixedStrings { fn matches_path(&self, path: &OsStr) -> bool { let path = path.as_encoded_bytes(); self.0.iter().all(|f| bytes_contains(path, f.as_bytes())) } } pub struct ExactStrings(pub Vec); impl Matcher for ExactStrings { fn matches_path(&self, path: &OsStr) -> bool { self.0.iter().all(|s| s.as_str() == path) } } /// Matcher that matches everything pub struct MatchAll; impl Matcher for MatchAll { fn matches_path(&self, _path: &OsStr) -> bool { true } } pub fn build_patterns( mut patterns: Vec, pattern_type: PatternType, ignore_case: bool, ) -> Result { if patterns.is_empty() || is_empty_pattern(&patterns, pattern_type) { return Ok(Box::new(MatchAll)); } match pattern_type { PatternType::Glob => build_glob_matcher(patterns, ignore_case), PatternType::Fixed if !ignore_case => Ok(Box::new(FixedStrings(patterns))), PatternType::Exact if !ignore_case => Ok(Box::new(ExactStrings(patterns))), typ => { // TODO: is there a better way we could handle case insensitive fixed strings? if typ == PatternType::Fixed { for pattern in patterns.iter_mut() { *pattern = regex::escape(pattern); } } else if typ == PatternType::Exact { // Would using eq_ignore_ascii case be good enough? for pattern in patterns.iter_mut() { // Anchor the escaped pattern so the full filename (or path) must match exactly. // Literal. No substring matching. *pattern = format!("^{}$", regex::escape(pattern)); } } Ok(Box::new(build_regex_matcher(patterns, ignore_case)?)) } } } pub fn patterns_match_strings_with_leading_dots( patterns: &[String], pattern_type: PatternType, ) -> bool { let mut iter = patterns.iter(); match pattern_type { PatternType::Regex => iter.any(|pat| pattern_matches_strings_with_leading_dot(pat)), // fixed strings aren't anchored so always false PatternType::Fixed => false, // globs and exact just check if it starts with a . PatternType::Glob | PatternType::Exact => patterns.iter().any(|pat| pat.starts_with(".")), } } pub fn pattern_has_uppercase_char(pattern_type: PatternType, pattern: &str) -> bool { match pattern_type { PatternType::Regex => regex_helper::pattern_has_uppercase_char(pattern), // For everything else it's sufficient to just check if there are any uppercase letters _ => pattern.chars().any(char::is_uppercase), } } fn build_glob_matcher(patterns: Vec, ignore_case: bool) -> Result { Ok(if patterns.len() == 1 { Box::new(build_glob(&patterns[0], ignore_case)?.compile_matcher()) } else { let mut builder = GlobSetBuilder::new(); for pat in patterns { builder.add(build_glob(&pat, ignore_case)?); } Box::new(builder.build()?) }) } fn build_glob(pattern: &str, ignore_case: bool) -> Result { Ok(GlobBuilder::new(pattern) .literal_separator(true) .case_insensitive(ignore_case) .build()?) } fn build_regex_matcher(patterns: Vec, ignore_case: bool) -> Result { RegexSetBuilder::new(patterns) .case_insensitive(ignore_case) .dot_matches_new_line(true) .build() .map_err(|e| { anyhow!( "{}\n\nNote: You can search for literal substrings with '--fixed-strings' \ or literal strings with '--exact' options (instead of a regular expression). \ Alternatively, you can also use the '--glob' option to match on a glob pattern.", e, ) }) } fn is_empty_pattern(patterns: &[String], pattern_type: PatternType) -> bool { pattern_type != PatternType::Exact && patterns.len() == 1 && patterns[0].is_empty() } /// Test if the needle is a substring of the haystack fn bytes_contains(haystack: &[u8], needle: &[u8]) -> bool { memmem::find(haystack, needle).is_some() } #[test] fn empty_matches_all() { let patterns_none = build_patterns(Vec::new(), PatternType::Glob, false).unwrap(); let patterns_empty_str = build_patterns(vec!["".to_string()], PatternType::Glob, false).unwrap(); assert!(patterns_none.matches_path("abcd".as_ref())); assert!(patterns_empty_str.matches_path("abcd".as_ref())); }