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