#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::single_char_lifetime_names)]
#![allow(clippy::single_call_fn)]
#![allow(clippy::question_mark_used)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::fn_params_excessive_bools)]
#![allow(clippy::struct_excessive_bools)]
#![allow(clippy::multiple_crate_versions)]
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL_ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[cfg(target_env = "msvc")]
#[global_allocator]
static GLOBAL_ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use fnmatch_regex2::glob_to_regex;
use ignore::{DirEntry, WalkBuilder, WalkState};
use regex::{bytes::Regex, bytes::RegexBuilder};
pub use std::ffi::OsString;
use std::path::PathBuf;
use std::process::exit as process_exit;
pub use std::sync::mpsc::{channel as unbounded, Receiver,Sender};
pub type BoxBytes = Box<[u8]>;
use std::collections::HashSet;
use std::sync::OnceLock;
mod process_entries;
use process_entries::{process_entry_fullpath, process_entry_shortpath};
pub use process_entries::{FileNameBytes,AsBytes};
mod config;
mod constants;
pub use config::SearchConfig;
mod error;
use constants::{AVOID, START_PREFIX};
pub(crate) use constants::{DEPTH_CHECK, DOT_PATTERN};
pub use error::ScanError;
static AVOID_PATHS: OnceLock<HashSet<PathBuf>> = OnceLock::new();
#[allow(clippy::inline_always)]
#[inline(always)]
fn avoid_sys_paths(path_entry: &DirEntry) -> bool {
if path_entry.depth() > DEPTH_CHECK {
return true;
}
let paths = AVOID_PATHS.get_or_init(|| AVOID.iter().map(PathBuf::from).collect::<HashSet<_>>());
!paths.contains(path_entry.path())
}
#[allow(clippy::missing_errors_doc)]
#[must_use = "builds regex but modifies errors to map to custom error type"]
fn build_regex(pattern: &str, case_sensitive: bool) -> Result<Regex, ScanError> {
RegexBuilder::new(pattern)
.case_insensitive(case_sensitive)
.build()
.map_err(ScanError::Regex)
}
#[must_use]
fn process_glob_regex(glob_pattern: &str) -> String {
glob_to_regex(glob_pattern).map_or_else(
|_| {
eprintln!("This can't be processed as a glob pattern");
process_exit(1)
},
|good_pattern| good_pattern.as_str().into(),
)
}
#[inline]
pub fn find_files_iter(search_config: &SearchConfig) -> Result<Receiver<BoxBytes>, ScanError> {
let (tx, rx) = unbounded::<BoxBytes>();
let pattern_to_use = if search_config.use_glob {
process_glob_regex(&search_config.pattern)
} else {
search_config.pattern.clone()
};
let re: Option<Regex> = if search_config.pattern == DOT_PATTERN {
None
} else {
Some(build_regex(&pattern_to_use, search_config.case_sensitive)?)
};
let conditional_check: bool =
search_config.root != START_PREFIX || search_config.keep_sys_paths;
let process_entry = if search_config.use_glob || search_config.full_path {
process_entry_fullpath
} else {
process_entry_shortpath
};
WalkBuilder::new(&search_config.root)
.hidden(!search_config.hide_hidden)
.filter_entry(move |entry| conditional_check || avoid_sys_paths(entry))
.git_global(false)
.git_ignore(false)
.git_exclude(false)
.ignore(false)
.max_depth(search_config.max_depth)
.threads(search_config.thread_count)
.build_parallel()
.run(|| {
Box::new(|entry| {
entry.map_or(WalkState::Continue, |entry_path| {
if !search_config.keep_dirs
&& entry_path
.file_type()
.is_some_and(|filetype| filetype.is_dir())
{
return WalkState::Continue;
}
process_entry(&entry_path, re.as_ref(), &tx)
})
})
});
Ok(rx)
}
#[inline]
pub fn find_files(
pattern: &str,
root: &str,
hide_hidden: bool,
case_sensitive: bool,
thread_count: usize,
keep_dirs: bool,
keep_sys_paths: bool,
max_depth: Option<usize>,
use_glob: bool,
full_path: bool,
) -> Result<Vec<OsString>, ScanError> {
let search_config = SearchConfig {
pattern: pattern.to_string(),
root: root.into(),
hide_hidden,
case_sensitive,
thread_count,
keep_dirs,
keep_sys_paths,
max_depth,
use_glob,
full_path,
};
Ok(find_files_iter(&search_config)?
.iter()
.map(|arc_str| unsafe { OsString::from_encoded_bytes_unchecked(arc_str.into()) })
.collect::<Vec<OsString>>())
}