1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! The **main_impl** module exists to add I/O redirection to the main program function, to allow
//! test coverage analysis for integration tests.
//!
//! ## Normal Program Behavior
//!
//! *main()* in main.rs calls *main_io_wrapper* in this module, supplying arguments and the standard
//! output and error streams.
//! *main_io_wrapper*, in turn, calls *main_impl*, which is the equivalent of a typical *main()*
//! function.
//!
//! ## Special handling: the **integration_test_coverage** feature
//!
//! Integration tests are defined in tests/integration.rs. These tests invoke the compiled program
//! binary and test its functions with command-line arguments. Because they call a
//! separate binary instead of code in hashdeep-compare's codebase, code coverage tools can't observe
//! what code they use.
//!
//! To work around this, hashdeep-compare has an **integration_test_coverage** feature. When enabled,
//! integration.rs uses a modified test function that runs the same tests as before, but through direct
//! calls in the codebase, instead of through a precompiled binary. The *main_io_wrapper* function is
//! its interface in this mode. These direct calls are part of the test binary, and can be observed by
//! a code coverage tool.
//!
//! ## Justification
//!
//! Separating the program's main function code between main.rs and **main_impl** does add some
//! complexity to the codebase.
//! The option of doing integration testing through direct calls to *main_io_wrapper*, as the
//! **integration_test_coverage** mode does now, would test almost all of the relevant code: the only code bypassed
//! is the code in *main()* in main.rs, which is a minimal wrapper around *main_io_wrapper*. However,
//! actual invocation through a separate binary provides a higher level of certainty against potential
//! future anomalies that disrupt the creation of the binary itself, or the processing of its inputs.
//! Integration testing with binaries is meant to replicate the actual use of the tool as closely as
//! possible: for this reason, the **integration_test_coverage** feature-based mode switch is preferred.

use crate::*;
use std::error::Error;
use std::io::Write;

/// Specifies program arguments and (re)direction of stdout/stderr, then runs the program
///
/// Returns the program's exit code
///
/// This is called by
/// - main() in normal execution
/// - integration.rs when the **integration_test_coverage** feature is enabled
pub fn main_io_wrapper(
    args: &[&str],
    stdout: Box<dyn Write>,
    mut stderr: Box<dyn Write>,
) -> Result<i32, Box<dyn Error>> {

    let exit_code =
    match main_impl(args, stdout, &mut stderr)
    {
        Ok(()) => 0,
        Err(err) => {

            //conditionally use Display output for thiserror-based error types
            if let Some(err) = err.downcast_ref::<command::RunHashdeepCommandError>() {
                writeln! (stderr, "Error: \"{}\"", err)?;
            }
            else if let Some(err) = err.downcast_ref::<common::ReadLogEntriesFromFileError>() {
                writeln! (stderr, "Error: \"{}\"", err)?;
            }
            else if let Some(err) = err.downcast_ref::<common::WriteToFileError>() {
                writeln! (stderr, "Error: \"{}\"", err)?;
            }
            else if let Some(err) = err.downcast_ref::<partitioner::MatchPartitionError>() {
                writeln! (stderr, "Error: \"{}\"", err)?;
            }
            else {
                writeln! (stderr, "Error: {:?}", err)?;
            }

            1
        }
    };

    Ok(exit_code)
}


fn print_hashdeep_log_warnings (
    filename: &str,
    warning_lines: Option<Vec<String>>,
    stderr: &mut Box<dyn Write>) -> Result<(), Box<dyn Error>>
{
    if let Some(v) = warning_lines {
        writeln!(stderr, "Warnings emitted for hashdeep log at: {}", filename)?;
        for line in v {
            writeln!(stderr, "  {}", line)?;
        }
    }
    Ok(())
}

/// Called by main_io_wrapper: Accepts program arguments and runs the program
///
/// (This was the main() function before the **integration_test_coverage** feature was added)
fn main_impl(args: &[&str], mut stdout: Box<dyn Write>, stderr: &mut Box<dyn Write>) -> Result<(), Box<dyn Error>> {

    const VERSION: &str = env!("CARGO_PKG_VERSION");

    match args.get(1) {

        None | Some(&"help") => {

            let help_string =
            match args.get(2) {
                Some(&"hash") => help::help_hash_string(),
                Some(&"sort") => help::help_sort_string(),
                Some(&"part") => help::help_part_string(),
                _ => help::help_string(VERSION),
            };

            writeln!(stdout, "{}", help_string)?;

        },
        Some(&"hash") => {
            if args.len() < 4 {return Err("hash: not enough arguments".into());}
            if args.len() > 4 {return Err("hash: too many arguments".into());}

            command::run_hashdeep_command(
                args[2],
                args[3],
                "hashdeep")?;
        },
        Some(&"sort") => {
            if args.len() < 4 {return Err("sort: not enough arguments".into());}
            if args.len() > 4 {return Err("sort: too many arguments".into());}

            let warning_lines = sort::sort_log(args[2], args[3])?;
            print_hashdeep_log_warnings(args[2], warning_lines, stderr)?;
        },
        Some(&"part") => {
            if args.len() < 5 {return Err("part: not enough arguments".into());}
            if args.len() > 5 {return Err("part: too many arguments".into());}

            let partition_stats =
            partition::partition_log(args[2], args[3], args[4])?;

            writeln!(stdout, "{}", partition_stats.stats_string)?;
            print_hashdeep_log_warnings(args[2], partition_stats.file1_warning_lines, stderr)?;
            print_hashdeep_log_warnings(args[3], partition_stats.file2_warning_lines, stderr)?;
        },
        Some(&"version") => {
            if args.len() > 2 {return Err("version: does not accept arguments".into());}

            writeln!(stdout, "hashdeep-compare version {}", VERSION)?;
        },

        Some(x) => return Err(format!("invalid command: {}", x).into())
    }

    Ok(())
}