ex_cli/
finder.rs

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