Skip to main content

compare_dir/
lib.rs

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, 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
46/// Workaround for https://github.com/kojiishi/compare-dir/issues/8
47pub(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            // TODO: Use `slice_encoded_bytes` once stabilized.
55            // https://github.com/rust-lang/rust/issues/118485
56            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
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn human_readable_size_tests() {
71        assert_eq!(human_readable_size(0), "0 bytes");
72        assert_eq!(human_readable_size(1), "1 bytes");
73        assert_eq!(human_readable_size(1023), "1023 bytes");
74        assert_eq!(human_readable_size(1024), "1.0KB");
75        assert_eq!(human_readable_size(1024 * 1024), "1.0MB");
76        assert_eq!(human_readable_size(1024 * 1024 * 1024), "1.0GB");
77        assert_eq!(human_readable_size(1024 * 1024 * 1024 * 1024), "1024.0GB");
78    }
79
80    #[cfg(windows)]
81    #[test]
82    fn strip_prefix_share_root() -> anyhow::Result<()> {
83        let path = Path::new(r"\\server\share\dir1\dir2");
84        let base = Path::new(r"\\server\share");
85        assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
86        assert_eq!(path.strip_prefix(base)?.to_str().unwrap(), r"dir1\dir2");
87        Ok(())
88    }
89
90    #[cfg(windows)]
91    #[test]
92    fn strip_prefix_unc_root() -> anyhow::Result<()> {
93        let path = Path::new(r"\\?\UNC\server\share\dir1\dir2");
94        let base = Path::new(r"\\?\UNC\server\share");
95        assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
96        // assert_eq!(path.strip_prefix(base)?.to_str().unwrap(), r"dir1\dir2");
97        Ok(())
98    }
99}