hashdeep_compare/
command.rs

1use std::process::{Command,Stdio};
2use std::fs::OpenOptions;
3use std::io::ErrorKind;
4
5use thiserror::Error;
6use anyhow::anyhow;
7use which::which;
8
9const CANNOT_FIND_BINARY_PATH_STR : &str = "external hashdeep binary cannot be found (is hashdeep installed?)";
10
11
12#[derive(Error, Debug)]
13pub enum RunHashdeepCommandError {
14
15    #[error("{}",CANNOT_FIND_BINARY_PATH_STR)]
16    CannotFindBinaryPath,
17
18    #[error("{0} exists (will not overwrite existing files)")]
19    OutputFileExists(String),
20
21    #[error("{0} and {1} exist (will not overwrite existing files)")]
22    OutputFilesExist(String, String),
23
24    #[error("\"{0}\" cannot be opened for writing (does the directory exist?)")]
25    OutputFileNotFound(String),
26
27    #[error("\"{0}\" cannot be opened for writing ({})", .1)]
28    OutputFileOtherError(String, #[source] std::io::Error),
29
30    #[error(transparent)]
31    Io(#[from] std::io::Error),
32
33    #[error(transparent)]
34    Other(#[from] anyhow::Error),
35}
36
37impl RunHashdeepCommandError {
38
39    fn new(e: std::io::Error, path: &str) -> Self {
40
41        match e.kind() {
42            ErrorKind::AlreadyExists => RunHashdeepCommandError::OutputFileExists(path.to_string()),
43            ErrorKind::NotFound      => RunHashdeepCommandError::OutputFileNotFound(path.to_string()),
44            _                        => RunHashdeepCommandError::OutputFileOtherError(path.to_string(), e),
45        }
46    }
47}
48
49/// Runs hashdeep with the settings recommended for hashdeep-compare.
50///
51/// The log includes (recursively) all files and directories in `target_directory`,
52/// and is written to `output_path_base`, with hashdeep's stderr
53/// written to `output_path_base` + ".errors".
54///
55/// # Errors
56///
57/// An error will be returned if
58/// * the `hashdeep` command is not available
59/// * the output log file or error file already exist (will not overwrite existing files)
60/// * any other error occurs while creating the output files
61/// * any other error occurs when running `hashdeep`
62pub fn run_hashdeep_command(
63    target_directory: &str,
64    output_path_base: &str,
65    hashdeep_command_name: &str,
66) -> Result<(), RunHashdeepCommandError> {
67
68    //confirm availability of external hashdeep binary
69    match which(hashdeep_command_name) {
70        Err(which::Error::CannotFindBinaryPath) => return Err(RunHashdeepCommandError::CannotFindBinaryPath),
71        Err(x) => return Err(anyhow!(x).into()),
72        _ => ()
73    };
74
75    let error_log_suffix = ".errors";
76
77    let output_error_path = format!("{output_path_base}{error_log_suffix}");
78
79
80    //try to open both output files
81    let maybe_output_file =
82        OpenOptions::new().write(true).create_new(true).open(output_path_base)
83            .map_err(|e| RunHashdeepCommandError::new(e, output_path_base));
84
85    let maybe_error_file  =
86        OpenOptions::new().write(true).create_new(true).open(&output_error_path)
87            .map_err(|e| RunHashdeepCommandError::new(e, &output_error_path));
88
89
90    let (output_file, error_file) =
91
92    match (maybe_output_file, maybe_error_file) {
93
94        (Ok(output_file), Ok(error_file)) => (output_file, error_file),
95
96
97        //if either file failed to open, abort the command and clean up:
98
99        (Err(output_file_error), Ok(_)) => {
100
101            //delete the file that was successfully created
102            std::fs::remove_file(&output_error_path)?;
103
104            return Err(output_file_error);
105        },
106
107        (Ok(_), Err(error_file_error)) => {
108
109            //delete the file that was successfully created
110            std::fs::remove_file(output_path_base)?;
111
112            return Err(error_file_error);
113        },
114
115        (Err(output_file_error), Err(error_file_error)) => {
116
117            //if present, combine 2 OutputFileExists errors into 1 OutputFilesExist error
118            return Err(
119                if let ( RunHashdeepCommandError::OutputFileExists(file1),
120                         RunHashdeepCommandError::OutputFileExists(file2) )
121                        = (&output_file_error, &error_file_error)
122                {
123                    RunHashdeepCommandError::OutputFilesExist(file1.clone(), file2.clone())
124                }
125                else {
126                    //otherwise, just return the output file's error
127                    output_file_error
128                }
129            );
130        }
131    };
132
133    Command::new(hashdeep_command_name)
134
135    .arg("-l")
136    .arg("-r")
137    .arg("-o").arg("f")
138    .arg(target_directory)
139
140    .stdin(Stdio::null())
141    .stdout(output_file)
142    .stderr(error_file)
143
144    .status()?;
145    Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn run_hashdeep_command_missing_hashdeep_test() {
154
155        //directly test the 'no hashdeep' error
156        //(could eventually be replaced if integration testing can somehow hide hashdeep)
157
158        assert_eq!(CANNOT_FIND_BINARY_PATH_STR,
159
160            run_hashdeep_command("fake_target_dir",
161                                 "fake_output_path_base",
162                                 "nonexistent_program_name_Cmn2TMmwGO9U2j7")
163            .unwrap_err().to_string()
164        );
165    }
166}