use std::collections::HashSet;
use std::path::{Path, PathBuf};
use ignore::WalkBuilder;
use super::config::FileResolverConfig;
const GLOB_CHARS: &[char] = &['*', '?', '['];
pub struct FileResolver {
config: FileResolverConfig,
exclude_patterns: Vec<String>,
compiled_includes: Vec<glob::Pattern>,
compiled_dir_excludes: Vec<glob::Pattern>,
}
impl FileResolver {
pub fn new(config: FileResolverConfig) -> Self {
let exclude_patterns = config.effective_exclude();
let include_patterns = config.effective_include();
let compiled_includes =
include_patterns.iter().filter_map(|p| glob::Pattern::new(p).ok()).collect();
let compiled_dir_excludes = exclude_patterns
.iter()
.filter(|p| p.ends_with('/'))
.filter_map(|p| glob::Pattern::new(&p[..p.len() - 1]).ok())
.collect();
Self { config, exclude_patterns, compiled_includes, compiled_dir_excludes }
}
pub fn resolve(&mut self, paths: &[&str]) -> std::io::Result<Vec<PathBuf>> {
let mut seen: HashSet<PathBuf> = HashSet::new();
let mut result: Vec<PathBuf> = Vec::new();
for raw_path in paths {
let p = Path::new(raw_path);
if p.is_file() {
let resolved = canonicalize_or_absolute(p);
if !seen.contains(&resolved) && self.should_include_explicit(p) {
seen.insert(resolved.clone());
result.push(resolved);
}
} else if p.is_dir() {
for found in self.walk_directory(p) {
if !seen.contains(&found) {
seen.insert(found.clone());
result.push(found);
}
}
} else if raw_path.contains(GLOB_CHARS) {
for found in self.expand_glob(raw_path) {
let resolved = canonicalize_or_absolute(&found);
if !seen.contains(&resolved) {
seen.insert(resolved.clone());
result.push(resolved);
}
}
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path not found: {raw_path}"),
));
}
}
result.sort();
Ok(result)
}
fn should_include_explicit(&self, path: &Path) -> bool {
if self.config.force_exclude {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if matches_any_pattern(&self.exclude_patterns, name) {
return false;
}
}
if let Some(parent) = path.parent() {
for component in parent.components() {
let part = component.as_os_str().to_string_lossy();
let dir_with_slash = format!("{part}/");
if matches_any_pattern(&self.exclude_patterns, &dir_with_slash) {
return false;
}
}
}
}
!self.exceeds_max_size(path)
}
fn walk_directory(&self, root: &Path) -> Vec<PathBuf> {
let canonical_root = root.canonicalize().unwrap_or_else(|_| {
if root.is_absolute() {
root.to_path_buf()
} else {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")).join(root)
}
});
let mut builder = WalkBuilder::new(&canonical_root);
builder.hidden(false);
builder.ignore(false);
builder.git_ignore(self.config.respect_gitignore);
builder.git_global(false);
builder.git_exclude(false);
builder.parents(false);
builder.require_git(false);
builder.add_custom_ignore_filename(format!(".{}ignore", self.config.tool_name));
if self.config.files_max_size > 0 {
builder.max_filesize(Some(self.config.files_max_size));
}
let dir_excludes = self.compiled_dir_excludes.clone();
builder.filter_entry(move |entry| {
if entry.depth() == 0 {
return true;
}
let Some(ft) = entry.file_type() else { return true };
if !ft.is_dir() {
return true;
}
let name = entry.file_name().to_string_lossy();
!dir_excludes.iter().any(|p| p.matches(&name))
});
let compiled_includes = &self.compiled_includes;
let mut results = Vec::new();
for entry in builder.build().flatten() {
let Some(ft) = entry.file_type() else { continue };
if !ft.is_file() {
continue;
}
let path = entry.into_path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if compiled_includes.iter().any(|p| p.matches(name)) {
results.push(path);
}
}
}
results
}
fn expand_glob(&self, pattern: &str) -> Vec<PathBuf> {
let mut results = Vec::new();
let Ok(entries) = glob::glob(pattern) else {
return results;
};
for entry in entries.flatten() {
if entry.is_file() {
if !self.glob_entry_passes_filters(&entry) {
continue;
}
results.push(entry);
}
}
results
}
fn glob_entry_passes_filters(&self, entry: &Path) -> bool {
let Some(name) = entry.file_name().and_then(|n| n.to_str()) else {
return false;
};
if !self.compiled_includes.iter().any(|p| p.matches(name)) {
return false;
}
if self.exceeds_max_size(entry) {
return false;
}
for component in entry.components() {
if Some(component.as_os_str()) == entry.file_name() {
continue;
}
let part = component.as_os_str().to_string_lossy();
if self.compiled_dir_excludes.iter().any(|p| p.matches(&part)) {
return false;
}
}
true
}
fn exceeds_max_size(&self, path: &Path) -> bool {
if self.config.files_max_size == 0 {
return false;
}
match path.metadata() {
Ok(meta) => meta.len() > self.config.files_max_size,
Err(_) => false,
}
}
}
fn matches_any_pattern(patterns: &[String], name: &str) -> bool {
let is_dir_query = name.ends_with('/');
let bare_name = if is_dir_query { &name[..name.len() - 1] } else { name };
for pattern in patterns {
let pattern_is_dir = pattern.ends_with('/');
let bare_pattern =
if pattern_is_dir { &pattern[..pattern.len() - 1] } else { pattern.as_str() };
if pattern_is_dir && !is_dir_query {
continue;
}
if let Ok(glob_pattern) = glob::Pattern::new(bare_pattern) {
if glob_pattern.matches(bare_name) {
return true;
}
}
}
false
}
fn canonicalize_or_absolute(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| {
if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")).join(path)
}
})
}