1mod column_formatter;
2mod dir_comparer;
3mod file_comparer;
4mod file_hash_cache;
5mod file_hasher;
6mod file_iterator;
7mod progress;
8mod sort_stream;
9mod system_time_ext;
10
11pub(crate) use column_formatter::ColumnFormatter;
12pub use dir_comparer::{DirectoryComparer, FileComparisonMethod};
13pub use file_comparer::{Classification, FileComparer, FileComparisonResult};
14pub(crate) use file_hash_cache::FileHashCache;
15pub use file_hasher::{DuplicatedFiles, FileHasher};
16pub(crate) use file_iterator::FileIterator;
17pub(crate) use progress::Progress;
18pub use progress::ProgressBuilder;
19pub(crate) use sort_stream::sort_stream;
20pub(crate) use system_time_ext::SystemTimeExt;
21
22use std::path::{Path, PathBuf, StripPrefixError};
23
24pub(crate) fn build_thread_pool(
25 threads: usize,
26) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
27 rayon::ThreadPoolBuilder::new().num_threads(threads).build()
28}
29
30pub(crate) fn human_readable_size(size: u64) -> String {
31 const KB: u64 = 1024;
32 if size < KB {
33 return format!("{} bytes", size);
34 }
35 const KB_AS_F: f64 = KB as f64;
36 let mut size = size as f64;
37 for unit in ["KB", "MB"] {
38 size /= KB_AS_F;
39 if size < KB_AS_F {
40 return format!("{:.1}{}", size, unit);
41 }
42 }
43 format!("{:.1}GB", size / KB_AS_F)
44}
45
46pub(crate) fn strip_prefix<'a>(path: &'a Path, base: &Path) -> Result<&'a Path, StripPrefixError> {
48 let result = path.strip_prefix(base);
49 #[cfg(windows)]
50 if let Ok(result_path) = result {
51 let result_os_str = result_path.as_os_str();
52 let result_bytes = result_os_str.as_encoded_bytes();
53 if !result_bytes.is_empty() && result_bytes[0] as char == std::path::MAIN_SEPARATOR {
54 return Ok(Path::new(unsafe {
57 use std::ffi::OsStr;
58 OsStr::from_encoded_bytes_unchecked(&result_bytes[1..])
59 }));
60 }
61 }
62 result
63}
64
65pub(crate) fn common_ancestor(paths: &[impl AsRef<Path>]) -> Option<PathBuf> {
66 if paths.is_empty() {
67 return None;
68 }
69 let mut iter = paths.iter();
70 let mut common = iter.next()?.as_ref().to_path_buf();
71 for path in iter {
72 let path = path.as_ref();
73 let mut new_common = PathBuf::new();
74 for (c, p) in common.components().zip(path.components()) {
75 if c == p {
76 new_common.push(c);
77 } else {
78 break;
79 }
80 }
81 common = new_common;
82 if common.as_os_str().is_empty() {
83 return None;
84 }
85 }
86 Some(common)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn human_readable_size_tests() {
95 assert_eq!(human_readable_size(0), "0 bytes");
96 assert_eq!(human_readable_size(1), "1 bytes");
97 assert_eq!(human_readable_size(1023), "1023 bytes");
98 assert_eq!(human_readable_size(1024), "1.0KB");
99 assert_eq!(human_readable_size(1024 * 1024), "1.0MB");
100 assert_eq!(human_readable_size(1024 * 1024 * 1024), "1.0GB");
101 assert_eq!(human_readable_size(1024 * 1024 * 1024 * 1024), "1024.0GB");
102 }
103
104 #[cfg(windows)]
105 #[test]
106 fn strip_prefix_share_root() -> anyhow::Result<()> {
107 let path = Path::new(r"\\server\share\dir1\dir2");
108 let base = Path::new(r"\\server\share");
109 assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
110 assert_eq!(path.strip_prefix(base)?.to_str().unwrap(), r"dir1\dir2");
111 Ok(())
112 }
113
114 #[cfg(windows)]
115 #[test]
116 fn strip_prefix_unc_root() -> anyhow::Result<()> {
117 let path = Path::new(r"\\?\UNC\server\share\dir1\dir2");
118 let base = Path::new(r"\\?\UNC\server\share");
119 assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
120 Ok(())
122 }
123
124 #[test]
125 fn common_ancestor_tests() {
126 let empty: &[PathBuf] = &[];
127 assert_eq!(common_ancestor(empty), None);
128
129 let p1 = Path::new("/a/b/c");
130 assert_eq!(common_ancestor(&[p1]), Some(PathBuf::from("/a/b/c")));
131
132 let p2 = Path::new("/a/b/d");
133 assert_eq!(common_ancestor(&[p1, p2]), Some(PathBuf::from("/a/b")));
134
135 let p3 = Path::new("/a/x/y");
136 assert_eq!(common_ancestor(&[p1, p2, p3]), Some(PathBuf::from("/a")));
137
138 let p4 = Path::new("/b/c");
139 assert_eq!(common_ancestor(&[p1, p4]), Some(PathBuf::from("/")));
140
141 let p5 = Path::new("/a/b");
143 assert_eq!(common_ancestor(&[p1, p5]), Some(PathBuf::from("/a/b")));
144
145 let r1 = Path::new("a/b");
147 let r2 = Path::new("c/d");
148 assert_eq!(common_ancestor(&[r1, r2]), None);
149
150 let a1 = Path::new("/a/b");
152 let r3 = Path::new("a/b");
153 assert_eq!(common_ancestor(&[a1, r3]), None);
154 }
155}