use crate::cli::file::FileKind;
use crate::cli::recent::RecentKind;
use crate::config::Config;
use crate::error::{MyError, MyResult};
use crate::fs::entry::Entry;
use crate::fs::file::File;
use crate::fs::flags::FileFlags;
use crate::fs::system::System;
use crate::fs::total::Total;
use crate::git::cache::GitCache;
use crate::regex;
use crate::zip::wrapper::ZipKind;
use chrono::{DateTime, TimeZone, Utc};
use glob::{MatchOptions, Pattern};
use multimap::MultiMap;
use path_clean::PathClean;
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
#[cfg(windows)]
use std::path::MAIN_SEPARATOR_STR;
use std::path::{Component, Path, PathBuf};
use std::rc::Rc;
use std::time::SystemTime;
#[allow(dead_code)]
pub struct Finder<'a, S: System> {
config: &'a Config,
system: &'a S,
current: PathBuf,
options: MatchOptions,
start_time: RecentKind<DateTime<Utc>>,
git_cache: Option<Rc<GitCache>>,
git_bash: bool,
}
impl<'a, S: System> Finder<'a, S> {
pub fn new<Tz: TimeZone>(
config: &'a Config,
system: &'a S,
zone: &Tz,
current: PathBuf,
git_bash: bool,
) -> Self {
let options = Self::match_options(config);
let start_time = config.start_time(zone);
let git_cache = config.filter_git().map(GitCache::new).map(Rc::new);
Self {
config,
system,
current,
options,
start_time,
git_cache,
git_bash,
}
}
#[cfg(windows)]
fn match_options(config: &Config) -> MatchOptions {
let mut options = MatchOptions::new();
options.case_sensitive = config.case_sensitive().unwrap_or(false);
options
}
#[cfg(not(windows))]
fn match_options(config: &Config) -> MatchOptions {
let mut options = MatchOptions::new();
options.case_sensitive = config.case_sensitive().unwrap_or(true);
options
}
pub fn find_files(&self) -> MyResult<Vec<File>> {
let files = RefCell::new(BTreeSet::new());
let tasks = self.group_tasks()?;
for ((abs_root, rel_root), patterns) in tasks.iter_all() {
self.find_entries(&files, abs_root, rel_root, patterns)?;
self.find_parents(&files, abs_root, rel_root)?;
}
let files = files.into_inner().into_iter().collect();
Ok(files)
}
pub fn create_total(&self, files: &Vec<File>) -> Total {
Total::from_files(self.start_time, self.config, files)
}
fn group_tasks(&self) -> MyResult<MultiMap<(PathBuf, PathBuf), Pattern>> {
let mut tasks = MultiMap::new();
for pattern in self.config.patterns() {
if let Some((abs_root, rel_root, filename)) = self.parse_pattern(pattern) {
let pattern = Pattern::new(&filename).map_err(|e| (e, &filename))?;
tasks.insert((abs_root, rel_root), pattern);
}
}
Ok(tasks)
}
#[cfg(windows)]
fn parse_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
if self.git_bash {
let drive_regex = regex!(r#"^/([A-Za-z])/(.+)$"#);
if let Some(captures) = drive_regex.captures(pattern) {
let drive = captures.get(1).unwrap().as_str().to_uppercase();
let path = captures.get(2).unwrap().as_str().replace("/", MAIN_SEPARATOR_STR);
let pattern = format!("{}:{}{}", drive, MAIN_SEPARATOR_STR, path);
self.split_pattern(&pattern)
} else {
let pattern = pattern.replace("/", MAIN_SEPARATOR_STR);
self.split_pattern(&pattern)
}
} else {
self.split_pattern(pattern)
}
}
#[cfg(not(windows))]
fn parse_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
self.split_pattern(pattern)
}
fn split_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
let rel_root = PathBuf::from(pattern);
let abs_root = self.current.join(&rel_root).clean();
if requires_wildcard(&rel_root, self.config.zip_expand()) {
let name = String::from("*");
return Some((abs_root, rel_root, name));
}
if let Some(mut name) = find_name(&abs_root) {
if let Some(abs_root) = find_parent(&abs_root) {
if let Some(rel_root) = find_parent(&rel_root) {
if name.starts_with(".") {
name = format!("*{name}");
}
return Some((abs_root, rel_root, name));
}
}
}
None
}
fn find_entries(
&self,
files: &RefCell<BTreeSet<File>>,
abs_root: &Path,
rel_root: &Path,
patterns: &Vec<Pattern>,
) -> MyResult<()> {
let rel_depth = count_components(rel_root);
let git_cache = self.git_cache.as_ref().map(Rc::clone);
self.system.walk_entries(abs_root, rel_root, git_cache, &|entry| {
match entry {
Ok(entry) => {
self.insert_file(
files,
entry,
abs_root,
rel_root,
rel_depth,
patterns,
);
}
Err(error) => if !self.config.quiet() {
error.print_error();
}
}
})
}
fn insert_file(
&self,
files: &RefCell<BTreeSet<File>>,
entry: &dyn Entry,
abs_root: &Path,
rel_root: &Path,
rel_depth: usize,
patterns: &Vec<Pattern>,
) {
match self.create_file(entry, abs_root, rel_root, rel_depth, patterns) {
Ok(file) => if let Some(file) = file {
if !self.config.sort_name() || self.config.show_indent() || (file.file_type != FileKind::Dir) {
files.borrow_mut().insert(file);
}
}
Err(error) => if !self.config.quiet() {
error.print_error();
}
}
}
fn create_file(
&self,
entry: &dyn Entry,
abs_root: &Path,
rel_root: &Path,
rel_depth: usize,
patterns: &Vec<Pattern>,
) -> MyResult<Option<File>> {
if let Some(name) = entry.file_name().to_str() {
if !patterns.iter().any(|p| p.matches_with(name, self.options)) {
return Ok(None);
}
if let Some(depth) = self.config.min_depth() {
if entry.file_depth() < depth {
return Ok(None);
}
}
let file_type = FileKind::from_entry(self.system, entry);
if let Some(filter_types) = self.config.filter_types() {
if !filter_types.contains(&file_type) {
return Ok(None);
}
}
if let FileKind::Link(_) = file_type {
let link_path = self.system.read_link(entry)?;
if let Some(link_path) = link_path {
return if let Some(link_entry) = self.follow_link(entry.file_path(), &link_path) {
entry.copy_metadata(link_entry.as_ref().as_ref());
let link_path = link_entry.file_path().to_path_buf();
let link_type = FileKind::from_entry(self.system, link_entry.as_ref().as_ref());
self.create_inner(
entry,
abs_root,
rel_root,
rel_depth,
file_type,
Some((link_path, link_type)),
)
} else {
entry.reset_metadata();
self.create_inner(
entry,
abs_root,
rel_root,
rel_depth,
FileKind::Link(false),
Some((link_path, FileKind::Link(false))),
)
}
}
}
return self.create_inner(
entry,
abs_root,
rel_root,
rel_depth,
file_type,
None,
);
}
Ok(None)
}
fn follow_link(&self, file_path: &Path, link_path: &Path) -> Option<Rc<Box<dyn Entry>>> {
if let Some(file_parent) = file_path.parent() {
let link_path = file_parent.join(link_path);
return self.system.get_entry(&link_path).ok();
}
None
}
fn create_inner(
&self,
entry: &dyn Entry,
abs_root: &Path,
rel_root: &Path,
rel_depth: usize,
file_type: FileKind,
link_data: Option<(PathBuf, FileKind)>,
) -> MyResult<Option<File>> {
let file_time = DateTime::<Utc>::from(entry.file_time());
match self.start_time {
RecentKind::Before(time) if file_time > time => return Ok(None),
RecentKind::After(time) if file_time <= time => return Ok(None),
_ => (),
}
let abs_path = entry.file_path();
let git_flags = if let Some(git_cache) = &self.git_cache {
if entry.file_flags() != FileFlags::File {
return Ok(None);
}
let flags = git_cache.test_allowed(abs_path)?;
if flags.is_none() {
return Ok(None);
}
flags
} else {
None
};
if let Some(rel_path) = create_relative(abs_root, rel_root, abs_path) {
if let Some(abs_dir) = select_parent(abs_path, file_type) {
if let Some(rel_dir) = select_parent_from_owned(rel_path, file_type) {
let file_depth = entry.file_depth() + rel_depth;
let inner_depth = entry.inner_depth();
let file_name = select_name(abs_path, file_type).unwrap_or_default();
let file_ext = find_extension(abs_path, file_type);
let file_size = select_size(entry, &link_data, file_type);
let mut file = File::new(abs_dir, rel_dir, file_depth, inner_depth, file_name, file_ext, file_type)
.with_mode(entry.file_mode())
.with_size(file_size)
.with_time(file_time)
.with_git(git_flags);
#[cfg(unix)]
if self.config.show_owner() {
let user = self.system.find_user(entry.owner_uid());
let group = self.system.find_group(entry.owner_gid());
file = file.with_owner(user, group);
}
if self.config.show_crc() {
if let FileKind::File(_) | FileKind::Link(_) = file_type {
let file_crc = self.system.read_crc(entry);
file = file.with_crc(file_crc);
}
}
if self.config.show_sig() {
if let FileKind::File(_) | FileKind::Link(_) = file_type {
let file_sig = self.system.read_sig(entry);
file = file.with_sig(file_sig);
}
}
#[cfg(windows)]
if self.config.win_ver() {
if let Some(file_ver) = self.system.read_version(entry) {
file = file.with_version(file_ver);
}
}
if let Some((link_path, link_type)) = link_data {
file = file.with_link(link_path, link_type);
}
return Ok(Some(file));
}
}
}
Ok(None)
}
fn find_parents(
&self,
files: &RefCell<BTreeSet<File>>,
abs_root: &Path,
rel_root: &Path,
) -> MyResult<()> {
if self.config.show_indent() {
let parents = find_parents(files)?;
for (abs_path, file_depth) in parents.into_iter() {
self.insert_parent(files, abs_root, rel_root, abs_path, file_depth);
}
}
Ok(())
}
#[allow(unused_mut)]
fn insert_parent(
&self,
files: &RefCell<BTreeSet<File>>,
abs_root: &Path,
rel_root: &Path,
abs_path: PathBuf,
file_depth: usize,
) {
if let Some(rel_path) = create_relative(abs_root, rel_root, &abs_path) {
if let Some(abs_dir) = select_parent(&abs_path, FileKind::Dir) {
if let Some(rel_dir) = select_parent_from_owned(rel_path, FileKind::Dir) {
let sys_entry = self.system.get_entry(&abs_path).ok();
let inner_depth = sys_entry.as_ref().and_then(|e| e.inner_depth());
let file_mode = sys_entry.as_ref().map(|e| e.file_mode()).unwrap_or_default();
let file_time = sys_entry.as_ref().map(|e| e.file_time()).unwrap_or(SystemTime::UNIX_EPOCH);
let file_name = String::from("");
let file_ext = String::from("");
let mut file = File::new(abs_dir, rel_dir, file_depth, inner_depth, file_name, file_ext, FileKind::Dir)
.with_mode(file_mode)
.with_time(DateTime::<Utc>::from(file_time));
#[cfg(unix)]
if self.config.show_owner() {
let uid = sys_entry.as_ref().map(|e| e.owner_uid()).unwrap_or_default();
let gid = sys_entry.as_ref().map(|e| e.owner_gid()).unwrap_or_default();
let user = self.system.find_user(uid);
let group = self.system.find_group(gid);
file = file.with_owner(user, group);
}
files.borrow_mut().insert(file);
}
}
}
}
}
fn requires_wildcard(root: &Path, zip_expand: bool) -> bool {
let wildcard_regex = regex!(r"(^\.+$|[\\/]\.*$)");
if let Some(root) = root.to_str() {
if wildcard_regex.is_match(root) {
return true;
}
}
ZipKind::from_path(root, zip_expand).is_some()
}
pub fn count_components(path: &Path) -> usize {
path
.components()
.filter(|c| matches!(c, Component::Normal(_)))
.count()
}
fn find_parents(files: &RefCell<BTreeSet<File>>) -> MyResult<BTreeMap<PathBuf, usize>> {
let mut parents = BTreeMap::new();
for file in files.borrow().iter() {
let file_depth = file.file_depth + file.file_type.dir_offset();
find_ancestors(&mut parents, &file.abs_dir, file_depth)?;
}
Ok(parents)
}
fn find_ancestors(
parents: &mut BTreeMap<PathBuf, usize>,
abs_path: &Path,
file_depth: usize,
) -> MyResult<()> {
if let Some(file_depth) = file_depth.checked_sub(1) {
if file_depth > 0 {
if let Some(old_depth) = parents.insert(PathBuf::from(abs_path), file_depth) {
if old_depth != file_depth {
let error = format!("Inconsistent depth: {}", abs_path.display());
return Err(MyError::Text(error));
}
} else {
if let Some(abs_path) = abs_path.parent() {
find_ancestors(parents, abs_path, file_depth)?;
}
}
}
}
Ok(())
}
fn create_relative(
abs_root: &Path,
rel_root: &Path,
abs_path: &Path,
) -> Option<PathBuf> {
let mut abs_root = PathBuf::from(abs_root);
let mut rel_path = PathBuf::new();
loop {
if let Ok(path) = abs_path.strip_prefix(&abs_root) {
rel_path.push(path);
return Some(rel_root.join(rel_path).clean());
}
if !abs_root.pop() {
return None;
}
rel_path.push("..");
}
}
fn select_parent_from_owned(path: PathBuf, file_type: FileKind) -> Option<PathBuf> {
if file_type == FileKind::Dir {
Some(path)
} else {
find_parent(&path)
}
}
fn select_parent(path: &Path, file_type: FileKind) -> Option<PathBuf> {
if file_type == FileKind::Dir {
Some(PathBuf::from(path))
} else {
find_parent(path)
}
}
fn select_name(path: &Path, file_type: FileKind) -> Option<String> {
if file_type == FileKind::Dir {
Some(String::from(""))
} else {
find_name(path)
}
}
fn find_parent(path: &Path) -> Option<PathBuf> {
path.parent().map(PathBuf::from)
}
fn find_name(path: &Path) -> Option<String> {
path.file_name().and_then(OsStr::to_str).map(String::from)
}
fn find_extension(path: &Path, file_type: FileKind) -> String {
match file_type {
FileKind::File(_) | FileKind::Link(_) => path.extension()
.and_then(OsStr::to_str)
.map(str::to_ascii_lowercase)
.map(|ext| format!(".{ext}"))
.unwrap_or_default(),
_ => String::default(),
}
}
fn select_size(
entry: &dyn Entry,
link_data: &Option<(PathBuf, FileKind)>,
file_type: FileKind,
) -> u64 {
if file_type == FileKind::Dir {
return 0;
}
if let Some((_, link_type)) = link_data {
if *link_type == FileKind::Dir {
return 0;
}
}
entry.file_size()
}
#[cfg(test)]
mod tests {
use crate::cli::file::{ExecKind, FileKind};
use crate::cli::recent::{PeriodKind, RecentKind};
use crate::config::Config;
use crate::finder::{create_relative, requires_wildcard, Finder};
use crate::fs::file::File;
use crate::fs::system::tests::MockSystem;
use crate::util::matcher::tests::*;
use chrono::Utc;
use googletest::prelude::*;
use std::path::PathBuf;
#[gtest]
fn test_dir_requires_wildcard() {
expect_true!(test_wildcard(".", false));
expect_true!(test_wildcard("..", false));
expect_true!(test_wildcard("/", false));
expect_true!(test_wildcard("/path/to/dir/", false));
expect_true!(test_wildcard("/path/to/dir/.", false));
expect_true!(test_wildcard("/path/to/dir/..", false));
expect_true!(test_wildcard(r"\", false));
expect_true!(test_wildcard(r"\path\to\dir\", false));
expect_true!(test_wildcard(r"\path\to\dir\.", false));
expect_true!(test_wildcard(r"\path\to\dir\..", false));
}
#[gtest]
fn test_file_requires_wildcard() {
expect_false!(test_wildcard("lower", false));
expect_false!(test_wildcard("lower.zip", false));
expect_false!(test_wildcard("lower.7z", false));
expect_false!(test_wildcard("lower.tar", false));
expect_false!(test_wildcard("UPPER", false));
expect_false!(test_wildcard("UPPER.ZIP", false));
expect_false!(test_wildcard("UPPER.7Z", false));
expect_false!(test_wildcard("UPPER.TAR", false));
expect_false!(test_wildcard("/path/to/dir/lower", false));
expect_false!(test_wildcard("/path/to/dir/lower.zip", false));
expect_false!(test_wildcard("/path/to/dir/lower.7z", false));
expect_false!(test_wildcard("/path/to/dir/lower.tar", false));
expect_false!(test_wildcard("/path/to/dir/UPPER", false));
expect_false!(test_wildcard("/path/to/dir/UPPER.ZIP", false));
expect_false!(test_wildcard("/path/to/dir/UPPER.7Z", false));
expect_false!(test_wildcard("/path/to/dir/UPPER.TAR", false));
expect_false!(test_wildcard(r"\path\to\dir\lower", false));
expect_false!(test_wildcard(r"\path\to\dir\lower.zip", false));
expect_false!(test_wildcard(r"\path\to\dir\lower.7z", false));
expect_false!(test_wildcard(r"\path\to\dir\lower.tar", false));
expect_false!(test_wildcard(r"\path\to\dir\UPPER", false));
expect_false!(test_wildcard(r"\path\to\dir\UPPER.ZIP", false));
expect_false!(test_wildcard(r"\path\to\dir\UPPER.7Z", false));
expect_false!(test_wildcard(r"\path\to\dir\UPPER.TAR", false));
}
#[gtest]
fn test_archive_requires_wildcard() {
expect_false!(test_wildcard("lower", true));
expect_true!(test_wildcard("lower.zip", true));
expect_true!(test_wildcard("lower.7z", true));
expect_true!(test_wildcard("lower.tar", true));
expect_false!(test_wildcard("UPPER", true));
expect_true!(test_wildcard("UPPER.ZIP", true));
expect_true!(test_wildcard("UPPER.7Z", true));
expect_true!(test_wildcard("UPPER.TAR", true));
expect_false!(test_wildcard("/path/to/dir/lower", true));
expect_true!(test_wildcard("/path/to/dir/lower.zip", true));
expect_true!(test_wildcard("/path/to/dir/lower.7z", true));
expect_true!(test_wildcard("/path/to/dir/lower.tar", true));
expect_false!(test_wildcard("/path/to/dir/UPPER", true));
expect_true!(test_wildcard("/path/to/dir/UPPER.ZIP", true));
expect_true!(test_wildcard("/path/to/dir/UPPER.7Z", true));
expect_true!(test_wildcard("/path/to/dir/UPPER.TAR", true));
expect_false!(test_wildcard(r"\path\to\dir\lower", true));
expect_true!(test_wildcard(r"\path\to\dir\lower.zip", true));
expect_true!(test_wildcard(r"\path\to\dir\lower.7z", true));
expect_true!(test_wildcard(r"\path\to\dir\lower.tar", true));
expect_false!(test_wildcard(r"\path\to\dir\UPPER", true));
expect_true!(test_wildcard(r"\path\to\dir\UPPER.ZIP", true));
expect_true!(test_wildcard(r"\path\to\dir\UPPER.7Z", true));
expect_true!(test_wildcard(r"\path\to\dir\UPPER.TAR", true));
}
fn test_wildcard(root: &str, zip_expand: bool) -> bool {
let root = PathBuf::from(root);
requires_wildcard(&root, zip_expand)
}
#[gtest]
#[cfg(all(windows, any()))]
fn test_counts_components() {
use crate::finder::count_components;
expect_eq!(count_components(&PathBuf::from(r"")), 0);
expect_eq!(count_components(&PathBuf::from(r"..")), 0);
expect_eq!(count_components(&PathBuf::from(r"..\dir")), 1);
expect_eq!(count_components(&PathBuf::from(r"..\dir\subdir")), 2);
expect_eq!(count_components(&PathBuf::from(r".")), 0);
expect_eq!(count_components(&PathBuf::from(r".\dir")), 1);
expect_eq!(count_components(&PathBuf::from(r".\dir\subdir")), 2);
expect_eq!(count_components(&PathBuf::from(r"dir")), 1);
expect_eq!(count_components(&PathBuf::from(r"dir\subdir")), 2);
expect_eq!(count_components(&PathBuf::from(r"\dir")), 1);
expect_eq!(count_components(&PathBuf::from(r"\dir\subdir")), 2);
expect_eq!(count_components(&PathBuf::from(r"D:\dir")), 1);
expect_eq!(count_components(&PathBuf::from(r"D:\dir\subdir")), 2);
expect_eq!(count_components(&PathBuf::from(r"\\unc\dir")), 1);
expect_eq!(count_components(&PathBuf::from(r"\\unc\dir\subdir")), 2);
}
#[gtest]
#[cfg(not(windows))]
fn test_counts_components() {
use crate::finder::count_components;
expect_eq!(count_components(&PathBuf::from("")), 0);
expect_eq!(count_components(&PathBuf::from("..")), 0);
expect_eq!(count_components(&PathBuf::from("../dir")), 1);
expect_eq!(count_components(&PathBuf::from("../dir/subdir")), 2);
expect_eq!(count_components(&PathBuf::from(".")), 0);
expect_eq!(count_components(&PathBuf::from("./dir")), 1);
expect_eq!(count_components(&PathBuf::from("./dir/subdir")), 2);
expect_eq!(count_components(&PathBuf::from("dir")), 1);
expect_eq!(count_components(&PathBuf::from("dir/subdir")), 2);
expect_eq!(count_components(&PathBuf::from("/dir")), 1);
expect_eq!(count_components(&PathBuf::from("/dir/subdir")), 2);
}
#[gtest]
fn test_creates_relative_paths() {
expect_that!(test_relative("/root"), some(eq("..")));
expect_that!(test_relative("/root/dir"), some(eq("../dir")));
expect_that!(test_relative("/root/dir/subdir"), some(eq("../dir/subdir")));
expect_that!(test_relative("/root/dir2"), some(eq("../dir2")));
expect_that!(test_relative("/root/dir2/subdir"), some(eq("../dir2/subdir")));
expect_that!(test_relative("/"), some(eq("../..")));
expect_that!(test_relative("/root2/dir"), some(eq("../../root2/dir")));
expect_that!(test_relative("/root2/dir/subdir"), some(eq("../../root2/dir/subdir")));
}
fn test_relative(abs_path: &str) -> Option<PathBuf> {
let abs_root = PathBuf::from("/root/dir");
let rel_root = PathBuf::from("../dir");
let abs_path = PathBuf::from(abs_path);
create_relative(&abs_root, &rel_root, &abs_path)
}
#[gtest]
fn test_parses_file_attributes_in_root_directory() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
expect_that!(files, elements_are![
pat!(File {
abs_dir: eq("/root"),
rel_dir: eq(""),
file_name: eq("archive.sh"),
file_ext: eq(".sh"),
file_depth: eq(&1),
file_type: eq(&FileKind::File(ExecKind::User)),
file_mode: eq(&0o744),
file_size: eq(&100),
file_time: time_eq(2023, 1, 1, 0, 0, 0),
link_data: none(),
..
}),
pat!(File {
abs_dir: eq("/root/dir"),
rel_dir: eq("dir"),
file_name: eq(""),
file_ext: eq(""),
file_depth: eq(&1),
file_type: eq(&FileKind::Dir),
file_mode: eq(&0o755),
file_size: eq(&0),
file_time: time_eq(2023, 2, 2, 0, 0, 0),
link_data: none(),
..
}),
pat!(File {
abs_dir: eq("/root/dir"),
rel_dir: eq("dir"),
file_name: eq("link1"),
file_ext: eq(""),
file_depth: eq(&2),
file_type: eq(&FileKind::Link(true)),
file_mode: eq(&0o755),
file_size: eq(&0),
file_time: time_eq(2023, 3, 3, 0, 0, 0),
link_data: some((eq("/root/dir/subdir"), eq(&FileKind::Dir))),
..
}),
pat!(File {
abs_dir: eq("/root/dir"),
rel_dir: eq("dir"),
file_name: eq("link2"),
file_ext: eq(""),
file_depth: eq(&2),
file_type: eq(&FileKind::Link(true)),
file_mode: eq(&0o644),
file_size: eq(&500),
file_time: time_eq(2023, 5, 5, 0, 0, 0),
link_data: some((eq("/root/dir/subdir/alpha.txt"), eq(&FileKind::File(ExecKind::None)))),
..
}),
pat!(File {
abs_dir: eq("/root/dir"),
rel_dir: eq("dir"),
file_name: eq("link3"),
file_ext: eq(""),
file_depth: eq(&2),
file_type: eq(&FileKind::Link(false)),
file_mode: eq(&0o644),
file_size: eq(&0),
file_time: time_eq(1970, 1, 1, 0, 0, 0),
link_data: some((eq("/etc/missing.txt"), eq(&FileKind::Link(false)))),
..
}),
pat!(File {
abs_dir: eq("/root/dir/subdir"),
rel_dir: eq("dir/subdir"),
file_name: eq(""),
file_ext: eq(""),
file_depth: eq(&2),
file_type: eq(&FileKind::Dir),
file_mode: eq(&0o755),
file_size: eq(&0),
file_time: time_eq(2023, 3, 3, 0, 0, 0),
link_data: none(),
..
}),
pat!(File {
abs_dir: eq("/root/dir/subdir"),
rel_dir: eq("dir/subdir"),
file_name: eq("alpha.csv"),
file_ext: eq(".csv"),
file_depth: eq(&3),
file_type: eq(&FileKind::File(ExecKind::None)),
file_mode: eq(&0o644),
file_size: eq(&400),
file_time: time_eq(2023, 4, 4, 0, 0, 0),
link_data: none(),
..
}),
pat!(File {
abs_dir: eq("/root/dir/subdir"),
rel_dir: eq("dir/subdir"),
file_name: eq("alpha.txt"),
file_ext: eq(".txt"),
file_depth: eq(&3),
file_type: eq(&FileKind::File(ExecKind::None)),
file_mode: eq(&0o644),
file_size: eq(&500),
file_time: time_eq(2023, 5, 5, 0, 0, 0),
link_data: none(),
..
}),
pat!(File {
abs_dir: eq("/root/dir/subdir"),
rel_dir: eq("dir/subdir"),
file_name: eq("beta.csv"),
file_ext: eq(".csv"),
file_depth: eq(&3),
file_type: eq(&FileKind::File(ExecKind::None)),
file_mode: eq(&0o644),
file_size: eq(&600),
file_time: time_eq(2023, 6, 6, 0, 0, 0),
link_data: none(),
..
}),
pat!(File {
abs_dir: eq("/root/dir/subdir"),
rel_dir: eq("dir/subdir"),
file_name: eq("beta.txt"),
file_ext: eq(".txt"),
file_depth: eq(&3),
file_type: eq(&FileKind::File(ExecKind::None)),
file_mode: eq(&0o644),
file_size: eq(&700),
file_time: time_eq(2023, 7, 7, 0, 0, 0),
link_data: none(),
..
}),
]);
}
#[gtest]
fn test_finds_multiple_patterns_in_same_directory() {
let config = Config::default()
.with_patterns(vec!["dir/subdir/alpha.*", "dir/subdir/*.txt"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_finds_multiple_patterns_in_diff_directories() {
let config = Config::default()
.with_patterns(vec!["dir/alpha.*", "dir/subdir/*.txt"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_finds_files_if_recurse_no_indent_in_root_directory() {
let config = Config::default()
.with_patterns(vec!["*.txt"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_finds_parents_if_recurse_with_indent_in_root_directory() {
let config = Config::default()
.with_patterns(vec!["*.txt"])
.with_recurse_all(true)
.with_show_indent(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_finds_parents_if_recurse_with_indent_in_branch_directory() {
let config = Config::default()
.with_patterns(vec!["dir/*.txt"])
.with_recurse_all(true)
.with_show_indent(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_finds_parents_if_recurse_with_indent_in_leaf_directory() {
let config = Config::default()
.with_patterns(vec!["dir/subdir/*.txt"])
.with_recurse_all(true)
.with_show_indent(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_hides_directories_if_order_by_name() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true)
.with_sort_name(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/archive.sh",
"/root/dir/link1",
"/root/dir/link2",
"/root/dir/link3",
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_finds_files_with_bare_filename() {
let config = Config::default()
.with_patterns(vec!["beta.csv"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/subdir/beta.csv",
]);
}
#[gtest]
fn test_finds_files_with_bare_extension() {
let config = Config::default()
.with_patterns(vec![".csv"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/beta.csv",
]);
}
#[gtest]
fn test_filters_files_by_minimum_depth() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_min_depth(2);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/link1",
"/root/dir/link2",
"/root/dir/link3",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_filters_files_by_maximum_depth() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_max_depth(2);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/archive.sh",
"/root/dir/",
"/root/dir/link1",
"/root/dir/link2",
"/root/dir/link3",
"/root/dir/subdir/",
]);
}
#[gtest]
fn test_filters_files_by_file_type() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true)
.with_filter_types(vec![
FileKind::File(ExecKind::None),
FileKind::File(ExecKind::User),
FileKind::File(ExecKind::Other),
]);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/archive.sh",
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_filters_files_before_recent_time() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true)
.with_curr_time(2024, 1, 1, 0, 0, 0)
.with_filter_recent(RecentKind::Before(PeriodKind::Month(8)));
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/archive.sh",
"/root/dir/",
"/root/dir/link1",
"/root/dir/link3",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.csv",
]);
}
#[gtest]
fn test_filters_files_after_recent_time() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true)
.with_curr_time(2024, 1, 1, 0, 0, 0)
.with_filter_recent(RecentKind::After(PeriodKind::Month(8)));
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/dir/link2",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
]);
}
#[gtest]
fn test_calculates_total_from_files() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let total = finder.create_total(&files);
expect_eq!(total.max_size, 700);
expect_eq!(total.total_size, 2800);
#[cfg(unix)]
expect_eq!(total.user_width, 0);
#[cfg(unix)]
expect_eq!(total.group_width, 0);
#[cfg(windows)]
expect_eq!(total.ver_width, 0);
expect_eq!(total.ext_width, 4);
expect_eq!(total.num_files, 8);
expect_eq!(total.num_dirs, 2);
}
#[gtest]
#[cfg(unix)]
fn test_calculates_total_from_files_with_no_owners() {
let config = Config::default()
.with_show_owner(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let total = finder.create_total(&files);
expect_eq!(total.user_width, 1);
expect_eq!(total.group_width, 1);
}
#[gtest]
#[cfg(unix)]
fn test_calculates_total_from_files_with_some_owners() {
let config = Config::default()
.with_patterns(vec!["*"])
.with_recurse_all(true)
.with_show_owner(true);
let system = create_system(&config, create_entries);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let total = finder.create_total(&files);
expect_eq!(total.user_width, 5);
expect_eq!(total.group_width, 6);
}
fn create_entries(system: &mut MockSystem) {
system.insert_entry(1, 'f', 0o744, 0, 0, 100, 2023, 1, 1, "archive.sh", None);
system.insert_entry(1, 'd', 0o755, 1000, 500, 4096, 2023, 2, 2, "dir", None);
system.insert_entry(2, 'l', 0o644, 1000, 500, 99, 2023, 12, 31, "dir/link1", Some("subdir"));
system.insert_entry(2, 'l', 0o644, 1000, 500, 99, 2023, 12, 31, "dir/link2", Some("subdir/alpha.txt"));
system.insert_entry(2, 'l', 0o644, 1000, 500, 99, 2023, 12, 31, "dir/link3", Some("/etc/missing.txt"));
system.insert_entry(2, 'd', 0o755, 1500, 500, 4096, 2023, 3, 3, "dir/subdir", None);
system.insert_entry(3, 'f', 0o644, 1500, 500, 400, 2023, 4, 4, "dir/subdir/alpha.csv", None);
system.insert_entry(3, 'f', 0o644, 1500, 500, 500, 2023, 5, 5, "dir/subdir/alpha.txt", None);
system.insert_entry(3, 'f', 0o644, 1500, 500, 600, 2023, 6, 6, "dir/subdir/beta.csv", None);
system.insert_entry(3, 'f', 0o644, 1500, 500, 700, 2023, 7, 7, "dir/subdir/beta.txt", None);
}
#[gtest]
fn test_performs_case_sensitive_search() {
let config = Config::default()
.with_patterns(vec!["A*"])
.with_recurse_all(true)
.with_case_sensitive(true);
let system = create_system(&config, create_cases);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/A1.txt",
"/root/A2.txt",
]);
}
#[gtest]
fn test_performs_case_insensitive_search() {
let config = Config::default()
.with_patterns(vec!["A*"])
.with_recurse_all(true)
.with_case_sensitive(false);
let system = create_system(&config, create_cases);
let finder = create_finder(&config, &system);
let files = find_files(&finder);
let paths = convert_paths(files);
expect_that!(paths, elements_are![
"/root/A1.txt",
"/root/A2.txt",
"/root/a1.txt",
"/root/a2.txt",
]);
}
fn create_cases(system: &mut MockSystem) {
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "A1.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "A2.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "B3.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "B4.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "a1.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "a2.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "b3.txt", None);
system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "b4.txt", None);
}
#[cfg(unix)]
fn create_system<F>(config: &Config, mut setter: F) -> MockSystem<'_> where
F: FnMut(&mut MockSystem),
{
use std::collections::BTreeMap;
let current = PathBuf::from("/root");
let user_names = BTreeMap::from([
(0, String::from("root")),
(1000, String::from("alice")),
(1500, String::from("bob")),
]);
let group_names = BTreeMap::from([
(0, String::from("root")),
(500, String::from("public")),
]);
let mut system = MockSystem::new(config, current, user_names, group_names);
setter(&mut system);
system
}
#[cfg(not(unix))]
fn create_system<F>(config: &Config, mut setter: F) -> MockSystem<'_> where
F: FnMut(&mut MockSystem),
{
let current = PathBuf::from("/root");
let mut system = MockSystem::new(config, current);
setter(&mut system);
system
}
fn create_finder<'a>(
config: &'a Config,
system: &'a MockSystem,
) -> Finder<'a, MockSystem<'a>> {
let current = PathBuf::from("/root");
Finder::new(config, system, &Utc, current, false)
}
fn find_files(finder: &Finder<MockSystem>) -> Vec<File> {
let mut files = finder.find_files().unwrap();
files.sort_by_key(File::get_path);
files
}
fn convert_paths(files: Vec<File>) -> Vec<String> {
files.into_iter().flat_map(convert_path).collect()
}
#[cfg(windows)]
fn convert_path(file: File) -> Option<String> {
use std::path::MAIN_SEPARATOR_STR;
let path = file.abs_dir.join(file.file_name);
path.to_str().map(|path| path.replace(MAIN_SEPARATOR_STR, "/"))
}
#[cfg(not(windows))]
fn convert_path(file: File) -> Option<String> {
let path = file.abs_dir.join(file.file_name);
path.to_str().map(str::to_string)
}
}