hashdeep_compare/
main_impl.rs

1//! The **main_impl** module exists to add I/O redirection to the main program function, to allow
2//! test coverage analysis for integration tests.
3//!
4//! ## Normal Program Behavior
5//!
6//! *main()* in main.rs calls *main_io_wrapper* in this module, supplying arguments and the standard
7//! output and error streams.
8//! *main_io_wrapper*, in turn, calls *main_impl*, which is the equivalent of a typical *main()*
9//! function.
10//!
11//! ## Special handling: the **integration_test_coverage** feature
12//!
13//! Integration tests are defined in tests/integration.rs. These tests invoke the compiled program
14//! binary and test its functions with command-line arguments. Because they call a
15//! separate binary instead of code in hashdeep-compare's codebase, code coverage tools can't observe
16//! what code they use.
17//!
18//! To work around this, hashdeep-compare has an **integration_test_coverage** feature. When enabled,
19//! integration.rs uses a modified test function that runs the same tests as before, but through direct
20//! calls in the codebase, instead of through a precompiled binary. The *main_io_wrapper* function is
21//! its interface in this mode. These direct calls are part of the test binary, and can be observed by
22//! a code coverage tool.
23//!
24//! ## Justification
25//!
26//! Separating the program's main function code between main.rs and **main_impl** does add some
27//! complexity to the codebase.
28//! The option of doing integration testing through direct calls to *main_io_wrapper*, as the
29//! **integration_test_coverage** mode does now, would test almost all of the relevant code: the only code bypassed
30//! is the code in *main()* in main.rs, which is a minimal wrapper around *main_io_wrapper*. However,
31//! actual invocation through a separate binary provides a higher level of certainty against potential
32//! future anomalies that disrupt the creation of the binary itself, or the processing of its inputs.
33//! Integration testing with binaries is meant to replicate the actual use of the tool as closely as
34//! possible: for this reason, the **integration_test_coverage** feature-based mode switch is preferred.
35
36use crate::*;
37use std::error::Error;
38use std::io::Write;
39use clap::{Parser, Subcommand};
40
41/// Specifies program arguments and (re)direction of stdout/stderr, then runs the program
42///
43/// Returns the program's exit code
44///
45/// This is called by
46/// - main() in normal execution
47/// - integration.rs when the **integration_test_coverage** feature is enabled
48pub fn main_io_wrapper(
49    args: &[&str],
50    mut stdout: impl Write,
51    mut stderr: impl Write,
52) -> Result<i32, Box<dyn Error>> {
53
54    let exit_code =
55    match main_impl(args, &mut stdout, &mut stderr)
56    {
57        Ok(()) => 0,
58        Err(err) => {
59            if let Some(err) = err.downcast_ref::<clap::Error>() {
60                if err.use_stderr() {
61                    write! (stderr, "{err}")?;
62                    // Code 2 on error matches `clap`'s behavior.
63                    2
64                }
65                else {
66                    write! (stdout, "{err}")?;
67                    0
68                }
69            }
70            else {
71                //conditionally use Display output for thiserror-based error types
72                if let Some(err) = err.downcast_ref::<command::RunHashdeepCommandError>() {
73                    writeln! (stderr, "Error: \"{err}\"")?;
74                }
75                else if let Some(err) = err.downcast_ref::<common::ReadLogEntriesFromFileError>() {
76                    writeln! (stderr, "Error: \"{err}\"")?;
77                }
78                else if let Some(err) = err.downcast_ref::<common::WriteToFileError>() {
79                    writeln! (stderr, "Error: \"{err}\"")?;
80                }
81                else if let Some(err) = err.downcast_ref::<partitioner::MatchPartitionError>() {
82                    writeln! (stderr, "Error: \"{err}\"")?;
83                }
84                else {
85                    writeln! (stderr, "Error: {err:?}")?;
86                }
87
88                1
89            }
90        }
91    };
92
93    Ok(exit_code)
94}
95
96
97fn print_hashdeep_log_warnings (
98    filename: &str,
99    warning_lines: Option<Vec<String>>,
100    stderr: &mut impl Write) -> Result<(), Box<dyn Error>>
101{
102    if let Some(v) = warning_lines {
103        writeln!(stderr, "Warnings emitted for hashdeep log at: {filename}")?;
104        for line in v {
105            writeln!(stderr, "  {line}")?;
106        }
107    }
108    Ok(())
109}
110
111fn write_lines (writer: &mut impl Write, lines: Vec<String>) -> Result<(), Box<dyn Error>> {
112    lines.iter().try_for_each(|line| writeln!(writer, "{line}").map_err(Into::into))
113}
114
115/// Called by main_io_wrapper: Accepts program arguments and runs the program
116///
117/// (This was the main() function before the **integration_test_coverage** feature was added)
118fn main_impl(args: &[&str], stdout: &mut impl Write, stderr: &mut impl Write) -> Result<(), Box<dyn Error>> {
119
120    const VERSION: &str = env!("CARGO_PKG_VERSION");
121
122    #[derive(Parser, Debug)]
123    #[command(about = help::help_string(VERSION))]
124    struct CliArgs {
125        #[command(subcommand)]
126        command: Commands,
127    }
128
129    #[derive(Subcommand, Debug)]
130    #[command(disable_help_flag = true)]
131    enum Commands {
132        /// Display version string
133        Version,
134        #[command(after_long_help = help::help_hash_string())]
135        #[command(long_about = help::long_about_hash_string())]
136        /// Invoke hashdeep on a target directory
137        Hash {
138            #[arg(hide_long_help = true, id="path/to/target_dir")]
139            target_directory: String,
140            #[arg(hide_long_help = true, id="path/to/output_log.txt")]
141            output_path_base: String,
142        },
143        #[command(after_long_help = help::help_sort_string())]
144        #[command(long_about = help::long_about_sort_string())]
145        /// Sort a hashdeep log (by file path)
146        Sort {
147            #[arg(hide_long_help = true, id="path/to/unsorted_input.txt")]
148            input_file: String,
149            #[arg(hide_long_help = true, id="path/to/sorted_output.txt")]
150            output_file: String,
151        },
152        #[command(after_long_help = help::help_root_string())]
153        #[command(long_about = help::long_about_root_string())]
154        /// Change a hashdeep log root by removing a prefix from its filepaths
155        Root {
156            #[arg(hide_long_help = true, id="path/to/input.txt")]
157            input_file: String,
158            #[arg(hide_long_help = true, id="path/to/output.txt")]
159            output_file: String,
160            #[arg(hide_long_help = true, id="filepath prefix")]
161            file_path_prefix: String,
162        },
163        #[command(after_long_help = help::help_part_string())]
164        #[command(long_about = help::long_about_part_string())]
165        /// Partition contents of two hashdeep logs into category files
166        Part {
167            #[arg(hide_long_help = true, id="path/to/first_log.txt")]
168            input_file1: String,
169            #[arg(hide_long_help = true, id="path/to/second_log.txt")]
170            input_file2: String,
171            #[arg(hide_long_help = true, id="path/to/output_file_base")]
172            output_file_base: String,
173        },
174    }
175
176    let cli_args = CliArgs::try_parse_from(args)?;
177
178    match cli_args.command {
179        Commands::Hash {target_directory, output_path_base} => {
180            command::run_hashdeep_command(
181                target_directory.as_str(),
182                output_path_base.as_str(),
183                "hashdeep")?;
184        },
185        Commands::Sort {input_file, output_file} => {
186            let warning_lines = sort::sort_log(
187                input_file.as_str(),
188                output_file.as_str()
189            )?;
190            print_hashdeep_log_warnings(input_file.as_str(), warning_lines, stderr)?;
191        },
192        Commands::Root {input_file, output_file, file_path_prefix} => {
193            let success = root::change_root(
194                input_file.as_str(),
195                output_file.as_str(),
196                file_path_prefix.as_str(),
197            )?;
198            write_lines(stdout, success.info_lines)?;
199            write_lines(stderr, success.warning_lines)?;
200            print_hashdeep_log_warnings(input_file.as_str(), success.file_warning_lines, stderr)?;
201        },
202        Commands::Part {input_file1, input_file2, output_file_base} => {
203            let partition_stats =
204            partition::partition_log(
205                input_file1.as_str(),
206                input_file2.as_str(),
207                output_file_base.as_str()
208            )?;
209
210            writeln!(stdout, "{}", partition_stats.stats_string)?;
211            print_hashdeep_log_warnings(input_file1.as_str(), partition_stats.file1_warning_lines, stderr)?;
212            print_hashdeep_log_warnings(input_file2.as_str(), partition_stats.file2_warning_lines, stderr)?;
213        },
214        Commands::Version => {
215            writeln!(stdout, "hashdeep-compare version {VERSION}")?;
216        }
217    }
218
219    Ok(())
220}