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};
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
54pub(crate) fn common_ancestor(paths: &[impl AsRef<Path>]) -> Option<PathBuf> {
55    if paths.is_empty() {
56        return None;
57    }
58    let mut iter = paths.iter();
59    let mut common = iter.next()?.as_ref().to_path_buf();
60    for path in iter {
61        let path = path.as_ref();
62        let mut new_common = PathBuf::new();
63        for (c, p) in common.components().zip(path.components()) {
64            if c == p {
65                new_common.push(c);
66            } else {
67                break;
68            }
69        }
70        common = new_common;
71        if common.as_os_str().is_empty() {
72            return None;
73        }
74    }
75    Some(common)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn human_readable_size_tests() {
84        assert_eq!(human_readable_size(0), "0 bytes");
85        assert_eq!(human_readable_size(1), "1 bytes");
86        assert_eq!(human_readable_size(1023), "1023 bytes");
87        assert_eq!(human_readable_size(1024), "1.0KB");
88        assert_eq!(human_readable_size(1024 * 1024), "1.0MB");
89        assert_eq!(human_readable_size(1024 * 1024 * 1024), "1.0GB");
90        assert_eq!(human_readable_size(1024 * 1024 * 1024 * 1024), "1024.0GB");
91    }
92
93    #[test]
94    fn common_ancestor_tests() {
95        let empty: &[PathBuf] = &[];
96        assert_eq!(common_ancestor(empty), None);
97
98        let p1 = Path::new("/a/b/c");
99        assert_eq!(common_ancestor(&[p1]), Some(PathBuf::from("/a/b/c")));
100
101        let p2 = Path::new("/a/b/d");
102        assert_eq!(common_ancestor(&[p1, p2]), Some(PathBuf::from("/a/b")));
103
104        let p3 = Path::new("/a/x/y");
105        assert_eq!(common_ancestor(&[p1, p2, p3]), Some(PathBuf::from("/a")));
106
107        let p4 = Path::new("/b/c");
108        assert_eq!(common_ancestor(&[p1, p4]), Some(PathBuf::from("/")));
109
110        // Prefix case
111        let p5 = Path::new("/a/b");
112        assert_eq!(common_ancestor(&[p1, p5]), Some(PathBuf::from("/a/b")));
113
114        // Relative paths (no common root)
115        let r1 = Path::new("a/b");
116        let r2 = Path::new("c/d");
117        assert_eq!(common_ancestor(&[r1, r2]), None);
118
119        // Mixed absolute/relative
120        let a1 = Path::new("/a/b");
121        let r3 = Path::new("a/b");
122        assert_eq!(common_ancestor(&[a1, r3]), None);
123    }
124}