hashdeep_compare/
root.rs

1use crate::common::LogFile;
2use crate::log_entry::LogEntry;
3use crate::log_ops;
4
5#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
6pub struct ChangeRootSuccess
7{
8    /// Printable info lines
9    pub info_lines: Vec<String>,
10    /// Printable warning lines
11    pub warning_lines: Vec<String>,
12    /// Printable warning lines about the hashdeep log file, if any were emitted
13    pub file_warning_lines: Option<Vec<String>>,
14}
15
16fn info_lines(entries_matched: usize, entries_omitted: usize) -> Vec<String> {
17    let mut v = vec![];
18
19    let total_entries = entries_matched.checked_add(entries_omitted)
20        .expect("entry stats should not cause arithmetic overflow");
21    v.push(format!("Input file contains {total_entries} entries:"));
22
23    match entries_matched {
24        0 => {},
25        x if x == total_entries => v.push(format!("  All {x} entries matched the prefix")),
26        x => {
27            v.push(format!("  {x} entries matched the prefix"));
28            v.push(format!("  {entries_omitted} entries did not match the prefix and were omitted"));
29        },
30    }
31    v
32}
33
34fn warning_lines(entries_matched: usize, entries_omitted: usize, root_prefix: &str) -> Vec<String> {
35    let mut v = vec![];
36
37    if entries_matched == 0 && entries_omitted == 0 {
38        v.push("Warning: No entries were loaded from the input file".to_string());
39    }
40    else if entries_matched == 0 {
41        v.push("Warning: No entries matched the prefix (All entries were omitted)".to_string());
42    }
43
44    if root_prefix.is_empty() {
45        v.push("Warning: Prefix is empty (operation will have no effect)".to_string());
46    }
47    v
48}
49
50/// Reads a hashdeep log file and writes its entries to a new file, with
51/// its root directory adjusted:
52/// 1. file paths will have `root_prefix` removed
53/// 2. entries with file paths that don't start with `root_prefix` will be omitted
54///
55/// On success, returns a Vec of warning strings, if any warnings were emitted while reading the file.
56///
57/// # Errors
58///
59/// Any error emitted while reading or writing the files will be returned.
60pub fn change_root(filename: &str, out_filename: &str, root_prefix: &str)
61    -> Result<ChangeRootSuccess, Box<dyn std::error::Error>> {
62
63    let mut entry_count_before= 0;
64    let mut entry_count_after= 0;
65
66    let f = |log_file: &mut LogFile<Vec<LogEntry>>| {
67        entry_count_before = log_file.entries.len();
68
69        log_file.entries =
70            log_file.entries.iter().filter_map(|log_entry| {
71                log_entry.filename.strip_prefix(root_prefix).map(|new_path| {
72                    LogEntry{
73                        filename: new_path.to_string(),
74                        hashes: log_entry.hashes.clone(),
75                    }
76                })
77            }).collect();
78
79        entry_count_after = log_file.entries.len();
80    };
81
82    let file_warning_lines = log_ops::process_log(filename, out_filename, f)?;
83    let entries_matched = entry_count_after;
84    // Safety: this will not overflow, because filtering can only remove entries.
85    let entries_omitted = entry_count_before.checked_sub(entry_count_after)
86        .expect("filter should not increase entry count");
87
88    let info_lines = info_lines(entries_matched, entries_omitted);
89    let warning_lines = warning_lines(entries_matched, entries_omitted, root_prefix);
90
91    Ok(ChangeRootSuccess{file_warning_lines, info_lines, warning_lines})
92}
93
94#[cfg(test)]
95mod test {
96    use super::*;
97
98    use predicates::prelude::*;
99
100    #[test]
101    fn change_root_test() {
102        {
103            let temp_dir = tempfile::tempdir().unwrap();
104            let temp_file = temp_dir.path().join("test.txt");
105            let temp_file_path_str = temp_file.to_str().unwrap();
106
107            change_root("tests/test1.txt", temp_file_path_str, "hashdeepComp/").unwrap();
108
109            let p = predicates::path::eq_file("tests/test1_root_changed.txt");
110            assert!(p.eval(temp_file.as_path()));
111        }
112    }
113}