Skip to main content

ex_cli/
finder.rs

1use crate::cli::file::FileKind;
2use crate::cli::recent::RecentKind;
3use crate::config::Config;
4use crate::error::{MyError, MyResult};
5use crate::fs::entry::Entry;
6use crate::fs::file::File;
7use crate::fs::flags::FileFlags;
8use crate::fs::system::System;
9use crate::fs::total::Total;
10use crate::git::cache::GitCache;
11use crate::regex;
12use crate::zip::wrapper::ZipKind;
13use chrono::{DateTime, TimeZone, Utc};
14use glob::{MatchOptions, Pattern};
15use multimap::MultiMap;
16use path_clean::PathClean;
17use std::cell::RefCell;
18use std::collections::{BTreeMap, BTreeSet};
19use std::ffi::OsStr;
20#[cfg(windows)]
21use std::path::MAIN_SEPARATOR_STR;
22use std::path::{Component, Path, PathBuf};
23use std::rc::Rc;
24use std::time::SystemTime;
25
26#[allow(dead_code)]
27pub struct Finder<'a, S: System> {
28    config: &'a Config,
29    system: &'a S,
30    current: PathBuf,
31    options: MatchOptions,
32    start_time: RecentKind<DateTime<Utc>>,
33    git_cache: Option<Rc<GitCache>>,
34    git_bash: bool,
35}
36
37// noinspection RsLift
38impl<'a, S: System> Finder<'a, S> {
39    pub fn new<Tz: TimeZone>(
40        config: &'a Config,
41        system: &'a S,
42        zone: &Tz,
43        current: PathBuf,
44        git_bash: bool,
45    ) -> Self {
46        let options = Self::match_options(config);
47        let start_time = config.start_time(zone);
48        let git_cache = config.filter_git().map(GitCache::new).map(Rc::new);
49        Self {
50            config,
51            system,
52            current,
53            options,
54            start_time,
55            git_cache,
56            git_bash,
57        }
58    }
59
60    #[cfg(windows)]
61    fn match_options(config: &Config) -> MatchOptions {
62        let mut options = MatchOptions::new();
63        options.case_sensitive = config.case_sensitive().unwrap_or(false);
64        options
65    }
66
67    #[cfg(not(windows))]
68    fn match_options(config: &Config) -> MatchOptions {
69        let mut options = MatchOptions::new();
70        options.case_sensitive = config.case_sensitive().unwrap_or(true);
71        options
72    }
73
74    pub fn find_files(&self) -> MyResult<Vec<File>> {
75        let files = RefCell::new(BTreeSet::new());
76        let tasks = self.group_tasks()?;
77        for ((abs_root, rel_root), patterns) in tasks.iter_all() {
78            self.find_entries(&files, abs_root, rel_root, patterns)?;
79            self.find_parents(&files, abs_root, rel_root)?;
80        }
81        let files = files.into_inner().into_iter().collect();
82        Ok(files)
83    }
84
85    pub fn create_total(&self, files: &Vec<File>) -> Total {
86        Total::from_files(self.start_time, self.config, files)
87    }
88
89    fn group_tasks(&self) -> MyResult<MultiMap<(PathBuf, PathBuf), Pattern>> {
90        let mut tasks = MultiMap::new();
91        for pattern in self.config.patterns() {
92            if let Some((abs_root, rel_root, filename)) = self.parse_pattern(pattern) {
93                let pattern = Pattern::new(&filename).map_err(|e| (e, &filename))?;
94                tasks.insert((abs_root, rel_root), pattern);
95            }
96        }
97        Ok(tasks)
98    }
99
100    #[cfg(windows)]
101    fn parse_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
102        if self.git_bash {
103            let drive_regex = regex!(r#"^/([A-Za-z])/(.+)$"#);
104            if let Some(captures) = drive_regex.captures(pattern) {
105                let drive = captures.get(1).unwrap().as_str().to_uppercase();
106                let path = captures.get(2).unwrap().as_str().replace("/", MAIN_SEPARATOR_STR);
107                let pattern = format!("{}:{}{}", drive, MAIN_SEPARATOR_STR, path);
108                self.split_pattern(&pattern)
109            } else {
110                let pattern = pattern.replace("/", MAIN_SEPARATOR_STR);
111                self.split_pattern(&pattern)
112            }
113        } else {
114            self.split_pattern(pattern)
115        }
116    }
117
118    #[cfg(not(windows))]
119    fn parse_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
120        self.split_pattern(pattern)
121    }
122
123    fn split_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
124        let rel_root = PathBuf::from(pattern);
125        let abs_root = self.current.join(&rel_root).clean();
126        if requires_wildcard(&rel_root, self.config.zip_expand()) {
127            let name = String::from("*");
128            return Some((abs_root, rel_root, name));
129        }
130        if let Some(mut name) = find_name(&abs_root) {
131            if let Some(abs_root) = find_parent(&abs_root) {
132                if let Some(rel_root) = find_parent(&rel_root) {
133                    if name.starts_with(".") {
134                        name = format!("*{name}");
135                    }
136                    return Some((abs_root, rel_root, name));
137                }
138            }
139        }
140        None
141    }
142
143    fn find_entries(
144        &self,
145        files: &RefCell<BTreeSet<File>>,
146        abs_root: &Path,
147        rel_root: &Path,
148        patterns: &Vec<Pattern>,
149    ) -> MyResult<()> {
150        let rel_depth = count_components(rel_root);
151        let git_cache = self.git_cache.as_ref().map(Rc::clone);
152        self.system.walk_entries(abs_root, rel_root, git_cache, &|entry| {
153            match entry {
154                Ok(entry) => {
155                    self.insert_file(
156                        files,
157                        entry,
158                        abs_root,
159                        rel_root,
160                        rel_depth,
161                        patterns,
162                    );
163                }
164                Err(error) => if !self.config.quiet() {
165                    error.print_error();
166                }
167            }
168        })
169    }
170
171    fn insert_file(
172        &self,
173        files: &RefCell<BTreeSet<File>>,
174        entry: &dyn Entry,
175        abs_root: &Path,
176        rel_root: &Path,
177        rel_depth: usize,
178        patterns: &Vec<Pattern>,
179    ) {
180        match self.create_file(entry, abs_root, rel_root, rel_depth, patterns) {
181            Ok(file) => if let Some(file) = file {
182                if !self.config.sort_name() || self.config.show_indent() || (file.file_type != FileKind::Dir) {
183                    files.borrow_mut().insert(file);
184                }
185            }
186            Err(error) => if !self.config.quiet() {
187                error.print_error();
188            }
189        }
190    }
191
192    fn create_file(
193        &self,
194        entry: &dyn Entry,
195        abs_root: &Path,
196        rel_root: &Path,
197        rel_depth: usize,
198        patterns: &Vec<Pattern>,
199    ) -> MyResult<Option<File>> {
200        if let Some(name) = entry.file_name().to_str() {
201            if !patterns.iter().any(|p| p.matches_with(name, self.options)) {
202                return Ok(None);
203            }
204            if let Some(depth) = self.config.min_depth() {
205                if entry.file_depth() < depth {
206                    return Ok(None);
207                }
208            }
209            let file_type = FileKind::from_entry(self.system, entry);
210            if let Some(filter_types) = self.config.filter_types() {
211                if !filter_types.contains(&file_type) {
212                    return Ok(None);
213                }
214            }
215            if let FileKind::Link(_) = file_type {
216                let link_path = self.system.read_link(entry)?;
217                if let Some(link_path) = link_path {
218                    return if let Some(link_entry) = self.follow_link(entry.file_path(), &link_path) {
219                        entry.copy_metadata(link_entry.as_ref().as_ref());
220                        let link_path = link_entry.file_path().to_path_buf();
221                        let link_type = FileKind::from_entry(self.system, link_entry.as_ref().as_ref());
222                        self.create_inner(
223                            entry,
224                            abs_root,
225                            rel_root,
226                            rel_depth,
227                            file_type,
228                            Some((link_path, link_type)),
229                        )
230                    } else {
231                        entry.reset_metadata();
232                        self.create_inner(
233                            entry,
234                            abs_root,
235                            rel_root,
236                            rel_depth,
237                            FileKind::Link(false),
238                            Some((link_path, FileKind::Link(false))),
239                        )
240                    }
241                }
242            }
243            return self.create_inner(
244                entry,
245                abs_root,
246                rel_root,
247                rel_depth,
248                file_type,
249                None,
250            );
251        }
252        Ok(None)
253    }
254
255    fn follow_link(&self, file_path: &Path, link_path: &Path) -> Option<Rc<Box<dyn Entry>>> {
256        if let Some(file_parent) = file_path.parent() {
257            let link_path = file_parent.join(link_path);
258            return self.system.get_entry(&link_path).ok();
259        }
260        None
261    }
262
263    fn create_inner(
264        &self,
265        entry: &dyn Entry,
266        abs_root: &Path,
267        rel_root: &Path,
268        rel_depth: usize,
269        file_type: FileKind,
270        link_data: Option<(PathBuf, FileKind)>,
271    ) -> MyResult<Option<File>> {
272        let file_time = DateTime::<Utc>::from(entry.file_time());
273        match self.start_time {
274            RecentKind::Before(time) if file_time > time => return Ok(None),
275            RecentKind::After(time) if file_time <= time => return Ok(None),
276            _ => (),
277        }
278        let abs_path = entry.file_path();
279        let git_flags = if let Some(git_cache) = &self.git_cache {
280            if entry.file_flags() != FileFlags::File {
281                return Ok(None);
282            }
283            let flags = git_cache.test_allowed(abs_path)?;
284            if flags.is_none() {
285                return Ok(None);
286            }
287            flags
288        } else {
289            None
290        };
291        if let Some(rel_path) = create_relative(abs_root, rel_root, abs_path) {
292            if let Some(abs_dir) = select_parent(abs_path, file_type) {
293                if let Some(rel_dir) = select_parent_from_owned(rel_path, file_type) {
294                    let file_depth = entry.file_depth() + rel_depth;
295                    let inner_depth = entry.inner_depth();
296                    let file_name = select_name(abs_path, file_type).unwrap_or_default();
297                    let file_ext = find_extension(abs_path, file_type);
298                    let file_size = select_size(entry, &link_data, file_type);
299                    let mut file = File::new(abs_dir, rel_dir, file_depth, inner_depth, file_name, file_ext, file_type)
300                        .with_mode(entry.file_mode())
301                        .with_size(file_size)
302                        .with_time(file_time)
303                        .with_git(git_flags);
304                    #[cfg(unix)]
305                    if self.config.show_owner() {
306                        let user = self.system.find_user(entry.owner_uid());
307                        let group = self.system.find_group(entry.owner_gid());
308                        file = file.with_owner(user, group);
309                    }
310                    if self.config.show_crc() {
311                        if let FileKind::File(_) | FileKind::Link(_) = file_type {
312                            let file_crc = self.system.read_crc(entry);
313                            file = file.with_crc(file_crc);
314                        }
315                    }
316                    if self.config.show_sig() {
317                        if let FileKind::File(_) | FileKind::Link(_) = file_type {
318                            let file_sig = self.system.read_sig(entry);
319                            file = file.with_sig(file_sig);
320                        }
321                    }
322                    #[cfg(windows)]
323                    if self.config.win_ver() {
324                        if let Some(file_ver) = self.system.read_version(entry) {
325                            file = file.with_version(file_ver);
326                        }
327                    }
328                    if let Some((link_path, link_type)) = link_data {
329                        file = file.with_link(link_path, link_type);
330                    }
331                    return Ok(Some(file));
332                }
333            }
334        }
335        Ok(None)
336    }
337
338    fn find_parents(
339        &self,
340        files: &RefCell<BTreeSet<File>>,
341        abs_root: &Path,
342        rel_root: &Path,
343    ) -> MyResult<()> {
344        if self.config.show_indent() {
345            let parents = find_parents(files)?;
346            for (abs_path, file_depth) in parents.into_iter() {
347                self.insert_parent(files, abs_root, rel_root, abs_path, file_depth);
348            }
349        }
350        Ok(())
351    }
352
353    #[allow(unused_mut)]
354    fn insert_parent(
355        &self,
356        files: &RefCell<BTreeSet<File>>,
357        abs_root: &Path,
358        rel_root: &Path,
359        abs_path: PathBuf,
360        file_depth: usize,
361    ) {
362        if let Some(rel_path) = create_relative(abs_root, rel_root, &abs_path) {
363            if let Some(abs_dir) = select_parent(&abs_path, FileKind::Dir) {
364                if let Some(rel_dir) = select_parent_from_owned(rel_path, FileKind::Dir) {
365                    let sys_entry = self.system.get_entry(&abs_path).ok();
366                    let inner_depth = sys_entry.as_ref().and_then(|e| e.inner_depth());
367                    let file_mode = sys_entry.as_ref().map(|e| e.file_mode()).unwrap_or_default();
368                    let file_time = sys_entry.as_ref().map(|e| e.file_time()).unwrap_or(SystemTime::UNIX_EPOCH);
369                    let file_name = String::from("");
370                    let file_ext = String::from("");
371                    let mut file = File::new(abs_dir, rel_dir, file_depth, inner_depth, file_name, file_ext, FileKind::Dir)
372                        .with_mode(file_mode)
373                        .with_time(DateTime::<Utc>::from(file_time));
374                    #[cfg(unix)]
375                    if self.config.show_owner() {
376                        let uid = sys_entry.as_ref().map(|e| e.owner_uid()).unwrap_or_default();
377                        let gid = sys_entry.as_ref().map(|e| e.owner_gid()).unwrap_or_default();
378                        let user = self.system.find_user(uid);
379                        let group = self.system.find_group(gid);
380                        file = file.with_owner(user, group);
381                    }
382                    files.borrow_mut().insert(file);
383                }
384            }
385        }
386    }
387}
388
389fn requires_wildcard(root: &Path, zip_expand: bool) -> bool {
390    let wildcard_regex = regex!(r"(^\.+$|[\\/]\.*$)");
391    if let Some(root) = root.to_str() {
392        if wildcard_regex.is_match(root) {
393            return true;
394        }
395    }
396    ZipKind::from_path(root, zip_expand).is_some()
397}
398
399pub fn count_components(path: &Path) -> usize {
400    path
401        .components()
402        .filter(|c| matches!(c, Component::Normal(_)))
403        .count()
404}
405
406fn find_parents(files: &RefCell<BTreeSet<File>>) -> MyResult<BTreeMap<PathBuf, usize>> {
407    let mut parents = BTreeMap::new();
408    for file in files.borrow().iter() {
409        let file_depth = file.file_depth + file.file_type.dir_offset();
410        find_ancestors(&mut parents, &file.abs_dir, file_depth)?;
411    }
412    Ok(parents)
413}
414
415fn find_ancestors(
416    parents: &mut BTreeMap<PathBuf, usize>,
417    abs_path: &Path,
418    file_depth: usize,
419) -> MyResult<()> {
420    if let Some(file_depth) = file_depth.checked_sub(1) {
421        if file_depth > 0 {
422            if let Some(old_depth) = parents.insert(PathBuf::from(abs_path), file_depth) {
423                if old_depth != file_depth {
424                    let error = format!("Inconsistent depth: {}", abs_path.display());
425                    return Err(MyError::Text(error));
426                }
427            } else {
428                if let Some(abs_path) = abs_path.parent() {
429                    find_ancestors(parents, abs_path, file_depth)?;
430                }
431            }
432        }
433    }
434    Ok(())
435}
436
437fn create_relative(
438    abs_root: &Path,
439    rel_root: &Path,
440    abs_path: &Path,
441) -> Option<PathBuf> {
442    let mut abs_root = PathBuf::from(abs_root);
443    let mut rel_path = PathBuf::new();
444    loop {
445        if let Ok(path) = abs_path.strip_prefix(&abs_root) {
446            rel_path.push(path);
447            return Some(rel_root.join(rel_path).clean());
448        }
449        if !abs_root.pop() {
450            return None;
451        }
452        rel_path.push("..");
453    }
454}
455
456fn select_parent_from_owned(path: PathBuf, file_type: FileKind) -> Option<PathBuf> {
457    if file_type == FileKind::Dir {
458        Some(path)
459    } else {
460        find_parent(&path)
461    }
462}
463
464fn select_parent(path: &Path, file_type: FileKind) -> Option<PathBuf> {
465    if file_type == FileKind::Dir {
466        Some(PathBuf::from(path))
467    } else {
468        find_parent(path)
469    }
470}
471
472fn select_name(path: &Path, file_type: FileKind) -> Option<String> {
473    if file_type == FileKind::Dir {
474        Some(String::from(""))
475    } else {
476        find_name(path)
477    }
478}
479
480fn find_parent(path: &Path) -> Option<PathBuf> {
481    path.parent().map(PathBuf::from)
482}
483
484fn find_name(path: &Path) -> Option<String> {
485    path.file_name().and_then(OsStr::to_str).map(String::from)
486}
487
488fn find_extension(path: &Path, file_type: FileKind) -> String {
489    match file_type {
490        FileKind::File(_) | FileKind::Link(_) => path.extension()
491            .and_then(OsStr::to_str)
492            .map(str::to_ascii_lowercase)
493            .map(|ext| format!(".{ext}"))
494            .unwrap_or_default(),
495        _ => String::default(),
496    }
497}
498
499fn select_size(
500    entry: &dyn Entry,
501    link_data: &Option<(PathBuf, FileKind)>,
502    file_type: FileKind,
503) -> u64 {
504    if file_type == FileKind::Dir {
505        return 0;
506    }
507    if let Some((_, link_type)) = link_data {
508        if *link_type == FileKind::Dir {
509            return 0;
510        }
511    }
512    entry.file_size()
513}
514
515#[cfg(test)]
516mod tests {
517    use crate::cli::file::{ExecKind, FileKind};
518    use crate::cli::recent::{PeriodKind, RecentKind};
519    use crate::config::Config;
520    use crate::finder::{create_relative, requires_wildcard, Finder};
521    use crate::fs::file::File;
522    use crate::fs::system::tests::MockSystem;
523    use crate::util::matcher::tests::*;
524    use chrono::Utc;
525    use googletest::prelude::*;
526    use std::path::PathBuf;
527
528    #[gtest]
529    fn test_dir_requires_wildcard() {
530        expect_true!(test_wildcard(".", false));
531        expect_true!(test_wildcard("..", false));
532        expect_true!(test_wildcard("/", false));
533        expect_true!(test_wildcard("/path/to/dir/", false));
534        expect_true!(test_wildcard("/path/to/dir/.", false));
535        expect_true!(test_wildcard("/path/to/dir/..", false));
536        expect_true!(test_wildcard(r"\", false));
537        expect_true!(test_wildcard(r"\path\to\dir\", false));
538        expect_true!(test_wildcard(r"\path\to\dir\.", false));
539        expect_true!(test_wildcard(r"\path\to\dir\..", false));
540    }
541
542    #[gtest]
543    fn test_file_requires_wildcard() {
544        expect_false!(test_wildcard("lower", false));
545        expect_false!(test_wildcard("lower.zip", false));
546        expect_false!(test_wildcard("lower.7z", false));
547        expect_false!(test_wildcard("lower.tar", false));
548        expect_false!(test_wildcard("UPPER", false));
549        expect_false!(test_wildcard("UPPER.ZIP", false));
550        expect_false!(test_wildcard("UPPER.7Z", false));
551        expect_false!(test_wildcard("UPPER.TAR", false));
552        expect_false!(test_wildcard("/path/to/dir/lower", false));
553        expect_false!(test_wildcard("/path/to/dir/lower.zip", false));
554        expect_false!(test_wildcard("/path/to/dir/lower.7z", false));
555        expect_false!(test_wildcard("/path/to/dir/lower.tar", false));
556        expect_false!(test_wildcard("/path/to/dir/UPPER", false));
557        expect_false!(test_wildcard("/path/to/dir/UPPER.ZIP", false));
558        expect_false!(test_wildcard("/path/to/dir/UPPER.7Z", false));
559        expect_false!(test_wildcard("/path/to/dir/UPPER.TAR", false));
560        expect_false!(test_wildcard(r"\path\to\dir\lower", false));
561        expect_false!(test_wildcard(r"\path\to\dir\lower.zip", false));
562        expect_false!(test_wildcard(r"\path\to\dir\lower.7z", false));
563        expect_false!(test_wildcard(r"\path\to\dir\lower.tar", false));
564        expect_false!(test_wildcard(r"\path\to\dir\UPPER", false));
565        expect_false!(test_wildcard(r"\path\to\dir\UPPER.ZIP", false));
566        expect_false!(test_wildcard(r"\path\to\dir\UPPER.7Z", false));
567        expect_false!(test_wildcard(r"\path\to\dir\UPPER.TAR", false));
568    }
569
570    #[gtest]
571    fn test_archive_requires_wildcard() {
572        expect_false!(test_wildcard("lower", true));
573        expect_true!(test_wildcard("lower.zip", true));
574        expect_true!(test_wildcard("lower.7z", true));
575        expect_true!(test_wildcard("lower.tar", true));
576        expect_false!(test_wildcard("UPPER", true));
577        expect_true!(test_wildcard("UPPER.ZIP", true));
578        expect_true!(test_wildcard("UPPER.7Z", true));
579        expect_true!(test_wildcard("UPPER.TAR", true));
580        expect_false!(test_wildcard("/path/to/dir/lower", true));
581        expect_true!(test_wildcard("/path/to/dir/lower.zip", true));
582        expect_true!(test_wildcard("/path/to/dir/lower.7z", true));
583        expect_true!(test_wildcard("/path/to/dir/lower.tar", true));
584        expect_false!(test_wildcard("/path/to/dir/UPPER", true));
585        expect_true!(test_wildcard("/path/to/dir/UPPER.ZIP", true));
586        expect_true!(test_wildcard("/path/to/dir/UPPER.7Z", true));
587        expect_true!(test_wildcard("/path/to/dir/UPPER.TAR", true));
588        expect_false!(test_wildcard(r"\path\to\dir\lower", true));
589        expect_true!(test_wildcard(r"\path\to\dir\lower.zip", true));
590        expect_true!(test_wildcard(r"\path\to\dir\lower.7z", true));
591        expect_true!(test_wildcard(r"\path\to\dir\lower.tar", true));
592        expect_false!(test_wildcard(r"\path\to\dir\UPPER", true));
593        expect_true!(test_wildcard(r"\path\to\dir\UPPER.ZIP", true));
594        expect_true!(test_wildcard(r"\path\to\dir\UPPER.7Z", true));
595        expect_true!(test_wildcard(r"\path\to\dir\UPPER.TAR", true));
596    }
597
598    fn test_wildcard(root: &str, zip_expand: bool) -> bool {
599        let root = PathBuf::from(root);
600        requires_wildcard(&root, zip_expand)
601    }
602
603    #[gtest]
604    #[cfg(all(windows, any()))]
605    fn test_counts_components() {
606        use crate::finder::count_components;
607        expect_eq!(count_components(&PathBuf::from(r"")), 0);
608        expect_eq!(count_components(&PathBuf::from(r"..")), 0);
609        expect_eq!(count_components(&PathBuf::from(r"..\dir")), 1);
610        expect_eq!(count_components(&PathBuf::from(r"..\dir\subdir")), 2);
611        expect_eq!(count_components(&PathBuf::from(r".")), 0);
612        expect_eq!(count_components(&PathBuf::from(r".\dir")), 1);
613        expect_eq!(count_components(&PathBuf::from(r".\dir\subdir")), 2);
614        expect_eq!(count_components(&PathBuf::from(r"dir")), 1);
615        expect_eq!(count_components(&PathBuf::from(r"dir\subdir")), 2);
616        expect_eq!(count_components(&PathBuf::from(r"\dir")), 1);
617        expect_eq!(count_components(&PathBuf::from(r"\dir\subdir")), 2);
618        expect_eq!(count_components(&PathBuf::from(r"D:\dir")), 1);
619        expect_eq!(count_components(&PathBuf::from(r"D:\dir\subdir")), 2);
620        expect_eq!(count_components(&PathBuf::from(r"\\unc\dir")), 1);
621        expect_eq!(count_components(&PathBuf::from(r"\\unc\dir\subdir")), 2);
622    }
623
624    #[gtest]
625    #[cfg(not(windows))]
626    fn test_counts_components() {
627        use crate::finder::count_components;
628        expect_eq!(count_components(&PathBuf::from("")), 0);
629        expect_eq!(count_components(&PathBuf::from("..")), 0);
630        expect_eq!(count_components(&PathBuf::from("../dir")), 1);
631        expect_eq!(count_components(&PathBuf::from("../dir/subdir")), 2);
632        expect_eq!(count_components(&PathBuf::from(".")), 0);
633        expect_eq!(count_components(&PathBuf::from("./dir")), 1);
634        expect_eq!(count_components(&PathBuf::from("./dir/subdir")), 2);
635        expect_eq!(count_components(&PathBuf::from("dir")), 1);
636        expect_eq!(count_components(&PathBuf::from("dir/subdir")), 2);
637        expect_eq!(count_components(&PathBuf::from("/dir")), 1);
638        expect_eq!(count_components(&PathBuf::from("/dir/subdir")), 2);
639    }
640
641    #[gtest]
642    fn test_creates_relative_paths() {
643        expect_that!(test_relative("/root"), some(eq("..")));
644        expect_that!(test_relative("/root/dir"), some(eq("../dir")));
645        expect_that!(test_relative("/root/dir/subdir"), some(eq("../dir/subdir")));
646        expect_that!(test_relative("/root/dir2"), some(eq("../dir2")));
647        expect_that!(test_relative("/root/dir2/subdir"), some(eq("../dir2/subdir")));
648        expect_that!(test_relative("/"), some(eq("../..")));
649        expect_that!(test_relative("/root2/dir"), some(eq("../../root2/dir")));
650        expect_that!(test_relative("/root2/dir/subdir"), some(eq("../../root2/dir/subdir")));
651    }
652
653    fn test_relative(abs_path: &str) -> Option<PathBuf> {
654        let abs_root = PathBuf::from("/root/dir");
655        let rel_root = PathBuf::from("../dir");
656        let abs_path = PathBuf::from(abs_path);
657        create_relative(&abs_root, &rel_root, &abs_path)
658    }
659
660    #[gtest]
661    fn test_parses_file_attributes_in_root_directory() {
662        let config = Config::default()
663            .with_patterns(vec!["*"])
664            .with_recurse_all(true);
665        let system = create_system(&config, create_entries);
666        let finder = create_finder(&config, &system);
667        let files = find_files(&finder);
668        expect_that!(files, elements_are![
669            pat!(File {
670                abs_dir: eq("/root"),
671                rel_dir: eq(""),
672                file_name: eq("archive.sh"),
673                file_ext: eq(".sh"),
674                file_depth: eq(&1),
675                file_type: eq(&FileKind::File(ExecKind::User)),
676                file_mode: eq(&0o744),
677                file_size: eq(&100),
678                file_time: time_eq(2023, 1, 1, 0, 0, 0),
679                link_data: none(),
680                ..
681            }),
682            pat!(File {
683                abs_dir: eq("/root/dir"),
684                rel_dir: eq("dir"),
685                file_name: eq(""),
686                file_ext: eq(""),
687                file_depth: eq(&1),
688                file_type: eq(&FileKind::Dir),
689                file_mode: eq(&0o755),
690                file_size: eq(&0),
691                file_time: time_eq(2023, 2, 2, 0, 0, 0),
692                link_data: none(),
693                ..
694            }),
695            pat!(File {
696                abs_dir: eq("/root/dir"),
697                rel_dir: eq("dir"),
698                file_name: eq("link1"),
699                file_ext: eq(""),
700                file_depth: eq(&2),
701                file_type: eq(&FileKind::Link(true)),
702                file_mode: eq(&0o755),
703                file_size: eq(&0),
704                file_time: time_eq(2023, 3, 3, 0, 0, 0),
705                link_data: some((eq("/root/dir/subdir"), eq(&FileKind::Dir))),
706                ..
707            }),
708            pat!(File {
709                abs_dir: eq("/root/dir"),
710                rel_dir: eq("dir"),
711                file_name: eq("link2"),
712                file_ext: eq(""),
713                file_depth: eq(&2),
714                file_type: eq(&FileKind::Link(true)),
715                file_mode: eq(&0o644),
716                file_size: eq(&500),
717                file_time: time_eq(2023, 5, 5, 0, 0, 0),
718                link_data: some((eq("/root/dir/subdir/alpha.txt"), eq(&FileKind::File(ExecKind::None)))),
719                ..
720            }),
721            pat!(File {
722                abs_dir: eq("/root/dir"),
723                rel_dir: eq("dir"),
724                file_name: eq("link3"),
725                file_ext: eq(""),
726                file_depth: eq(&2),
727                file_type: eq(&FileKind::Link(false)),
728                file_mode: eq(&0o644),
729                file_size: eq(&0),
730                file_time: time_eq(1970, 1, 1, 0, 0, 0),
731                link_data: some((eq("/etc/missing.txt"), eq(&FileKind::Link(false)))),
732                ..
733            }),
734            pat!(File {
735                abs_dir: eq("/root/dir/subdir"),
736                rel_dir: eq("dir/subdir"),
737                file_name: eq(""),
738                file_ext: eq(""),
739                file_depth: eq(&2),
740                file_type: eq(&FileKind::Dir),
741                file_mode: eq(&0o755),
742                file_size: eq(&0),
743                file_time: time_eq(2023, 3, 3, 0, 0, 0),
744                link_data: none(),
745                ..
746            }),
747            pat!(File {
748                abs_dir: eq("/root/dir/subdir"),
749                rel_dir: eq("dir/subdir"),
750                file_name: eq("alpha.csv"),
751                file_ext: eq(".csv"),
752                file_depth: eq(&3),
753                file_type: eq(&FileKind::File(ExecKind::None)),
754                file_mode: eq(&0o644),
755                file_size: eq(&400),
756                file_time: time_eq(2023, 4, 4, 0, 0, 0),
757                link_data: none(),
758                ..
759            }),
760            pat!(File {
761                abs_dir: eq("/root/dir/subdir"),
762                rel_dir: eq("dir/subdir"),
763                file_name: eq("alpha.txt"),
764                file_ext: eq(".txt"),
765                file_depth: eq(&3),
766                file_type: eq(&FileKind::File(ExecKind::None)),
767                file_mode: eq(&0o644),
768                file_size: eq(&500),
769                file_time: time_eq(2023, 5, 5, 0, 0, 0),
770                link_data: none(),
771                ..
772            }),
773            pat!(File {
774                abs_dir: eq("/root/dir/subdir"),
775                rel_dir: eq("dir/subdir"),
776                file_name: eq("beta.csv"),
777                file_ext: eq(".csv"),
778                file_depth: eq(&3),
779                file_type: eq(&FileKind::File(ExecKind::None)),
780                file_mode: eq(&0o644),
781                file_size: eq(&600),
782                file_time: time_eq(2023, 6, 6, 0, 0, 0),
783                link_data: none(),
784                ..
785            }),
786            pat!(File {
787                abs_dir: eq("/root/dir/subdir"),
788                rel_dir: eq("dir/subdir"),
789                file_name: eq("beta.txt"),
790                file_ext: eq(".txt"),
791                file_depth: eq(&3),
792                file_type: eq(&FileKind::File(ExecKind::None)),
793                file_mode: eq(&0o644),
794                file_size: eq(&700),
795                file_time: time_eq(2023, 7, 7, 0, 0, 0),
796                link_data: none(),
797                ..
798            }),
799        ]);
800    }
801
802    #[gtest]
803    fn test_finds_multiple_patterns_in_same_directory() {
804        let config = Config::default()
805            .with_patterns(vec!["dir/subdir/alpha.*", "dir/subdir/*.txt"])
806            .with_recurse_all(true);
807        let system = create_system(&config, create_entries);
808        let finder = create_finder(&config, &system);
809        let files = find_files(&finder);
810        let paths = convert_paths(files);
811        expect_that!(paths, elements_are![
812            "/root/dir/subdir/alpha.csv",
813            "/root/dir/subdir/alpha.txt",
814            "/root/dir/subdir/beta.txt",
815        ]);
816    }
817
818    #[gtest]
819    fn test_finds_multiple_patterns_in_diff_directories() {
820        let config = Config::default()
821            .with_patterns(vec!["dir/alpha.*", "dir/subdir/*.txt"])
822            .with_recurse_all(true);
823        let system = create_system(&config, create_entries);
824        let finder = create_finder(&config, &system);
825        let files = find_files(&finder);
826        let paths = convert_paths(files);
827        expect_that!(paths, elements_are![
828            "/root/dir/subdir/alpha.csv",
829            "/root/dir/subdir/alpha.txt",
830            "/root/dir/subdir/beta.txt",
831        ]);
832    }
833
834    #[gtest]
835    fn test_finds_files_if_recurse_no_indent_in_root_directory() {
836        let config = Config::default()
837            .with_patterns(vec!["*.txt"])
838            .with_recurse_all(true);
839        let system = create_system(&config, create_entries);
840        let finder = create_finder(&config, &system);
841        let files = find_files(&finder);
842        let paths = convert_paths(files);
843        expect_that!(paths, elements_are![
844            "/root/dir/subdir/alpha.txt",
845            "/root/dir/subdir/beta.txt",
846        ]);
847    }
848
849    #[gtest]
850    fn test_finds_parents_if_recurse_with_indent_in_root_directory() {
851        let config = Config::default()
852            .with_patterns(vec!["*.txt"])
853            .with_recurse_all(true)
854            .with_show_indent(true);
855        let system = create_system(&config, create_entries);
856        let finder = create_finder(&config, &system);
857        let files = find_files(&finder);
858        let paths = convert_paths(files);
859        expect_that!(paths, elements_are![
860            "/root/dir/",
861            "/root/dir/subdir/",
862            "/root/dir/subdir/alpha.txt",
863            "/root/dir/subdir/beta.txt",
864        ]);
865    }
866
867    #[gtest]
868    fn test_finds_parents_if_recurse_with_indent_in_branch_directory() {
869        let config = Config::default()
870            .with_patterns(vec!["dir/*.txt"])
871            .with_recurse_all(true)
872            .with_show_indent(true);
873        let system = create_system(&config, create_entries);
874        let finder = create_finder(&config, &system);
875        let files = find_files(&finder);
876        let paths = convert_paths(files);
877        expect_that!(paths, elements_are![
878            "/root/dir/",
879            "/root/dir/subdir/",
880            "/root/dir/subdir/alpha.txt",
881            "/root/dir/subdir/beta.txt",
882        ]);
883    }
884
885    #[gtest]
886    fn test_finds_parents_if_recurse_with_indent_in_leaf_directory() {
887        let config = Config::default()
888            .with_patterns(vec!["dir/subdir/*.txt"])
889            .with_recurse_all(true)
890            .with_show_indent(true);
891        let system = create_system(&config, create_entries);
892        let finder = create_finder(&config, &system);
893        let files = find_files(&finder);
894        let paths = convert_paths(files);
895        expect_that!(paths, elements_are![
896            "/root/dir/",
897            "/root/dir/subdir/",
898            "/root/dir/subdir/alpha.txt",
899            "/root/dir/subdir/beta.txt",
900        ]);
901    }
902
903    #[gtest]
904    fn test_hides_directories_if_order_by_name() {
905        let config = Config::default()
906            .with_patterns(vec!["*"])
907            .with_recurse_all(true)
908            .with_sort_name(true);
909        let system = create_system(&config, create_entries);
910        let finder = create_finder(&config, &system);
911        let files = find_files(&finder);
912        let paths = convert_paths(files);
913        expect_that!(paths, elements_are![
914            "/root/archive.sh",
915            "/root/dir/link1",
916            "/root/dir/link2",
917            "/root/dir/link3",
918            "/root/dir/subdir/alpha.csv",
919            "/root/dir/subdir/alpha.txt",
920            "/root/dir/subdir/beta.csv",
921            "/root/dir/subdir/beta.txt",
922        ]);
923    }
924
925    #[gtest]
926    fn test_finds_files_with_bare_filename() {
927        let config = Config::default()
928            .with_patterns(vec!["beta.csv"])
929            .with_recurse_all(true);
930        let system = create_system(&config, create_entries);
931        let finder = create_finder(&config, &system);
932        let files = find_files(&finder);
933        let paths = convert_paths(files);
934        expect_that!(paths, elements_are![
935            "/root/dir/subdir/beta.csv",
936        ]);
937    }
938
939    #[gtest]
940    fn test_finds_files_with_bare_extension() {
941        let config = Config::default()
942            .with_patterns(vec![".csv"])
943            .with_recurse_all(true);
944        let system = create_system(&config, create_entries);
945        let finder = create_finder(&config, &system);
946        let files = find_files(&finder);
947        let paths = convert_paths(files);
948        expect_that!(paths, elements_are![
949            "/root/dir/subdir/alpha.csv",
950            "/root/dir/subdir/beta.csv",
951        ]);
952    }
953
954    #[gtest]
955    fn test_filters_files_by_minimum_depth() {
956        let config = Config::default()
957            .with_patterns(vec!["*"])
958            .with_min_depth(2);
959        let system = create_system(&config, create_entries);
960        let finder = create_finder(&config, &system);
961        let files = find_files(&finder);
962        let paths = convert_paths(files);
963        expect_that!(paths, elements_are![
964            "/root/dir/link1",
965            "/root/dir/link2",
966            "/root/dir/link3",
967            "/root/dir/subdir/",
968            "/root/dir/subdir/alpha.csv",
969            "/root/dir/subdir/alpha.txt",
970            "/root/dir/subdir/beta.csv",
971            "/root/dir/subdir/beta.txt",
972        ]);
973    }
974
975    #[gtest]
976    fn test_filters_files_by_maximum_depth() {
977        let config = Config::default()
978            .with_patterns(vec!["*"])
979            .with_max_depth(2);
980        let system = create_system(&config, create_entries);
981        let finder = create_finder(&config, &system);
982        let files = find_files(&finder);
983        let paths = convert_paths(files);
984        expect_that!(paths, elements_are![
985            "/root/archive.sh",
986            "/root/dir/",
987            "/root/dir/link1",
988            "/root/dir/link2",
989            "/root/dir/link3",
990            "/root/dir/subdir/",
991        ]);
992    }
993
994    #[gtest]
995    fn test_filters_files_by_file_type() {
996        let config = Config::default()
997            .with_patterns(vec!["*"])
998            .with_recurse_all(true)
999            .with_filter_types(vec![
1000                FileKind::File(ExecKind::None),
1001                FileKind::File(ExecKind::User),
1002                FileKind::File(ExecKind::Other),
1003            ]);
1004        let system = create_system(&config, create_entries);
1005        let finder = create_finder(&config, &system);
1006        let files = find_files(&finder);
1007        let paths = convert_paths(files);
1008        expect_that!(paths, elements_are![
1009            "/root/archive.sh",
1010            "/root/dir/subdir/alpha.csv",
1011            "/root/dir/subdir/alpha.txt",
1012            "/root/dir/subdir/beta.csv",
1013            "/root/dir/subdir/beta.txt",
1014        ]);
1015    }
1016
1017    #[gtest]
1018    fn test_filters_files_before_recent_time() {
1019        let config = Config::default()
1020            .with_patterns(vec!["*"])
1021            .with_recurse_all(true)
1022            .with_curr_time(2024, 1, 1, 0, 0, 0)
1023            .with_filter_recent(RecentKind::Before(PeriodKind::Month(8)));
1024        let system = create_system(&config, create_entries);
1025        let finder = create_finder(&config, &system);
1026        let files = find_files(&finder);
1027        let paths = convert_paths(files);
1028        expect_that!(paths, elements_are![
1029            "/root/archive.sh",
1030            "/root/dir/",
1031            "/root/dir/link1",
1032            "/root/dir/link3",
1033            "/root/dir/subdir/",
1034            "/root/dir/subdir/alpha.csv",
1035        ]);
1036    }
1037
1038    #[gtest]
1039    fn test_filters_files_after_recent_time() {
1040        let config = Config::default()
1041            .with_patterns(vec!["*"])
1042            .with_recurse_all(true)
1043            .with_curr_time(2024, 1, 1, 0, 0, 0)
1044            .with_filter_recent(RecentKind::After(PeriodKind::Month(8)));
1045        let system = create_system(&config, create_entries);
1046        let finder = create_finder(&config, &system);
1047        let files = find_files(&finder);
1048        let paths = convert_paths(files);
1049        expect_that!(paths, elements_are![
1050            "/root/dir/link2",
1051            "/root/dir/subdir/alpha.txt",
1052            "/root/dir/subdir/beta.csv",
1053            "/root/dir/subdir/beta.txt",
1054        ]);
1055    }
1056
1057    #[gtest]
1058    fn test_calculates_total_from_files() {
1059        let config = Config::default()
1060            .with_patterns(vec!["*"])
1061            .with_recurse_all(true);
1062        let system = create_system(&config, create_entries);
1063        let finder = create_finder(&config, &system);
1064        let files = find_files(&finder);
1065        let total = finder.create_total(&files);
1066        expect_eq!(total.max_size, 700);
1067        expect_eq!(total.total_size, 2800);
1068        #[cfg(unix)]
1069        expect_eq!(total.user_width, 0);
1070        #[cfg(unix)]
1071        expect_eq!(total.group_width, 0);
1072        #[cfg(windows)]
1073        expect_eq!(total.ver_width, 0);
1074        expect_eq!(total.ext_width, 4);
1075        expect_eq!(total.num_files, 8);
1076        expect_eq!(total.num_dirs, 2);
1077    }
1078
1079    #[gtest]
1080    #[cfg(unix)]
1081    fn test_calculates_total_from_files_with_no_owners() {
1082        let config = Config::default()
1083            .with_show_owner(true);
1084        let system = create_system(&config, create_entries);
1085        let finder = create_finder(&config, &system);
1086        let files = find_files(&finder);
1087        let total = finder.create_total(&files);
1088        expect_eq!(total.user_width, 1);
1089        expect_eq!(total.group_width, 1);
1090    }
1091
1092    #[gtest]
1093    #[cfg(unix)]
1094    fn test_calculates_total_from_files_with_some_owners() {
1095        let config = Config::default()
1096            .with_patterns(vec!["*"])
1097            .with_recurse_all(true)
1098            .with_show_owner(true);
1099        let system = create_system(&config, create_entries);
1100        let finder = create_finder(&config, &system);
1101        let files = find_files(&finder);
1102        let total = finder.create_total(&files);
1103        expect_eq!(total.user_width, 5);
1104        expect_eq!(total.group_width, 6);
1105    }
1106
1107    fn create_entries(system: &mut MockSystem) {
1108        system.insert_entry(1, 'f', 0o744, 0, 0, 100, 2023, 1, 1, "archive.sh", None);
1109        system.insert_entry(1, 'd', 0o755, 1000, 500, 4096, 2023, 2, 2, "dir", None);
1110        system.insert_entry(2, 'l', 0o644, 1000, 500, 99, 2023, 12, 31, "dir/link1", Some("subdir"));
1111        system.insert_entry(2, 'l', 0o644, 1000, 500, 99, 2023, 12, 31, "dir/link2", Some("subdir/alpha.txt"));
1112        system.insert_entry(2, 'l', 0o644, 1000, 500, 99, 2023, 12, 31, "dir/link3", Some("/etc/missing.txt"));
1113        system.insert_entry(2, 'd', 0o755, 1500, 500, 4096, 2023, 3, 3, "dir/subdir", None);
1114        system.insert_entry(3, 'f', 0o644, 1500, 500, 400, 2023, 4, 4, "dir/subdir/alpha.csv", None);
1115        system.insert_entry(3, 'f', 0o644, 1500, 500, 500, 2023, 5, 5, "dir/subdir/alpha.txt", None);
1116        system.insert_entry(3, 'f', 0o644, 1500, 500, 600, 2023, 6, 6, "dir/subdir/beta.csv", None);
1117        system.insert_entry(3, 'f', 0o644, 1500, 500, 700, 2023, 7, 7, "dir/subdir/beta.txt", None);
1118    }
1119
1120    #[gtest]
1121    fn test_performs_case_sensitive_search() {
1122        let config = Config::default()
1123            .with_patterns(vec!["A*"])
1124            .with_recurse_all(true)
1125            .with_case_sensitive(true);
1126        let system = create_system(&config, create_cases);
1127        let finder = create_finder(&config, &system);
1128        let files = find_files(&finder);
1129        let paths = convert_paths(files);
1130        expect_that!(paths, elements_are![
1131            "/root/A1.txt",
1132            "/root/A2.txt",
1133        ]);
1134    }
1135
1136    #[gtest]
1137    fn test_performs_case_insensitive_search() {
1138        let config = Config::default()
1139            .with_patterns(vec!["A*"])
1140            .with_recurse_all(true)
1141            .with_case_sensitive(false);
1142        let system = create_system(&config, create_cases);
1143        let finder = create_finder(&config, &system);
1144        let files = find_files(&finder);
1145        let paths = convert_paths(files);
1146        expect_that!(paths, elements_are![
1147            "/root/A1.txt",
1148            "/root/A2.txt",
1149            "/root/a1.txt",
1150            "/root/a2.txt",
1151        ]);
1152    }
1153
1154    fn create_cases(system: &mut MockSystem) {
1155        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "A1.txt", None);
1156        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "A2.txt", None);
1157        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "B3.txt", None);
1158        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "B4.txt", None);
1159        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "a1.txt", None);
1160        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "a2.txt", None);
1161        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "b3.txt", None);
1162        system.insert_entry(1, 'f', 0o000, 0, 0, 0, 1970, 1, 1, "b4.txt", None);
1163    }
1164
1165    #[cfg(unix)]
1166    fn create_system<F>(config: &Config, mut setter: F) -> MockSystem<'_> where
1167        F: FnMut(&mut MockSystem),
1168    {
1169        use std::collections::BTreeMap;
1170        let current = PathBuf::from("/root");
1171        let user_names = BTreeMap::from([
1172            (0, String::from("root")),
1173            (1000, String::from("alice")),
1174            (1500, String::from("bob")),
1175        ]);
1176        let group_names = BTreeMap::from([
1177            (0, String::from("root")),
1178            (500, String::from("public")),
1179        ]);
1180        let mut system = MockSystem::new(config, current, user_names, group_names);
1181        setter(&mut system);
1182        system
1183    }
1184
1185    #[cfg(not(unix))]
1186    fn create_system<F>(config: &Config, mut setter: F) -> MockSystem<'_> where
1187        F: FnMut(&mut MockSystem),
1188    {
1189        let current = PathBuf::from("/root");
1190        let mut system = MockSystem::new(config, current);
1191        setter(&mut system);
1192        system
1193    }
1194
1195    fn create_finder<'a>(
1196        config: &'a Config,
1197        system: &'a MockSystem,
1198    ) -> Finder<'a, MockSystem<'a>> {
1199        let current = PathBuf::from("/root");
1200        Finder::new(config, system, &Utc, current, false)
1201    }
1202
1203    fn find_files(finder: &Finder<MockSystem>) -> Vec<File> {
1204        let mut files = finder.find_files().unwrap();
1205        files.sort_by_key(File::get_path);
1206        files
1207    }
1208
1209    fn convert_paths(files: Vec<File>) -> Vec<String> {
1210        files.into_iter().flat_map(convert_path).collect()
1211    }
1212
1213    #[cfg(windows)]
1214    fn convert_path(file: File) -> Option<String> {
1215        use std::path::MAIN_SEPARATOR_STR;
1216        let path = file.abs_dir.join(file.file_name);
1217        path.to_str().map(|path| path.replace(MAIN_SEPARATOR_STR, "/"))
1218    }
1219
1220    #[cfg(not(windows))]
1221    fn convert_path(file: File) -> Option<String> {
1222        let path = file.abs_dir.join(file.file_name);
1223        path.to_str().map(str::to_string)
1224    }
1225}