1pub mod full;
2pub mod light;
3
4use 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}
14fn 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 false
36}
37
38#[cfg(test)]
39mod tests_apply_filter {
40 use super::*;
41
42 #[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 let mut results: Vec<EitherOrBoth> = Vec::new();
114
115 for left_entry in &left_entries {
116 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 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 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}