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_item;
7mod file_iterator;
8mod progress;
9mod sort_stream;
10mod system_time_ext;
11
12pub(crate) use column_formatter::ColumnFormatter;
13pub use dir_comparer::{DirectoryComparer, FileComparisonMethod};
14pub use file_comparer::{Classification, FileComparer, FileComparisonResult};
15pub(crate) use file_hash_cache::FileHashCache;
16pub use file_hasher::{DuplicatedFiles, FileHasher};
17pub use file_item::FileItem;
18pub(crate) use file_iterator::FileIterator;
19pub(crate) use progress::Progress;
20pub use progress::ProgressBuilder;
21pub(crate) use progress::ProgressValue;
22pub(crate) use progress::SharedProgress;
23pub(crate) use sort_stream::sort_stream;
24pub(crate) use system_time_ext::SystemTimeExt;
25
26/// Output format for comparison results.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum OutputFormat {
29    Default,
30    Symbol,
31    Yaml,
32    Shell,
33    PowerShell,
34}
35
36/// Mode for checking files against the cache.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum CheckMode {
39    /// Same as `!update` (does not update cache unless size is 0 and modified time is unchanged).
40    Check,
41    /// Update cache if metadata is changed or new, otherwise no-op.
42    Update,
43    /// Same as `update` (always recomputes hashes and updates cache).
44    UpdateAll,
45}
46
47use std::path::{Path, PathBuf};
48
49pub(crate) fn build_thread_pool(
50    threads: usize,
51) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
52    rayon::ThreadPoolBuilder::new().num_threads(threads).build()
53}
54
55pub(crate) fn human_readable_size(size: u64) -> String {
56    const KB: u64 = 1024;
57    if size < KB {
58        return format!("{} bytes", size);
59    }
60    const KB_AS_F: f64 = KB as f64;
61    let mut size = size as f64;
62    for unit in ["KB", "MB"] {
63        size /= KB_AS_F;
64        if size < KB_AS_F {
65            return format!("{:.1}{}", size, unit);
66        }
67    }
68    format!("{:.1}GB", size / KB_AS_F)
69}
70
71pub(crate) fn common_ancestor(paths: &[impl AsRef<Path>]) -> Option<PathBuf> {
72    if paths.is_empty() {
73        return None;
74    }
75    let mut iter = paths.iter();
76    let mut common = iter.next()?.as_ref().to_path_buf();
77    for path in iter {
78        let path = path.as_ref();
79        let mut new_common = PathBuf::new();
80        for (c, p) in common.components().zip(path.components()) {
81            if c == p {
82                new_common.push(c);
83            } else {
84                break;
85            }
86        }
87        common = new_common;
88        if common.as_os_str().is_empty() {
89            return None;
90        }
91    }
92    Some(common)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn human_readable_size_tests() {
101        assert_eq!(human_readable_size(0), "0 bytes");
102        assert_eq!(human_readable_size(1), "1 bytes");
103        assert_eq!(human_readable_size(1023), "1023 bytes");
104        assert_eq!(human_readable_size(1024), "1.0KB");
105        assert_eq!(human_readable_size(1024 * 1024), "1.0MB");
106        assert_eq!(human_readable_size(1024 * 1024 * 1024), "1.0GB");
107        assert_eq!(human_readable_size(1024 * 1024 * 1024 * 1024), "1024.0GB");
108    }
109
110    #[test]
111    fn common_ancestor_tests() {
112        let empty: &[PathBuf] = &[];
113        assert_eq!(common_ancestor(empty), None);
114
115        let p1 = Path::new("/a/b/c");
116        assert_eq!(common_ancestor(&[p1]), Some(PathBuf::from("/a/b/c")));
117
118        let p2 = Path::new("/a/b/d");
119        assert_eq!(common_ancestor(&[p1, p2]), Some(PathBuf::from("/a/b")));
120
121        let p3 = Path::new("/a/x/y");
122        assert_eq!(common_ancestor(&[p1, p2, p3]), Some(PathBuf::from("/a")));
123
124        let p4 = Path::new("/b/c");
125        assert_eq!(common_ancestor(&[p1, p4]), Some(PathBuf::from("/")));
126
127        // Prefix case
128        let p5 = Path::new("/a/b");
129        assert_eq!(common_ancestor(&[p1, p5]), Some(PathBuf::from("/a/b")));
130
131        // Relative paths (no common root)
132        let r1 = Path::new("a/b");
133        let r2 = Path::new("c/d");
134        assert_eq!(common_ancestor(&[r1, r2]), None);
135
136        // Mixed absolute/relative
137        let a1 = Path::new("/a/b");
138        let r3 = Path::new("a/b");
139        assert_eq!(common_ancestor(&[a1, r3]), None);
140    }
141}