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
35impl<'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}