Skip to main content

compare_dir/
lib.rs

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