use crate::grep::error::{GrepError, GrepResult};
#[cfg(feature = "globset")]
use globset::{GlobBuilder, GlobMatcher};
#[derive(Clone)]
pub struct EntryFilter {
pattern: String,
#[cfg(feature = "globset")]
matcher: GlobMatcher,
}
impl EntryFilter {
#[cfg(feature = "globset")]
pub fn new(pattern: &str) -> GrepResult<Self> {
let glob = GlobBuilder::new(pattern)
.literal_separator(true)
.build()
.map_err(|e| GrepError::glob_pattern(pattern, e.to_string()))?;
Ok(Self {
pattern: pattern.to_string(),
matcher: glob.compile_matcher(),
})
}
#[cfg(not(feature = "globset"))]
pub fn new(pattern: &str) -> GrepResult<Self> {
Ok(Self {
pattern: pattern.to_string(),
})
}
#[cfg(feature = "globset")]
pub fn matches(&self, path: &str) -> bool {
let normalized = path.trim_start_matches('/').trim_start_matches("./");
self.matcher.is_match(normalized)
}
#[cfg(not(feature = "globset"))]
pub fn matches(&self, path: &str) -> bool {
let normalized = path.trim_start_matches('/').trim_start_matches("./");
if self.pattern == "*" {
return true;
}
if let Some(prefix) = self.pattern.strip_suffix('*') {
if let Some(p) = prefix.strip_suffix('/') {
return normalized.starts_with(p);
}
return normalized.starts_with(prefix);
}
if let Some(suffix) = self.pattern.strip_prefix('*') {
return normalized.ends_with(suffix);
}
normalized == self.pattern || path == self.pattern
}
pub fn pattern(&self) -> &str {
&self.pattern
}
pub fn match_all() -> Self {
#[cfg(feature = "globset")]
{
Self::new("**/*").expect("** pattern should always compile")
}
#[cfg(not(feature = "globset"))]
{
Self {
pattern: "*".to_string(),
}
}
}
}
impl std::fmt::Debug for EntryFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntryFilter")
.field("pattern", &self.pattern)
.finish()
}
}
impl std::fmt::Display for EntryFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.pattern)
}
}
pub fn should_skip_entry(path: &str) -> bool {
let name = path.rsplit('/').next().unwrap_or(path);
let binary_extensions = [
"exe", "dll", "so", "dylib", "o", "obj", "a", "lib", "png", "jpg", "jpeg", "gif", "bmp",
"ico", "webp", "mp3", "mp4", "wav", "ogg", "flac", "avi", "mkv", "mov", "pdf", "doc",
"docx", "xls", "xlsx", "ppt", "pptx", "zip", "tar", "gz", "bz2", "xz", "7z", "rar",
"class", "pyc", "pyo", "wasm", "ttf", "otf", "woff", "woff2", "eot",
];
if let Some(ext) = name.rsplit('.').next() {
if binary_extensions.contains(&ext.to_lowercase().as_str()) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_all() {
let filter = EntryFilter::match_all();
assert!(filter.matches("anything"));
assert!(filter.matches("path/to/file.txt"));
}
#[cfg(feature = "globset")]
#[test]
fn test_glob_star() {
let filter = EntryFilter::new("*.rs").expect("should compile");
assert!(filter.matches("main.rs"));
assert!(filter.matches("lib.rs"));
assert!(!filter.matches("src/main.rs")); assert!(!filter.matches("main.txt"));
}
#[cfg(feature = "globset")]
#[test]
fn test_glob_double_star() {
let filter = EntryFilter::new("**/*.rs").expect("should compile");
assert!(filter.matches("main.rs"));
assert!(filter.matches("src/main.rs"));
assert!(filter.matches("src/foo/bar/baz.rs"));
assert!(!filter.matches("main.txt"));
}
#[cfg(feature = "globset")]
#[test]
fn test_glob_directory() {
let filter = EntryFilter::new("src/**").expect("should compile");
assert!(filter.matches("src/main.rs"));
assert!(filter.matches("src/foo/bar.rs"));
assert!(!filter.matches("tests/test.rs"));
}
#[cfg(feature = "globset")]
#[test]
fn test_glob_alternatives() {
let filter = EntryFilter::new("*.{rs,toml}").expect("should compile");
assert!(filter.matches("main.rs"));
assert!(filter.matches("Cargo.toml"));
assert!(!filter.matches("main.txt"));
}
#[test]
fn test_should_skip_binary() {
assert!(should_skip_entry("image.png"));
assert!(should_skip_entry("path/to/binary.exe"));
assert!(should_skip_entry("archive.zip"));
assert!(!should_skip_entry("source.rs"));
assert!(!should_skip_entry("readme.md"));
}
#[test]
fn test_normalized_paths() {
let filter = EntryFilter::match_all();
assert!(filter.matches("/absolute/path"));
assert!(filter.matches("./relative/path"));
}
}