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