use ::std::fs;
use ::std::fs::DirEntry;
use ::std::path::Path;
use ::std::path::PathBuf;
use ::itertools::Itertools;
use ::log::debug;
use ::log::trace;
use ::regex::Regex;
use ::smallvec::{smallvec, SmallVec};
use crate::filter::{unique_prefix, Keep, Order as UniqueOrder};
use crate::find::Nested::StopOnMatch;
use crate::find::OnErr;
use crate::find::Order;
use crate::find::{DirWithArgs, PathModification};
enum IsMatch {
Include,
Exclude,
NoMatch,
}
fn validate_roots_unique(roots: &[PathBuf]) -> Result<(), String> {
let unique_roots = unique_prefix(
roots
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect(),
UniqueOrder::SortAscending,
Keep::First,
);
if unique_roots.len() < roots.len() {
return Err(format!(
"root directories (-r) overlap; unique ones are: {}",
unique_roots.iter().join(", ")
));
}
Ok(())
}
type Dirs = SmallVec<[PathBuf; 2]>;
pub fn find_dir_with(args: DirWithArgs) -> Result<Vec<PathBuf>, String> {
debug!("args = {:?}", args);
validate_roots_unique(&args.roots)?;
let mut results = vec![];
for root in &args.roots {
debug!("searching root '{}'", root.to_str().unwrap());
let mut matches = find_matching_dirs(root, &args, args.max_depth)?;
if args.path_modification == PathModification::Relative {
matches = matches
.into_iter()
.map(|pth| {
pth.strip_prefix(root)
.expect("failed to make path relative")
.to_path_buf()
})
.collect();
}
results.extend(matches);
}
if args.order == Order::SortAscending {
results.sort_unstable();
}
Ok(results)
}
fn find_matching_dirs(
parent: &Path,
args: &DirWithArgs,
depth_remaining: u32,
) -> Result<Dirs, String> {
if depth_remaining == 0 {
return Ok(smallvec![]);
}
let content = dir_listing(parent, args.on_err)?;
let children_count_in_range = args.child_count_range.includes(content.len() as u32);
let mut results: Dirs;
let mut current_is_match = false;
let parent_match = if_count_ok(
children_count_in_range,
is_parent_match(parent, &args.itself, &args.not_self),
);
results = match parent_match {
IsMatch::Include => {
let found = parent.to_path_buf();
if args.nested == StopOnMatch {
debug!(
"found a match based on parent name: {}, not recursing deeper",
parent.to_str().unwrap()
);
return Ok(smallvec![found]);
}
debug!(
"found a match based on parent name: {}, searching deeper",
parent.to_str().unwrap()
);
current_is_match = true;
smallvec![found]
}
IsMatch::Exclude => return Ok(smallvec![]),
IsMatch::NoMatch => smallvec![],
};
trace!(
"found {} items in {}",
content.len(),
parent.to_str().unwrap()
);
for sub in &content {
if current_is_match {
continue;
}
let content_match = if_count_ok(
children_count_in_range,
is_content_match(
sub,
&args.files,
&args.not_files,
&args.dirs,
&args.not_dirs,
),
);
match content_match {
IsMatch::Include => {
let found = parent.to_path_buf();
if args.nested == StopOnMatch {
debug!(
"found a match based on child name: {}, not recursing deeper",
sub.to_str().unwrap()
);
return Ok(smallvec![found]);
}
debug!(
"found a match based on child name: {}, searching deeper",
sub.to_str().unwrap()
);
current_is_match = true;
results.push(found)
}
IsMatch::Exclude => return Ok(smallvec![]),
IsMatch::NoMatch => {}
}
}
let has_positive_pattern =
!args.itself.is_empty() || !args.files.is_empty() || !args.dirs.is_empty();
if args.child_count_range.is_provided()
&& children_count_in_range
&& !has_positive_pattern
&& !current_is_match
{
debug!("selecting {} based on range {} because there were no positive patterns, and negative ones did not match",
parent.to_str().unwrap(), args.child_count_range);
results.push(parent.to_path_buf())
}
for sub in content {
if !sub.is_dir() {
continue;
}
let found = find_matching_dirs(&sub, args, depth_remaining - 1)?;
results.extend(found);
}
Ok(results)
}
fn if_count_ok(count_ok: bool, is_match: IsMatch) -> IsMatch {
if !count_ok && matches!(is_match, IsMatch::Include) {
return IsMatch::Exclude;
}
is_match
}
fn dir_listing(parent: &Path, on_err: OnErr) -> Result<Dirs, String> {
let content = read_dir_err_handling(parent, on_err)?;
let mut subdirs = smallvec![];
for entry in content {
subdirs.push(entry.path().to_path_buf())
}
Ok(subdirs)
}
fn read_dir_err_handling(dir: &Path, on_err: OnErr) -> Result<SmallVec<[DirEntry; 2]>, String> {
match fs::read_dir(dir) {
Ok(res) => {
let mut entries = smallvec![];
for entry in res {
match entry {
Ok(entry) => entries.push(entry),
Err(err) => match on_err {
OnErr::Ignore => {}
OnErr::Warn => eprintln!("failed to read an entry in '{}', err {}; continuing (use -x=a to abort)", dir.to_str().unwrap(), err),
OnErr::Abort => eprintln!("failed to read an entry in '{}', err {}; stopping", dir.to_str().unwrap(), err),
}
}
}
Ok(entries)
}
Err(err) => match on_err {
OnErr::Ignore => Ok(smallvec![]),
OnErr::Warn => {
eprintln!(
"failed to scan directory '{}', err {}; continuing (use -x=a to abort)",
dir.to_str().unwrap(),
err
);
Ok(smallvec![])
}
OnErr::Abort => Err(format!(
"failed to scan directory '{}', err {}; stopping",
dir.to_str().unwrap(),
err
)),
},
}
}
fn is_parent_match(
dir: &Path,
positive_patterns: &[Regex],
negative_patterns: &Vec<Regex>,
) -> IsMatch {
if positive_patterns.is_empty() && negative_patterns.is_empty() {
return IsMatch::NoMatch;
}
if let Some(dir_name) = dir.file_name() {
let dir_name = dir_name.to_str().unwrap();
for re in negative_patterns {
if re.is_match(dir_name) {
return IsMatch::Exclude;
}
}
for re in positive_patterns {
if re.is_match(dir_name) {
debug!("parent match: '{}' matches '{}'", dir_name, re);
return IsMatch::Include;
}
}
}
IsMatch::NoMatch
}
fn is_content_match(
item: &Path,
positive_file_pattern: &Vec<Regex>,
negative_file_pattern: &Vec<Regex>,
positive_dir_pattern: &Vec<Regex>,
negative_dir_pattern: &Vec<Regex>,
) -> IsMatch {
if positive_file_pattern.is_empty()
&& negative_file_pattern.is_empty()
&& positive_dir_pattern.is_empty()
&& negative_dir_pattern.is_empty()
{
return IsMatch::NoMatch;
}
if let Some(item_name) = item.file_name() {
let item_name = item_name.to_str().unwrap();
for re in positive_file_pattern {
if re.is_match(item_name) && item.is_file() {
debug!("match: '{}' matches '{}'", item_name, re);
return IsMatch::Include;
}
}
for re in negative_file_pattern {
if re.is_match(item_name) && item.is_file() {
debug!("negative match: '{}' matches '{}'", item_name, re);
return IsMatch::Exclude;
}
}
for re in positive_dir_pattern {
if re.is_match(item_name) && item.is_dir() {
debug!("match: '{}' matches '{}'", item_name, re);
return IsMatch::Include;
}
}
for re in negative_dir_pattern {
if re.is_match(item_name) && item.is_dir() {
debug!("negative match: '{}' matches '{}'", item_name, re);
return IsMatch::Exclude;
}
}
}
IsMatch::NoMatch
}