dir_cmp/
lib.rs

1pub mod full;
2pub mod light;
3
4//use log::debug;
5use regex::Regex;
6use std::fs;
7use std::{io, path::PathBuf};
8
9#[derive(Debug)]
10pub enum Filter {
11    Exclude(Vec<Regex>),
12    Include(Vec<Regex>),
13}
14//returns true if the path should be filtered out
15fn apply_filter(path: &str, filter_opt: &Option<Filter>) -> bool {
16    if let Some(filter) = filter_opt {
17        match filter {
18            Filter::Exclude(pattern_list) => {
19                for pattern in pattern_list {
20                    if pattern.is_match(path) {
21                        return true;
22                    }
23                }
24            }
25            Filter::Include(pattern_list) => {
26                for pattern in pattern_list {
27                    if !pattern.is_match(path) {
28                        return true;
29                    }
30                }
31            }
32        }
33    }
34    //default if no filter values are provided
35    false
36}
37
38#[cfg(test)]
39mod tests_apply_filter {
40    use super::*;
41
42    // fn init() {
43    //     let _ = env_logger::builder().is_test(true).try_init();
44    // }
45
46    #[test]
47    fn empty() {
48        let path = ".git/config";
49        let filter = Some(Filter::Include(Vec::new()));
50
51        assert!(!apply_filter(path, &filter));
52    }
53
54    #[test]
55    fn none() {
56        let path = ".git/config";
57        let filter = None;
58
59        assert!(!apply_filter(path, &filter));
60    }
61
62    #[test]
63    fn include() {
64        let path = "src/main.rs";
65        let regex = Regex::new(r".rs").unwrap();
66        let filter = Some(Filter::Include(vec![regex]));
67
68        assert!(!apply_filter(path, &filter));
69    }
70
71    #[test]
72    fn exclude() {
73        let path = ".git/config";
74        let regex = Regex::new(".git").unwrap();
75        let filter = Some(Filter::Exclude(vec![regex]));
76
77        assert!(apply_filter(path, &filter));
78    }
79}
80
81#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
82pub enum EitherOrBoth {
83    Both(PathBuf, PathBuf),
84    Left(PathBuf),
85    Right(PathBuf),
86}
87
88fn zip_dir_entries(
89    left_dir: &PathBuf,
90    right_dir: &PathBuf,
91    left_base: &str,
92    right_base: &str,
93    filter: &Option<Filter>,
94) -> io::Result<Vec<EitherOrBoth>> {
95    let left_read_dir = fs::read_dir(left_dir)?;
96    let right_read_dir = fs::read_dir(right_dir)?;
97
98    let left_entries = left_read_dir
99        .map(|res| res.map(|e| e.path()))
100        .collect::<Result<Vec<_>, io::Error>>()
101        .expect("some error with left dir");
102
103    let right_entries = right_read_dir
104        .map(|res| res.map(|e| e.path()))
105        .collect::<Result<Vec<_>, io::Error>>()
106        .expect("some error with right dir");
107
108    // The order in which `read_dir` returns entries is not guaranteed. If reproducible
109    // ordering is required the entries should be explicitly sorted.
110
111    //left_entries.sort();
112    //right_entries.sort();
113    let mut results: Vec<EitherOrBoth> = Vec::new();
114
115    for left_entry in &left_entries {
116        //debug!("left entry: {:?}", left_entry);
117        let left_short_path = left_entry.strip_prefix(left_base).unwrap();
118        if !apply_filter(left_short_path.to_str().unwrap(), filter) {
119            let mut found_match = None;
120            for right_entry in &right_entries {
121                let right_short_path = right_entry.strip_prefix(right_base).unwrap();
122                if left_short_path == right_short_path {
123                    found_match = Some(EitherOrBoth::Both(
124                        left_entry.to_owned(),
125                        right_entry.to_owned(),
126                    ));
127                }
128            }
129
130            match found_match {
131                Some(both) => results.push(both),
132                None => results.push(EitherOrBoth::Left(left_entry.to_owned())),
133            }
134        }
135    }
136
137    for right_entry in &right_entries {
138        let right_short_path = right_entry.strip_prefix(right_base).unwrap();
139        if !apply_filter(right_short_path.to_str().unwrap(), filter) {
140            let mut found_match = None;
141            for left_entry in &left_entries {
142                let left_short_path = left_entry.strip_prefix(left_base).unwrap();
143                if left_short_path == right_short_path {
144                    found_match = Some(());
145                }
146            }
147
148            if found_match.is_none() {
149                results.push(EitherOrBoth::Right(right_entry.to_owned()));
150            }
151        }
152    }
153
154    Ok(results)
155}
156
157#[cfg(test)]
158mod tests_zip_dir_entries {
159    use super::*;
160    use std::fs;
161
162    fn create_temp_dir() -> tempfile::TempDir {
163        tempfile::Builder::new()
164            .prefix("compare_zip_dirs_")
165            .tempdir()
166            .unwrap()
167    }
168
169    fn init() {
170        let _ = env_logger::builder().is_test(true).try_init();
171    }
172
173    #[test]
174    fn emtpy() {
175        init();
176        let left_dir = create_temp_dir();
177        let left_path_buf = left_dir.into_path();
178        let left_base = left_path_buf.to_str().unwrap();
179
180        let right_dir = create_temp_dir();
181        let right_path_buf = right_dir.into_path();
182        let right_base = right_path_buf.to_str().unwrap();
183
184        let result = zip_dir_entries(
185            &left_path_buf,
186            &right_path_buf,
187            left_base,
188            right_base,
189            &None,
190        )
191        .unwrap();
192
193        assert_eq!(result, Vec::<EitherOrBoth>::new());
194    }
195
196    #[test]
197    fn both() {
198        init();
199        let left_dir = create_temp_dir();
200        let left_file = left_dir.path().join("file1");
201        fs::write(left_file.as_path(), b"Hello, world!").unwrap();
202        let left_path_buf = left_dir.into_path();
203        let left_base = left_path_buf.to_str().unwrap();
204
205        let right_dir = create_temp_dir();
206        let right_file = right_dir.path().join("file1");
207        fs::write(right_file.as_path(), b"Hello, world!").unwrap();
208        let right_path_buf = right_dir.into_path();
209        let right_base = right_path_buf.to_str().unwrap();
210
211        let result = zip_dir_entries(
212            &left_path_buf,
213            &right_path_buf,
214            left_base,
215            right_base,
216            &None,
217        )
218        .unwrap();
219
220        assert_eq!(result, vec![EitherOrBoth::Both(left_file, right_file)]);
221    }
222
223    #[test]
224    fn both_subdir() {
225        init();
226        let left_dir = create_temp_dir();
227        let left_sub_dir = left_dir.path().join("subdir");
228        fs::create_dir(left_sub_dir.as_path()).unwrap();
229        let left_file = left_sub_dir.as_path().join("file1");
230        fs::write(left_file.as_path(), b"Hello, world!").unwrap();
231        let left_base = left_dir.path().to_str().unwrap();
232
233        let right_dir = create_temp_dir();
234        let right_sub_dir = right_dir.path().join("subdir");
235        fs::create_dir(right_sub_dir.as_path()).unwrap();
236        let right_file = right_sub_dir.as_path().join("file1");
237        fs::write(right_file.as_path(), b"Hello, world!").unwrap();
238        let right_base = right_dir.path().to_str().unwrap();
239
240        let result =
241            zip_dir_entries(&left_sub_dir, &right_sub_dir, left_base, right_base, &None).unwrap();
242
243        assert_eq!(result, vec![EitherOrBoth::Both(left_file, right_file)]);
244    }
245    #[test]
246    fn left() {
247        init();
248        let left_dir = create_temp_dir();
249        let left_file = left_dir.path().join("file1");
250        fs::write(left_file.as_path(), b"Hello, world!").unwrap();
251        let left_path_buf = left_dir.into_path();
252        let left_base = left_path_buf.to_str().unwrap();
253
254        let right_dir = create_temp_dir();
255        let right_path_buf = right_dir.into_path();
256        let right_base = right_path_buf.to_str().unwrap();
257
258        let result = zip_dir_entries(
259            &left_path_buf,
260            &right_path_buf,
261            left_base,
262            right_base,
263            &None,
264        )
265        .unwrap();
266        assert_eq!(result, vec![EitherOrBoth::Left(left_file)]);
267    }
268    #[test]
269    fn right() {
270        init();
271        let left_dir = create_temp_dir();
272        let left_path_buf = left_dir.into_path();
273        let left_base = left_path_buf.to_str().unwrap();
274
275        let right_dir = create_temp_dir();
276        let right_file = right_dir.path().join("file1");
277        fs::write(right_file.as_path(), b"Hello, world!").unwrap();
278        let right_path_buf = right_dir.into_path();
279        let right_base = right_path_buf.to_str().unwrap();
280
281        let result = zip_dir_entries(
282            &left_path_buf,
283            &right_path_buf,
284            left_base,
285            right_base,
286            &None,
287        )
288        .unwrap();
289
290        assert_eq!(result, vec![EitherOrBoth::Right(right_file)]);
291    }
292}
293
294fn list_files(path: &PathBuf) -> Vec<PathBuf> {
295    let mut result: Vec<PathBuf> = Vec::new();
296
297    let read_dir = fs::read_dir(path).unwrap();
298
299    let dir_entries = read_dir
300        .map(|res| res.map(|e| e.path()))
301        .collect::<Result<Vec<_>, io::Error>>()
302        .expect("some error with left dir");
303    for entry in dir_entries {
304        if entry.is_dir() {
305            //get elements from sub dirs
306            let mut subtree_results = list_files(&entry);
307            result.append(&mut subtree_results);
308            continue;
309        }
310        if entry.is_file() {
311            result.push(entry);
312            continue;
313        }
314        if entry.is_symlink() {
315            //ignore
316            continue;
317        }
318    }
319    result
320}
321
322#[derive(Debug)]
323pub struct Options {
324    pub ignore_equal: bool,
325    pub ignore_left_only: bool,
326    pub ignore_right_only: bool,
327    pub filter: Option<Filter>,
328    pub recursive: bool,
329}
330
331#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
332pub enum FileCompResult {
333    Equal,
334    Different,
335}
336fn compare_two_files(left_path: &PathBuf, right_path: &PathBuf) -> io::Result<FileCompResult> {
337    let left_file = fs::read(left_path)?;
338    let right_file = fs::read(right_path)?;
339
340    if left_file == right_file {
341        Ok(FileCompResult::Equal)
342    } else {
343        Ok(FileCompResult::Different)
344    }
345}