use super::glob::{DEFAULT_EXCLUDE_GLOBS, get_glob_set, longest_base_path_wild_free};
use crate::{ListOptions, Result, SPath, get_depth};
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;
use walkdir::WalkDir;
pub struct GlobsFileIter {
inner: Box<dyn Iterator<Item = SPath>>,
}
impl GlobsFileIter {
pub fn new(
dir: impl AsRef<Path>,
include_globs: Option<&[&str]>,
list_options: Option<ListOptions<'_>>,
) -> Result<Self> {
let main_base = SPath::from_std_path(dir.as_ref())?;
let (include_patterns, negated_excludes) = if let Some(globs) = include_globs {
let mut includes = Vec::new();
let mut excludes = Vec::new();
for &pattern in globs {
if let Some(negative_pattern) = pattern.strip_prefix("!") {
excludes.push(negative_pattern);
} else {
includes.push(pattern);
}
}
if includes.is_empty() && !excludes.is_empty() {
(vec!["**"], excludes)
} else {
(includes, excludes)
}
} else {
(vec!["**"], Vec::new())
};
let list_options = if !negated_excludes.is_empty() {
match list_options {
Some(opts) => {
let mut new_opts = ListOptions {
exclude_globs: opts.exclude_globs.clone(),
relative_glob: opts.relative_glob,
depth: opts.depth,
};
if let Some(existing_excludes) = &mut new_opts.exclude_globs {
let mut combined = existing_excludes.clone();
combined.extend(negated_excludes);
new_opts.exclude_globs = Some(combined);
} else {
new_opts.exclude_globs = Some(negated_excludes);
}
Some(new_opts)
}
None => {
Some(ListOptions {
exclude_globs: Some(negated_excludes),
relative_glob: false,
depth: None,
})
}
}
} else {
list_options
};
let groups = process_globs(&main_base, &include_patterns)?;
let use_relative_glob = list_options.as_ref().is_some_and(|o| o.relative_glob);
let exclude_globs_raw: Option<&[&str]> = list_options.as_ref().and_then(|o| o.exclude_globs());
let exclude_globs_set = exclude_globs_raw
.or(Some(DEFAULT_EXCLUDE_GLOBS))
.map(get_glob_set)
.transpose()?;
let mut group_iterators: Vec<Box<dyn Iterator<Item = SPath>>> = Vec::new();
let max_depth = list_options.and_then(|o| o.depth);
let exclude_globs_set = Arc::new(exclude_globs_set);
for GlobGroup {
base: group_base,
patterns,
prefixes,
} in groups.into_iter()
{
let pats: Vec<&str> = patterns.iter().map(|s| s.as_str()).collect();
let depth = get_depth(&pats, max_depth);
let globset = get_glob_set(&pats)?;
let allowed_prefixes = Arc::new(prefixes);
let base_clone_for_dirs = group_base.clone();
let exclude_globs_set_clone = exclude_globs_set.clone();
let allowed_prefixes_for_dirs = allowed_prefixes.clone();
let iter = WalkDir::new(group_base.path())
.max_depth(depth)
.into_iter()
.filter_entry(move |e| {
let Ok(path) = SPath::from_std_path(e.path()) else {
return false;
};
let is_dir = e.file_type().is_dir();
if is_dir {
if let Some(exclude_globs) = exclude_globs_set_clone.as_ref() {
if use_relative_glob {
if let Some(rel_path) = path.diff(&base_clone_for_dirs)
&& exclude_globs.is_match(&rel_path)
{
return false;
}
} else if exclude_globs.is_match(&path) {
return false;
}
}
if !allowed_prefixes_for_dirs.is_empty()
&& !directory_matches_allowed_prefixes(
&path,
&base_clone_for_dirs,
allowed_prefixes_for_dirs.as_ref(),
) {
return false;
}
}
true
})
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_file())
.filter_map(SPath::from_walkdir_entry_ok);
let exclude_globs_set_clone = exclude_globs_set.clone();
let main_base_clone = main_base.clone();
let base_clone = group_base.clone();
let iter = iter.filter(move |sfile| {
if let Some(exclude) = exclude_globs_set_clone.as_ref() {
if use_relative_glob {
if let Some(rel_path) = sfile.diff(&main_base_clone)
&& exclude.is_match(&rel_path)
{
return false;
}
} else if exclude.is_match(sfile) {
return false;
}
}
let rel_path = match sfile.diff(base_clone.path()) {
Some(p) => p,
None => return false,
};
globset.is_match(rel_path)
});
group_iterators.push(Box::new(iter));
}
let combined_iter = group_iterators.into_iter().fold(
Box::new(std::iter::empty()) as Box<dyn Iterator<Item = SPath>>,
|acc, iter| Box::new(acc.chain(iter)) as Box<dyn Iterator<Item = SPath>>,
);
let dedup_iter = combined_iter
.scan(HashSet::<SPath>::new(), |seen, file| {
let path = file.clone();
if seen.insert(path) {
Some(Some(file))
} else {
Some(None)
}
})
.flatten();
Ok(GlobsFileIter {
inner: Box::new(dedup_iter),
})
}
}
impl Iterator for GlobsFileIter {
type Item = SPath;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
struct GlobGroup {
base: SPath,
patterns: Vec<String>,
prefixes: Vec<String>,
}
fn process_globs(main_base: &SPath, globs: &[&str]) -> Result<Vec<GlobGroup>> {
let mut groups: Vec<(SPath, Vec<String>)> = Vec::new();
let mut relative_patterns: Vec<String> = Vec::new();
for &glob in globs {
let path_glob = SPath::new(glob);
if path_glob.is_absolute() {
let abs_base = longest_base_path_wild_free(&path_glob);
let rel_pattern = relative_from_absolute(&path_glob, &abs_base);
if let Some((_, patterns)) = groups.iter_mut().find(|(b, _)| b.as_str() == abs_base.as_str()) {
patterns.push(rel_pattern);
} else {
groups.push((abs_base, vec![rel_pattern]));
}
} else {
let cleaned = glob.trim_start_matches("./").to_string();
let base_candidate: &str = main_base.as_str();
let base_str_cleaned = {
let s = base_candidate.trim_start_matches("./");
if s.is_empty() {
String::new()
} else {
let mut t = s.to_string();
if !t.ends_with("/") {
t.push('/');
}
t
}
};
if !base_str_cleaned.is_empty() && cleaned.starts_with(&base_str_cleaned) {
let relative = cleaned[base_str_cleaned.len()..].to_string();
relative_patterns.push(relative);
} else {
relative_patterns.push(cleaned);
}
}
}
if !relative_patterns.is_empty() {
groups.push((main_base.clone(), relative_patterns));
}
groups.sort_by_key(|(base, _)| base.as_str().len());
let mut final_groups: Vec<GlobGroup> = Vec::new();
for (base, patterns) in groups {
let mut merged = false;
for existing_group in final_groups.iter_mut() {
if existing_group.base.starts_with(&base) {
let diff = base.diff(&existing_group.base).map(|p| p.to_string()).unwrap_or_default();
for pat in patterns.iter() {
let new_pat = if diff.is_empty() {
pat.to_string()
} else {
SPath::new(&diff).join(pat).to_string()
};
existing_group.patterns.push(new_pat.clone());
}
let mut new_prefixes = Vec::new();
let mut full_traversal_needed = false;
for pat in existing_group.patterns.iter() {
let pfx = glob_literal_prefixes(pat);
if pfx.is_empty() {
full_traversal_needed = true;
break;
}
append_adjusted(&mut new_prefixes, &pfx);
}
if full_traversal_needed {
existing_group.prefixes.clear();
} else {
normalize_prefixes(&mut new_prefixes);
existing_group.prefixes = new_prefixes;
}
merged = true;
break;
} else if base.starts_with(&existing_group.base) {
let diff_segment = base.diff(&existing_group.base).map(|p| p.to_string()).unwrap_or_default();
for pat in patterns.iter() {
let new_pat = if diff_segment.is_empty() {
pat.clone()
} else {
SPath::new(&diff_segment).join(pat).to_string()
};
existing_group.patterns.push(new_pat);
}
let mut new_prefixes = Vec::new();
let mut full_traversal_needed = false;
for pat in existing_group.patterns.iter() {
let pfx = glob_literal_prefixes(pat);
if pfx.is_empty() {
full_traversal_needed = true;
break;
}
append_adjusted(&mut new_prefixes, &pfx);
}
if full_traversal_needed {
existing_group.prefixes.clear();
} else {
normalize_prefixes(&mut new_prefixes);
existing_group.prefixes = new_prefixes;
}
merged = true;
break;
}
}
if !merged {
let mut prefixes = Vec::new();
let mut full_traversal_needed = false;
for pat in patterns.iter() {
let pfx = glob_literal_prefixes(pat);
if pfx.is_empty() {
full_traversal_needed = true;
break;
}
append_adjusted(&mut prefixes, &pfx);
}
if full_traversal_needed {
prefixes.clear();
} else {
normalize_prefixes(&mut prefixes);
}
final_groups.push(GlobGroup {
base,
patterns,
prefixes,
});
}
}
Ok(final_groups)
}
fn relative_from_absolute(glob: &SPath, group_base: &SPath) -> String {
glob.diff(group_base).map(|p| p.to_string()).unwrap_or_else(|| glob.to_string())
}
fn directory_matches_allowed_prefixes(path: &SPath, base: &SPath, prefixes: &[String]) -> bool {
if prefixes.is_empty() {
return true;
}
if path.as_str() == base.as_str() {
return true;
}
let Some(mut rel_path) = path.diff(base.path()) else {
return true;
};
{
let rel_str = rel_path.as_str();
if let Some(stripped) = rel_str.strip_prefix("./") {
if stripped.is_empty() {
return true;
}
rel_path = SPath::new(stripped);
} else if rel_str.is_empty() {
return true;
}
}
prefixes.iter().any(|prefix| {
let prefix = prefix.as_str();
if prefix.is_empty() {
return true;
}
let prefix_spath = SPath::new(prefix);
rel_path.starts_with(&prefix_spath) || prefix_spath.starts_with(&rel_path)
})
}
fn glob_literal_prefixes(pattern: &str) -> Vec<String> {
let clean = pattern.trim_start_matches("./");
if clean.is_empty() {
return Vec::new();
}
let segments: Vec<&str> = clean.split('/').filter(|s| !s.is_empty() && *s != ".").collect();
if segments.len() <= 1 {
return Vec::new();
}
let mut prefixes = vec![String::new()];
for &segment in segments.iter().take(segments.len() - 1) {
if segment == ".." || segment_contains_wildcard(segment) {
break;
}
let mut next = Vec::new();
if let Some(options) = expand_brace_segment(segment) {
for prefix in &prefixes {
for option in options.iter() {
let new_prefix = if prefix.is_empty() {
option.clone()
} else {
SPath::new(prefix).join(option).to_string()
};
next.push(new_prefix);
}
}
} else if segment.contains('{') || segment.contains('}') {
break;
} else {
for prefix in &prefixes {
let new_prefix = if prefix.is_empty() {
segment.to_string()
} else {
SPath::new(prefix).join(segment).to_string()
};
next.push(new_prefix);
}
}
if next.is_empty() {
break;
}
prefixes = next;
}
if prefixes.len() == 1 && prefixes[0].is_empty() {
Vec::new()
} else {
prefixes
}
}
fn expand_brace_segment(segment: &str) -> Option<Vec<String>> {
if segment.starts_with('{') && segment.ends_with('}') {
let inner = &segment[1..segment.len() - 1];
if inner.contains('{') || inner.contains('}') {
return None;
}
let options: Vec<String> = inner
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
if options.is_empty() { None } else { Some(options) }
} else {
None
}
}
fn segment_contains_wildcard(segment: &str) -> bool {
segment.contains('*') || segment.contains('?') || segment.contains('[')
}
fn append_adjusted(target: &mut Vec<String>, values: &[String]) {
for value in values {
target.push(value.to_string());
}
}
fn normalize_prefixes(prefixes: &mut Vec<String>) {
if prefixes.is_empty() {
return;
}
if prefixes.iter().any(|p| p.is_empty()) {
prefixes.clear();
return;
}
prefixes.sort();
prefixes.dedup();
}