1mod dir_comparer;
2mod file_comparer;
3mod file_hash_cache;
4mod file_hasher;
5mod file_iterator;
6mod progress;
7mod sort_stream;
8mod system_time_ext;
9
10pub use dir_comparer::{DirectoryComparer, FileComparisonMethod};
11pub use file_comparer::{Classification, FileComparer, FileComparisonResult};
12pub(crate) use file_hash_cache::FileHashCache;
13pub use file_hasher::{DuplicatedFiles, FileHasher};
14pub(crate) use file_iterator::FileIterator;
15pub(crate) use progress::Progress;
16pub use progress::ProgressBuilder;
17pub(crate) use sort_stream::sort_stream;
18pub(crate) use system_time_ext::SystemTimeExt;
19
20use std::path::{Path, StripPrefixError};
21
22pub(crate) fn build_thread_pool(
23 threads: usize,
24) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
25 rayon::ThreadPoolBuilder::new().num_threads(threads).build()
26}
27
28pub(crate) fn human_readable_size(size: u64) -> String {
29 const KB: u64 = 1024;
30 if size < KB {
31 return format!("{} bytes", size);
32 }
33 const KB_AS_F: f64 = KB as f64;
34 let mut size = size as f64;
35 for unit in ["KB", "MB"] {
36 size /= KB_AS_F;
37 if size < KB_AS_F {
38 return format!("{:.1}{}", size, unit);
39 }
40 }
41 format!("{:.1}GB", size / KB_AS_F)
42}
43
44pub(crate) fn strip_prefix<'a>(path: &'a Path, base: &Path) -> Result<&'a Path, StripPrefixError> {
46 let result = path.strip_prefix(base);
47 #[cfg(windows)]
48 if let Ok(result_path) = result {
49 let result_os_str = result_path.as_os_str();
50 let result_bytes = result_os_str.as_encoded_bytes();
51 if !result_bytes.is_empty() && result_bytes[0] as char == std::path::MAIN_SEPARATOR {
52 return Ok(Path::new(unsafe {
55 use std::ffi::OsStr;
56 OsStr::from_encoded_bytes_unchecked(&result_bytes[1..])
57 }));
58 }
59 }
60 result
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn human_readable_size_tests() {
69 assert_eq!(human_readable_size(0), "0 bytes");
70 assert_eq!(human_readable_size(1), "1 bytes");
71 assert_eq!(human_readable_size(1023), "1023 bytes");
72 assert_eq!(human_readable_size(1024), "1.0KB");
73 assert_eq!(human_readable_size(1024 * 1024), "1.0MB");
74 assert_eq!(human_readable_size(1024 * 1024 * 1024), "1.0GB");
75 assert_eq!(human_readable_size(1024 * 1024 * 1024 * 1024), "1024.0GB");
76 }
77
78 #[cfg(windows)]
79 #[test]
80 fn strip_prefix_share_root() -> anyhow::Result<()> {
81 let path = Path::new(r"\\server\share\dir1\dir2");
82 let base = Path::new(r"\\server\share");
83 assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
84 assert_eq!(path.strip_prefix(base)?.to_str().unwrap(), r"dir1\dir2");
85 Ok(())
86 }
87
88 #[cfg(windows)]
89 #[test]
90 fn strip_prefix_unc_root() -> anyhow::Result<()> {
91 let path = Path::new(r"\\?\UNC\server\share\dir1\dir2");
92 let base = Path::new(r"\\?\UNC\server\share");
93 assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
94 Ok(())
96 }
97}