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
22/// Output format for comparison results.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum OutputFormat {
25    Default,
26    Symbol,
27    Yaml,
28}
29
30use std::path::{Path, PathBuf, StripPrefixError};
31
32pub(crate) fn build_thread_pool(
33    threads: usize,
34) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
35    rayon::ThreadPoolBuilder::new().num_threads(threads).build()
36}
37
38pub(crate) fn human_readable_size(size: u64) -> String {
39    const KB: u64 = 1024;
40    if size < KB {
41        return format!("{} bytes", size);
42    }
43    const KB_AS_F: f64 = KB as f64;
44    let mut size = size as f64;
45    for unit in ["KB", "MB"] {
46        size /= KB_AS_F;
47        if size < KB_AS_F {
48            return format!("{:.1}{}", size, unit);
49        }
50    }
51    format!("{:.1}GB", size / KB_AS_F)
52}
53
54/// Workaround for https://github.com/kojiishi/compare-dir/issues/8
55pub(crate) fn strip_prefix<'a>(path: &'a Path, base: &Path) -> Result<&'a Path, StripPrefixError> {
56    let result = path.strip_prefix(base);
57    #[cfg(windows)]
58    if let Ok(result_path) = result {
59        let result_os_str = result_path.as_os_str();
60        let result_bytes = result_os_str.as_encoded_bytes();
61        if !result_bytes.is_empty() && result_bytes[0] as char == std::path::MAIN_SEPARATOR {
62            // TODO: Use `slice_encoded_bytes` once stabilized.
63            // https://github.com/rust-lang/rust/issues/118485
64            return Ok(Path::new(unsafe {
65                use std::ffi::OsStr;
66                OsStr::from_encoded_bytes_unchecked(&result_bytes[1..])
67            }));
68        }
69    }
70    result
71}
72
73pub(crate) fn common_ancestor(paths: &[impl AsRef<Path>]) -> Option<PathBuf> {
74    if paths.is_empty() {
75        return None;
76    }
77    let mut iter = paths.iter();
78    let mut common = iter.next()?.as_ref().to_path_buf();
79    for path in iter {
80        let path = path.as_ref();
81        let mut new_common = PathBuf::new();
82        for (c, p) in common.components().zip(path.components()) {
83            if c == p {
84                new_common.push(c);
85            } else {
86                break;
87            }
88        }
89        common = new_common;
90        if common.as_os_str().is_empty() {
91            return None;
92        }
93    }
94    Some(common)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn human_readable_size_tests() {
103        assert_eq!(human_readable_size(0), "0 bytes");
104        assert_eq!(human_readable_size(1), "1 bytes");
105        assert_eq!(human_readable_size(1023), "1023 bytes");
106        assert_eq!(human_readable_size(1024), "1.0KB");
107        assert_eq!(human_readable_size(1024 * 1024), "1.0MB");
108        assert_eq!(human_readable_size(1024 * 1024 * 1024), "1.0GB");
109        assert_eq!(human_readable_size(1024 * 1024 * 1024 * 1024), "1024.0GB");
110    }
111
112    #[cfg(windows)]
113    #[test]
114    fn strip_prefix_share_root() -> anyhow::Result<()> {
115        let path = Path::new(r"\\server\share\dir1\dir2");
116        let base = Path::new(r"\\server\share");
117        assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
118        assert_eq!(path.strip_prefix(base)?.to_str().unwrap(), r"dir1\dir2");
119        Ok(())
120    }
121
122    #[cfg(windows)]
123    #[test]
124    fn strip_prefix_unc_root() -> anyhow::Result<()> {
125        let path = Path::new(r"\\?\UNC\server\share\dir1\dir2");
126        let base = Path::new(r"\\?\UNC\server\share");
127        assert_eq!(strip_prefix(path, base)?.to_str().unwrap(), r"dir1\dir2");
128        // assert_eq!(path.strip_prefix(base)?.to_str().unwrap(), r"dir1\dir2");
129        Ok(())
130    }
131
132    #[test]
133    fn common_ancestor_tests() {
134        let empty: &[PathBuf] = &[];
135        assert_eq!(common_ancestor(empty), None);
136
137        let p1 = Path::new("/a/b/c");
138        assert_eq!(common_ancestor(&[p1]), Some(PathBuf::from("/a/b/c")));
139
140        let p2 = Path::new("/a/b/d");
141        assert_eq!(common_ancestor(&[p1, p2]), Some(PathBuf::from("/a/b")));
142
143        let p3 = Path::new("/a/x/y");
144        assert_eq!(common_ancestor(&[p1, p2, p3]), Some(PathBuf::from("/a")));
145
146        let p4 = Path::new("/b/c");
147        assert_eq!(common_ancestor(&[p1, p4]), Some(PathBuf::from("/")));
148
149        // Prefix case
150        let p5 = Path::new("/a/b");
151        assert_eq!(common_ancestor(&[p1, p5]), Some(PathBuf::from("/a/b")));
152
153        // Relative paths (no common root)
154        let r1 = Path::new("a/b");
155        let r2 = Path::new("c/d");
156        assert_eq!(common_ancestor(&[r1, r2]), None);
157
158        // Mixed absolute/relative
159        let a1 = Path::new("/a/b");
160        let r3 = Path::new("a/b");
161        assert_eq!(common_ancestor(&[a1, r3]), None);
162    }
163}