1use crate::hasher::{hash_file, HashType};
2use anyhow::{anyhow, Error};
3use bytes::BytesMut;
4use chrono::Utc;
5use rand::{thread_rng, Rng};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs::File;
9use std::io::Write;
10use std::path::Path;
11use std::sync::{Arc, Mutex};
12use std::thread::JoinHandle;
13use std::{env, fs, thread};
14use walkdir::DirEntry;
15
16#[derive(Debug, Clone)]
17pub struct Snapshot {
18 pub file_hashes: Arc<Mutex<HashMap<String, FileMetadata>>>,
19 pub black_list: Vec<String>,
20 pub root_path: String,
21 pub hash_type: HashType,
22 pub uuid: String,
23 pub date_created: i64,
24}
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct FileMetadata {
27 pub path: String,
28 pub check_sum: Vec<u8>,
29 pub size: u64,
30 pub ino: u64,
31 pub ctime: i64,
32 pub mtime: i64,
33}
34
35impl Default for FileMetadata {
36 fn default() -> Self {
37 FileMetadata {
38 path: "".to_string(),
39 check_sum: vec![],
40 size: 0,
41 ino: 0,
42 ctime: 0,
43 mtime: 0,
44 }
45 }
46}
47
48impl Snapshot {
49 pub fn new(
50 path: &Path,
51 hash_type: HashType,
52 black_list: Vec<String>,
53 verbose: bool,
54 ) -> Result<Snapshot, Error> {
55 let root_path = match path.to_str() {
56 None => "".to_string(),
57 Some(p) => p.to_string(),
58 };
59 let mut rand = thread_rng();
60 let uuid_int: u128 = rand.gen();
61 let uuid = uuid_int.to_string();
62 if verbose {
63 println!("Walking Directory: {}", path.to_str().unwrap());
64 }
65
66 let file_paths = walkdir::WalkDir::new(path).sort_by_file_name();
67 let file_hashes: Arc<Mutex<HashMap<String, FileMetadata>>> =
68 Arc::new(Mutex::new(HashMap::new()));
69 let mut pool: Vec<JoinHandle<()>> = vec![];
70
71 let mut paths: Vec<Option<DirEntry>> = vec![];
72 file_paths
73 .into_iter()
74 .flatten()
75 .for_each(|a| paths.push(Option::from(a)));
76
77 if verbose {
78 println!("Skipping (Blacklisted): {:?}", black_list);
79 }
80
81 while !paths.is_empty() {
82 #[allow(clippy::collapsible_match)]
83 if let Some(p) = paths.pop() {
84 if let Some(p) = p {
85 let mut blacklisted = false;
86 black_list.iter().for_each(|bl| {
87 if let Some(a) = p.path().to_str() {
88 if a.starts_with(bl) {
89 blacklisted = true;
90 }
91 }
92 });
93
94 if p.path().is_file() && !blacklisted {
95 let bind = file_hashes.clone();
96 let handle = thread::spawn(move || {
97 let mut binding = bind.lock();
98 let ht = binding.as_mut().expect("binding error");
99 if let Err(e) = hash_file(p.path(), ht, hash_type, verbose) {
100 println!("Warning: {e}")
101 }
102 });
103 pool.push(handle);
104 if pool.len() > 4 {
105 if let Some(handle) = pool.pop() {
106 handle.join().expect("could not join handle")
107 }
108 }
109 }
110 }
111 }
112 }
113
114 for handle in pool {
115 handle.join().expect("could not join handle")
116 }
117
118 Ok(Snapshot {
119 file_hashes,
120 black_list,
121 root_path,
122 hash_type,
123 uuid,
124 date_created: Utc::now().timestamp(),
125 })
126 }
127}
128
129impl Default for Snapshot {
130 fn default() -> Self {
131 let black_list: Vec<String> = vec![];
132 Snapshot {
133 file_hashes: Arc::new(Mutex::new(HashMap::new())),
134 black_list,
135 root_path: "".to_string(),
136 hash_type: HashType::BLAKE3,
137 uuid: "".to_string(),
138 date_created: 0,
139 }
140 }
141}
142
143#[derive(Debug)]
144pub enum SnapshotChangeType {
145 None,
146 Created,
147 Deleted,
148 Changed,
149}
150
151#[derive(Debug)]
152pub struct SnapshotCompareResult {
153 pub created: Vec<String>,
154 pub deleted: Vec<String>,
155 pub changed: Vec<String>,
156}
157
158pub fn compare_hashes(
159 left: Snapshot,
160 right: Snapshot,
161 verbose: bool,
162) -> Option<(SnapshotChangeType, SnapshotCompareResult)> {
163 #[allow(unused)]
164 let success = true;
165 let mut created: Vec<String> = vec![];
166 let mut deleted: Vec<String> = vec![];
167 let mut changed: Vec<String> = vec![];
168
169 if let Ok(left_lock) = left.file_hashes.lock() {
170 for left_entry in left_lock.iter() {
172 if let Ok(curr_lock) = right.file_hashes.lock() {
173 match curr_lock.get(left_entry.0) {
174 Some(right_entry) => {
176 if !right_entry.check_sum.eq(&left_entry.1.check_sum) {
177 changed.push(right_entry.path.to_string());
178 }
179 }
180 None => {
182 deleted.push(left_entry.0.to_string());
183 }
184 }
185 }
186 }
187 }
188 if let Ok(e) = right.file_hashes.lock() {
190 for right_entry in e.iter() {
191 if left.file_hashes.lock().ok()?.get(right_entry.0).is_none() {
192 created.push(right_entry.0.to_string());
193 }
194 }
195 }
196
197 let mut return_type = SnapshotChangeType::None;
198 if !created.is_empty() {
199 return_type = SnapshotChangeType::Created;
200 }
201 if !deleted.is_empty() {
202 return_type = SnapshotChangeType::Deleted;
203 }
204 if !changed.is_empty() {
205 return_type = SnapshotChangeType::Changed;
206 }
207
208 Some((
209 return_type,
210 SnapshotCompareResult {
211 created,
212 deleted,
213 changed,
214 },
215 ))
216}
217
218pub fn compare_hashes_and_modify_date(
219 left: Snapshot,
220 right: Snapshot,
221) -> Option<(SnapshotChangeType, SnapshotCompareResult)> {
222 #[allow(unused)]
223 let success = true;
224 let mut created: Vec<String> = vec![];
225 let mut deleted: Vec<String> = vec![];
226 let mut changed: Vec<String> = vec![];
227
228 if let Ok(left_lock) = left.file_hashes.lock() {
229 for left_entry in left_lock.iter() {
231 if let Ok(curr_lock) = right.file_hashes.lock() {
232 match curr_lock.get(left_entry.0) {
233 Some(right_entry) => {
235 if !right_entry.check_sum.eq(&left_entry.1.check_sum) {
237 changed.push(right_entry.path.to_string());
238 }
239 if !right_entry.mtime.eq(&left_entry.1.mtime)
241 || !right_entry.ctime.eq(&left_entry.1.ctime)
242 {
243 changed.push(right_entry.path.to_string());
244 }
245 }
246 None => {
248 deleted.push(left_entry.0.to_string());
249 }
250 }
251 }
252 }
253 }
254
255 if let Ok(e) = right.file_hashes.lock() {
257 for right_entry in e.iter() {
258 if left.file_hashes.lock().ok()?.get(right_entry.0).is_none() {
259 created.push(right_entry.0.to_string());
260 }
261 }
262 }
263
264 let mut return_type = SnapshotChangeType::None;
265 if !created.is_empty() {
266 return_type = SnapshotChangeType::Created;
267 }
268 if !deleted.is_empty() {
269 return_type = SnapshotChangeType::Deleted;
270 }
271 if !changed.is_empty() {
272 return_type = SnapshotChangeType::Changed;
273 }
274
275 Some((
276 return_type,
277 SnapshotCompareResult {
278 created,
279 deleted,
280 changed,
281 },
282 ))
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286struct SerializableSnapshot {
287 pub file_hashes: Vec<FileMetadata>,
288 pub root_path: String,
289 pub hash_type: HashType,
290 pub uuid: String,
291 pub date_created: i64,
292}
293
294fn path_resolve(path: String) -> String {
295 #[allow(unused)]
296 let mut full_path = String::new();
297 if path.starts_with("./") {
298 let mut cur_dir: String = match env::current_dir() {
299 Ok(pb) => match pb.to_str() {
300 None => String::new(),
301 Some(str) => str.to_string(),
302 },
303 Err(_) => String::new(),
304 };
305 cur_dir.push('/');
306 full_path = path.replace("./", cur_dir.as_str());
307 } else {
308 full_path = path.to_string();
309 }
310 full_path
311}
312
313pub fn export(snapshot: Snapshot, path: String, overwrite: bool, verbose: bool) -> Result<(), Error> {
314 let full_path = path_resolve(path);
315
316 let mut fh: Vec<FileMetadata> = vec![];
317
318 if let Ok(unlocked) = snapshot.file_hashes.lock() {
319 for entry in unlocked.iter() {
320 fh.push(entry.1.clone())
321 }
322 }
323
324 let serializable = SerializableSnapshot {
325 file_hashes: fh,
326 root_path: snapshot.root_path,
327 hash_type: snapshot.hash_type,
328 uuid: snapshot.uuid,
329 date_created: snapshot.date_created,
330 };
331
332 let serialized = serde_json::to_string(&serializable)?;
333 let filename = full_path
335 .split('/')
336 .last()
337 .expect("unable to get full path");
338 let path_only = full_path.replace(filename, "");
339 if Path::new(&full_path).exists() && overwrite {
343 fs::remove_file(&full_path)?;
344 write_to_file(path_only, full_path, serialized)?
345 } else if !Path::new(&full_path).exists() {
346 write_to_file(path_only, full_path, serialized)?
347 };
348 Ok(())
349}
350
351fn write_to_file(path_only: String, full_path: String, serialized: String) -> Result<(), Error> {
352 if fs::create_dir_all(&path_only).is_ok() {
353 if let Ok(mut file_handle) = File::create(full_path) {
354 Ok(file_handle.write_all(serialized.as_bytes())?)
355 } else {
356 Err(anyhow!("Unable to write to path: {}", path_only.clone()))
357 }
358 } else {
359 Err(anyhow!("Unable to write to path: {}", path_only.clone()))
360 }
361}
362
363pub fn import(path: String, verbose: bool) -> Result<Snapshot, Error> {
364 #[allow(unused)]
365 let buffer = BytesMut::new();
366 let full_path = path_resolve(path);
367 if let Ok(bytes) = fs::read(full_path) {
368 let snapshot = serde_json::from_slice::<SerializableSnapshot>(&bytes)?;
369
370 let mut fh: HashMap<String, FileMetadata> = HashMap::new();
371
372 if verbose {
373 for entry in snapshot.file_hashes {
374 if let Some(_res) = fh.insert(entry.path.clone(), entry.clone()) {
375 println!("successfully imported: {}", entry.path);
376 }
377 }
378 }
379
380
381 let black_list: Vec<String> = vec![];
382 Ok(Snapshot {
383 file_hashes: Arc::new(Mutex::new(fh)),
384 black_list,
385 root_path: snapshot.root_path,
386 hash_type: snapshot.hash_type,
387 uuid: snapshot.uuid,
388 date_created: snapshot.date_created,
389 })
390 } else {
391 Ok(Snapshot::default())
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use crate::{compare_snapshots, compare_snapshots_including_modify_date};
399 use std::fs;
400 use std::fs::File;
401 use std::path::Path;
402
403 #[test]
404 fn dangerous() {
405 let mut snap = Snapshot::new(
406 Path::new("/proc"),
407 HashType::BLAKE3,
408 vec![
409 "testkey".to_string(),
410 "/dev".to_string(),
411 "/proc".to_string(),
412 "/tmp".to_string(),
413 ],
414 true,
415 );
416 assert!(snap.is_ok());
417
418 let mut snap = Snapshot::new(
419 Path::new("/dev"),
420 HashType::BLAKE3,
421 vec![
422 "testkey".to_string(),
423 "/dev".to_string(),
424 "/proc".to_string(),
425 "/tmp".to_string(),
426 ],
427 true,
428 );
429 assert!(snap.is_ok());
430
431 let mut snap = Snapshot::new(
432 Path::new("/tmp"),
433 HashType::BLAKE3,
434 vec![
435 "testkey".to_string(),
436 "/dev".to_string(),
437 "/proc".to_string(),
438 "/tmp".to_string(),
439 ],
440 true,
441 );
442 assert!(snap.is_ok());
443 }
444
445 #[test]
446 fn blacklist() {
447 let mut snap = Snapshot::new(
448 Path::new("/etc"),
449 HashType::BLAKE3,
450 vec!["testkey".to_string()],
451 true,
452 )
453 .unwrap();
454 println!("{:#?}", snap.clone().black_list);
455 assert_eq!(snap.black_list.pop().unwrap(), "testkey".to_string());
456 }
457
458 #[test]
459 fn create_snapshot_blake3() {
460 let test_snap_b3 = Snapshot::new(Path::new("/etc"), HashType::BLAKE3, vec![], false);
461 assert!(test_snap_b3.unwrap().file_hashes.lock().unwrap().len() > 0);
462 }
463 #[test]
464 fn create_snapshot_md5() {
465 let test_snap_md5 = Snapshot::new(Path::new("/etc"), HashType::MD5, vec![], true);
466 assert!(test_snap_md5.unwrap().file_hashes.lock().unwrap().len() > 0);
467 }
468 #[test]
469 fn create_snapshot_sha3() {
470 let test_snap_sha3 = Snapshot::new(Path::new("/etc"), HashType::SHA3, vec![], true);
471 assert!(test_snap_sha3.unwrap().file_hashes.lock().unwrap().len() > 0);
472 }
473
474 #[test]
475 fn export_snapshot() {
476 assert!(!Path::new("./target/build/out.snapshot").exists());
477 let test_snap_export = Snapshot::new(Path::new("/etc"), HashType::BLAKE3, vec![], true);
478 let _ = export(
479 test_snap_export.unwrap().clone(),
480 "./target/build/out.snapshot".to_string(),
481 true,
482 true,
483 );
484 assert!(Path::new("./target/build/out.snapshot").exists());
485 fs::remove_file(Path::new("./target/build/out.snapshot")).unwrap();
486 }
487
488 #[test]
489 fn import_snapshot() {
490 let test_snap_import = Snapshot::new(Path::new("/etc"), HashType::BLAKE3, vec![], true);
491 let _ = export(
492 test_snap_import.unwrap(),
493 "./target/build/in.snapshot".to_string(),
494 true,
495 true,
496 );
497 let snapshot = import("./target/build/in.snapshot".to_string(), true);
498 assert!(snapshot.unwrap().file_hashes.lock().unwrap().len() > 0);
499 fs::remove_file(Path::new("./target/build/in.snapshot")).unwrap();
500 }
501
502 #[test]
503 fn creation_detection() {
504 assert!(!Path::new("./target/build/test_creation/").exists());
505 fs::create_dir_all(Path::new("./target/build/test_creation/")).unwrap();
506 let test_snap_creation_1 = Snapshot::new(
507 Path::new("./target/build/test_creation/"),
508 HashType::BLAKE3,
509 vec![],
510 true,
511 );
512 File::create(Path::new("./target/build/test_creation/test1")).unwrap();
513 File::create(Path::new("./target/build/test_creation/test2")).unwrap();
514 File::create(Path::new("./target/build/test_creation/test3")).unwrap();
515 let test_snap_creation_2 = Snapshot::new(
516 Path::new("./target/build/test_creation/"),
517 HashType::BLAKE3,
518 vec![],
519 true,
520 );
521 assert_eq!(
522 compare_snapshots(test_snap_creation_1.unwrap(), test_snap_creation_2.unwrap(), true)
523 .unwrap()
524 .1
525 .created
526 .len(),
527 3
528 );
529 fs::remove_dir_all(Path::new("./target/build/test_creation/")).unwrap();
530 }
531
532 #[test]
533 fn deletion_detection() {
534 assert!(!Path::new("./target/build/test_deletion/").exists());
535 fs::create_dir_all(Path::new("./target/build/test_deletion/")).unwrap();
536 let test_snap_deletion_1 = Snapshot::new(
537 Path::new("./target/build/test_deletion/"),
538 HashType::BLAKE3,
539 vec![],
540 true,
541 );
542 File::create(Path::new("./target/build/test_deletion/test1")).unwrap();
543 File::create(Path::new("./target/build/test_deletion/test2")).unwrap();
544 File::create(Path::new("./target/build/test_deletion/test3")).unwrap();
545 let test_snap_deletion_2 = Snapshot::new(
546 Path::new("./target/build/test_deletion/"),
547 HashType::BLAKE3,
548 vec![],
549 true,
550 );
551 assert_eq!(
552 compare_snapshots(test_snap_deletion_2.unwrap(), test_snap_deletion_1.unwrap(), true)
553 .unwrap()
554 .1
555 .deleted
556 .len(),
557 3
558 );
559 fs::remove_dir_all(Path::new("./target/build/test_deletion/")).unwrap();
560 }
561
562 #[test]
563 fn change_detection() {
564 assert!(!Path::new("./target/build/test_change/").exists());
565 fs::create_dir_all(Path::new("./target/build/test_change/")).unwrap();
566 let mut file1 = File::create(Path::new("./target/build/test_change/test1")).unwrap();
567 let mut file2 = File::create(Path::new("./target/build/test_change/test2")).unwrap();
568 let mut file3 = File::create(Path::new("./target/build/test_change/test3")).unwrap();
569 let test_snap_change_1 = Snapshot::new(
570 Path::new("./target/build/test_change/"),
571 HashType::BLAKE3,
572 vec![],
573 true,
574 );
575 file1.write_all("file1".as_bytes()).unwrap();
576 file2.write_all("file2".as_bytes()).unwrap();
577 file3.write_all("file3".as_bytes()).unwrap();
578 let test_snap_change_2 = Snapshot::new(
579 Path::new("./target/build/test_change/"),
580 HashType::BLAKE3,
581 vec![],
582 true,
583 );
584 assert_eq!(
585 compare_snapshots(test_snap_change_1.unwrap(), test_snap_change_2.unwrap(), true)
586 .unwrap()
587 .1
588 .changed
589 .len(),
590 3
591 );
592 fs::remove_dir_all(Path::new("./target/build/test_change/")).unwrap();
593 }
594
595 #[test]
596 fn change_detection_including_modify_date() {
597 assert!(!Path::new("./target/build/test_change_modify/").exists());
598 fs::create_dir_all(Path::new("./target/build/test_change_modify/")).unwrap();
599 let _ = File::create(Path::new("./target/build/test_change_modify/test1")).unwrap();
600 let _ = File::create(Path::new("./target/build/test_change_modify/test2")).unwrap();
601 let _ = File::create(Path::new("./target/build/test_change_modify/test3")).unwrap();
602 let test_snap_change_1 = Snapshot::new(
603 Path::new("./target/build/test_change_modify/"),
604 HashType::BLAKE3,
605 vec![],
606 true,
607 );
608 let _ = fs::remove_file(Path::new("./target/build/test_change_modify/test1")).unwrap();
609 let _ = fs::remove_file(Path::new("./target/build/test_change_modify/test2")).unwrap();
610 let _ = fs::remove_file(Path::new("./target/build/test_change_modify/test3")).unwrap();
611
612 let _ = File::create(Path::new("./target/build/test_change_modify/test1")).unwrap();
613 let _ = File::create(Path::new("./target/build/test_change_modify/test2")).unwrap();
614 let _ = File::create(Path::new("./target/build/test_change_modify/test3")).unwrap();
615 let test_snap_change_2 = Snapshot::new(
616 Path::new("./target/build/test_change_modify/"),
617 HashType::BLAKE3,
618 vec![],
619 true,
620 );
621 assert_ne!(
622 compare_snapshots_including_modify_date(
623 test_snap_change_1.unwrap(),
624 test_snap_change_2.unwrap(),
625 true
626 )
627 .unwrap()
628 .1
629 .changed
630 .len(),
631 3
632 );
633 fs::remove_dir_all(Path::new("./target/build/test_change_modify/")).unwrap();
634 }
635}