Skip to main content

ex_cli/
finder.rs

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