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