From 11a0b1b5052a03b872784b3379fd79fff6fed0c8 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Wed, 22 Jan 2025 00:15:40 +0100 Subject: [PATCH 1/5] checksum: move line warning display to process_line loop --- src/uucore/src/lib/features/checksum.rs | 60 ++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 8b136922fb..8a01870216 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -789,7 +789,6 @@ fn process_non_algo_based_line( /// matched the expected. /// If the comparison didn't happen, return a `LineChecksumError`. fn process_checksum_line( - filename_input: &OsStr, line: &OsStr, i: usize, cli_algo_name: Option<&str>, @@ -806,34 +805,19 @@ fn process_checksum_line( // Use `LineInfo` to extract the data of a line. // Then, depending on its format, apply a different pre-treatment. - if let Some(line_info) = LineInfo::parse(line, cached_regex) { - if line_info.format == LineFormat::AlgoBased { - process_algo_based_line(&line_info, cli_algo_name, opts) - } else if let Some(cli_algo) = cli_algo_name { - // If we match a non-algo based regex, we expect a cli argument - // to give us the algorithm to use - process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) - } else { - // We have no clue of what algorithm to use - return Err(LineCheckError::ImproperlyFormatted); - } - } else { - if opts.warn { - let algo = if let Some(algo_name_input) = cli_algo_name { - algo_name_input.to_uppercase() - } else { - "Unknown algorithm".to_string() - }; - eprintln!( - "{}: {}: {}: improperly formatted {} checksum line", - util_name(), - &filename_input.maybe_quote(), - i + 1, - algo - ); - } + let Some(line_info) = LineInfo::parse(line, cached_regex) else { + return Err(LineCheckError::ImproperlyFormatted); + }; - Err(LineCheckError::ImproperlyFormatted) + if line_info.format == LineFormat::AlgoBased { + process_algo_based_line(&line_info, cli_algo_name, opts) + } else if let Some(cli_algo) = cli_algo_name { + // If we match a non-algo based regex, we expect a cli argument + // to give us the algorithm to use + process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) + } else { + // We have no clue of what algorithm to use + return Err(LineCheckError::ImproperlyFormatted); } } @@ -871,7 +855,6 @@ fn process_checksum_file( for (i, line) in lines.iter().enumerate() { let line_result = process_checksum_line( - filename_input, line, i, cli_algo_name, @@ -893,7 +876,24 @@ fn process_checksum_file( match line_result { Ok(()) => res.correct += 1, Err(DigestMismatch) => res.failed_cksum += 1, - Err(ImproperlyFormatted) => res.bad_format += 1, + Err(ImproperlyFormatted) => { + res.bad_format += 1; + + if opts.warn { + let algo = if let Some(algo_name_input) = cli_algo_name { + Cow::Owned(algo_name_input.to_uppercase()) + } else { + Cow::Borrowed("Unknown algorithm") + }; + eprintln!( + "{}: {}: {}: improperly formatted {} checksum line", + util_name(), + &filename_input.maybe_quote(), + i + 1, + algo + ); + } + } Err(CantOpenFile | FileIsDirectory) => res.failed_open_file += 1, Err(FileNotFound) if !opts.ignore_missing => res.failed_open_file += 1, _ => continue, From 7fff3670b3620289c3b9114d8607f1a7d12bb98c Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Wed, 22 Jan 2025 00:35:43 +0100 Subject: [PATCH 2/5] checksum: fix improperly formatted WARNING message --- src/uucore/src/lib/features/checksum.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 8a01870216..ce8f2cb956 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -648,12 +648,11 @@ fn get_input_file(filename: &OsStr) -> UResult> { fn identify_algo_name_and_length( line_info: &LineInfo, algo_name_input: Option<&str>, + last_algo: &mut Option, ) -> Option<(String, Option)> { - let algorithm = line_info - .algo_name - .clone() - .unwrap_or_default() - .to_lowercase(); + let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); + let algorithm = algo_from_line.to_lowercase(); + *last_algo = Some(algo_from_line); // check if we are called with XXXsum (example: md5sum) but we detected a different algo parsing the file // (for example SHA1 (f) = d...) @@ -726,11 +725,13 @@ fn process_algo_based_line( line_info: &LineInfo, cli_algo_name: Option<&str>, opts: ChecksumOptions, + last_algo: &mut Option, ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_name, algo_byte_len) = identify_algo_name_and_length(line_info, cli_algo_name) - .ok_or(LineCheckError::ImproperlyFormatted)?; + let (algo_name, algo_byte_len) = + identify_algo_name_and_length(line_info, cli_algo_name, last_algo) + .ok_or(LineCheckError::ImproperlyFormatted)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. @@ -795,6 +796,7 @@ fn process_checksum_line( cli_algo_length: Option, opts: ChecksumOptions, cached_regex: &mut Option, + last_algo: &mut Option, ) -> Result<(), LineCheckError> { let line_bytes = os_str_as_bytes(line)?; @@ -810,7 +812,7 @@ fn process_checksum_line( }; if line_info.format == LineFormat::AlgoBased { - process_algo_based_line(&line_info, cli_algo_name, opts) + process_algo_based_line(&line_info, cli_algo_name, opts, last_algo) } else if let Some(cli_algo) = cli_algo_name { // If we match a non-algo based regex, we expect a cli argument // to give us the algorithm to use @@ -852,6 +854,10 @@ fn process_checksum_file( // cached_regex is used to ensure that several non algo-based checksum line // will use the same regex. let mut cached_regex = None; + // last_algo caches the algorithm used in the last line to print a warning + // message for the current line if improperly formatted. + // Behavior tested in gnu_cksum_c::test_warn + let mut last_algo = None; for (i, line) in lines.iter().enumerate() { let line_result = process_checksum_line( @@ -861,6 +867,7 @@ fn process_checksum_file( cli_algo_length, opts, &mut cached_regex, + &mut last_algo, ); // Match a first time to elude critical UErrors, and increment the total @@ -882,6 +889,8 @@ fn process_checksum_file( if opts.warn { let algo = if let Some(algo_name_input) = cli_algo_name { Cow::Owned(algo_name_input.to_uppercase()) + } else if let Some(algo) = &last_algo { + Cow::Borrowed(algo.as_str()) } else { Cow::Borrowed("Unknown algorithm") }; From dd17a26dd8fb5ea6d409f28d411a69bb637e5866 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Wed, 22 Jan 2025 01:42:43 +0100 Subject: [PATCH 3/5] checksum: change verbosity system so `--status`, `--quiet` and `--warn` override each other --- src/uu/cksum/src/cksum.rs | 21 ++++-- src/uu/hashsum/src/hashsum.rs | 17 +++-- src/uucore/src/lib/features/checksum.rs | 97 ++++++++++++++++++------- 3 files changed, 95 insertions(+), 40 deletions(-) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 84be146ec0..cf95d1bd24 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,8 +13,9 @@ use std::iter; use std::path::Path; use uucore::checksum::{ calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, - ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, + ChecksumError, ChecksumOptions, ChecksumVerbose, ALGORITHM_OPTIONS_BLAKE2B, + ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, + SUPPORTED_ALGORITHMS, }; use uucore::{ encoding, @@ -322,13 +323,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { binary: binary_flag, ignore_missing, - quiet, - status, strict, - warn, + verbose, }; return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); @@ -462,19 +464,22 @@ pub fn uu_app() -> Command { .short('w') .long("warn") .help("warn about improperly formatted checksum lines") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::QUIET]), ) .arg( Arg::new(options::STATUS) .long("status") .help("don't output anything, status code shows success") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]), ) .arg( Arg::new(options::QUIET) .long(options::QUIET) .help("don't print OK for each successfully verified file") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::STATUS]), ) .arg( Arg::new(options::IGNORE_MISSING) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 1d3a758f5e..b8dc63c323 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -24,6 +24,7 @@ use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::checksum::ChecksumError; use uucore::checksum::ChecksumOptions; +use uucore::checksum::ChecksumVerbose; use uucore::checksum::HashAlgorithm; use uucore::error::{FromIo, UResult}; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; @@ -240,13 +241,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { binary, ignore_missing, - quiet, - status, strict, - warn, + verbose, }; // Execute the checksum validation @@ -356,14 +358,16 @@ pub fn uu_app_common() -> Command { .short('q') .long(options::QUIET) .help("don't print OK for each successfully verified file") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::WARN]), ) .arg( Arg::new(options::STATUS) .short('s') .long("status") .help("don't output anything, status code shows success") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::QUIET, options::WARN]), ) .arg( Arg::new(options::STRICT) @@ -382,7 +386,8 @@ pub fn uu_app_common() -> Command { .short('w') .long("warn") .help("warn about improperly formatted checksum lines") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::QUIET, options::STATUS]), ) .arg( Arg::new("zero") diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index ce8f2cb956..9dacc4c5c3 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -148,15 +148,57 @@ impl From for FileCheckError { } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)] +pub enum ChecksumVerbose { + Status, + Quiet, + Normal, + Warning, +} + +impl ChecksumVerbose { + pub fn new(status: bool, quiet: bool, warn: bool) -> Self { + use ChecksumVerbose::*; + + // Assume only one of the three booleans will be enabled at once. + // This is ensured by clap's overriding arguments. + match (status, quiet, warn) { + (true, _, _) => Status, + (_, true, _) => Quiet, + (_, _, true) => Warning, + _ => Normal, + } + } + + #[inline] + pub fn over_status(self) -> bool { + self > Self::Status + } + + #[inline] + pub fn over_quiet(self) -> bool { + self > Self::Quiet + } + + #[inline] + pub fn at_least_warning(self) -> bool { + self >= Self::Warning + } +} + +impl Default for ChecksumVerbose { + fn default() -> Self { + Self::Normal + } +} + /// This struct regroups CLI flags. #[derive(Debug, Default, Clone, Copy)] pub struct ChecksumOptions { pub binary: bool, pub ignore_missing: bool, - pub quiet: bool, - pub status: bool, pub strict: bool, - pub warn: bool, + pub verbose: ChecksumVerbose, } #[derive(Debug, Error)] @@ -235,20 +277,19 @@ pub fn create_sha3(bits: Option) -> UResult { } #[allow(clippy::comparison_chain)] -fn cksum_output(res: &ChecksumResult, status: bool) { +fn print_cksum_report(res: &ChecksumResult) { if res.bad_format == 1 { show_warning_caps!("{} line is improperly formatted", res.bad_format); } else if res.bad_format > 1 { show_warning_caps!("{} lines are improperly formatted", res.bad_format); } - if !status { - if res.failed_cksum == 1 { - show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); - } else if res.failed_cksum > 1 { - show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); - } + if res.failed_cksum == 1 { + show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); + } else if res.failed_cksum > 1 { + show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); } + if res.failed_open_file == 1 { show_warning_caps!("{} listed file could not be read", res.failed_open_file); } else if res.failed_open_file > 1 { @@ -284,10 +325,10 @@ impl FileChecksumResult { /// The cli options might prevent to display on the outcome of the /// comparison on STDOUT. - fn can_display(&self, opts: ChecksumOptions) -> bool { + fn can_display(&self, verbose: ChecksumVerbose) -> bool { match self { - FileChecksumResult::Ok => !opts.status && !opts.quiet, - FileChecksumResult::Failed => !opts.status, + FileChecksumResult::Ok => verbose.over_quiet(), + FileChecksumResult::Failed => verbose.over_status(), FileChecksumResult::CantOpen => true, } } @@ -310,9 +351,9 @@ fn print_file_report( filename: &[u8], result: FileChecksumResult, prefix: &str, - opts: ChecksumOptions, + verbose: ChecksumVerbose, ) { - if result.can_display(opts) { + if result.can_display(verbose) { let _ = write!(w, "{prefix}"); let _ = w.write_all(filename); let _ = writeln!(w, ": {result}"); @@ -589,7 +630,7 @@ fn get_file_to_check( filename_bytes, FileChecksumResult::CantOpen, "", - opts, + opts.verbose, ); }; match File::open(filename) { @@ -710,7 +751,7 @@ fn compute_and_check_digest_from_file( filename, FileChecksumResult::from_bool(checksum_correct), prefix, - opts, + opts.verbose, ); if checksum_correct { @@ -886,7 +927,7 @@ fn process_checksum_file( Err(ImproperlyFormatted) => { res.bad_format += 1; - if opts.warn { + if opts.verbose.at_least_warning() { let algo = if let Some(algo_name_input) = cli_algo_name { Cow::Owned(algo_name_input.to_uppercase()) } else if let Some(algo) = &last_algo { @@ -912,7 +953,7 @@ fn process_checksum_file( // not a single line correctly formatted found // return an error if res.total_properly_formatted() == 0 { - if !opts.status { + if opts.verbose.over_status() { log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); } set_exit_code(1); @@ -920,16 +961,20 @@ fn process_checksum_file( } // if any incorrectly formatted line, show it - cksum_output(&res, opts.status); + if opts.verbose.over_status() { + print_cksum_report(&res); + } if opts.ignore_missing && res.correct == 0 { // we have only bad format // and we had ignore-missing - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); + if opts.verbose.over_status() { + eprintln!( + "{}: {}: no file was verified", + util_name(), + filename_input.maybe_quote(), + ); + } set_exit_code(1); } @@ -1425,7 +1470,7 @@ mod tests { for (filename, result, prefix, expected) in cases { let mut buffer: Vec = vec![]; - print_file_report(&mut buffer, filename, *result, prefix, opts); + print_file_report(&mut buffer, filename, *result, prefix, opts.verbose); assert_eq!(&buffer, expected) } } From 5d0d1c2a9dca7dce642dfcec0a2ed49a54490240 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Wed, 22 Jan 2025 03:19:23 +0100 Subject: [PATCH 4/5] checksum: rework exit_code setting to make use of the return type, fix logic for --ignore-missing --- src/uucore/src/lib/features/checksum.rs | 39 ++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 9dacc4c5c3..9912bea745 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -19,7 +19,7 @@ use std::{ }; use crate::{ - error::{set_exit_code, FromIo, UError, UResult, USimpleError}, + error::{FromIo, UError, UResult, USimpleError}, os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, sum::{ Blake2b, Blake3, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha3_224, @@ -130,10 +130,12 @@ impl From for LineCheckError { enum FileCheckError { /// a generic UError was encountered in sub-functions UError(Box), - /// the checksum file is improperly formatted. - ImproperlyFormatted, /// reading of the checksum file failed CantOpenChecksumFile, + /// processing of the file is considered as a failure regarding the + /// provided flags. This however does not stop the processing of + /// further files. + Failed, } impl From> for FileCheckError { @@ -883,7 +885,6 @@ fn process_checksum_file( Err(e) => { // Could not read the file, show the error and continue to the next file show_error!("{e}"); - set_exit_code(1); return Err(FileCheckError::CantOpenChecksumFile); } } @@ -956,8 +957,7 @@ fn process_checksum_file( if opts.verbose.over_status() { log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); } - set_exit_code(1); - return Err(FileCheckError::ImproperlyFormatted); + return Err(FileCheckError::Failed); } // if any incorrectly formatted line, show it @@ -975,18 +975,22 @@ fn process_checksum_file( filename_input.maybe_quote(), ); } - set_exit_code(1); + return Err(FileCheckError::Failed); } // strict means that we should have an exit code. if opts.strict && res.bad_format > 0 { - set_exit_code(1); + return Err(FileCheckError::Failed); } - // if we have any failed checksum verification, we set an exit code - // except if we have ignore_missing - if (res.failed_cksum > 0 || res.failed_open_file > 0) && !opts.ignore_missing { - set_exit_code(1); + // If a file was missing, return an error unless we explicitly ignore it. + if res.failed_open_file > 0 && !opts.ignore_missing { + return Err(FileCheckError::Failed); + } + + // Obviously, if a checksum failed at some point, report the error. + if res.failed_cksum > 0 { + return Err(FileCheckError::Failed); } Ok(()) @@ -1004,16 +1008,23 @@ pub fn perform_checksum_validation<'a, I>( where I: Iterator, { + let mut failed = false; + // if cksum has several input files, it will print the result for each file for filename_input in files { use FileCheckError::*; match process_checksum_file(filename_input, algo_name_input, length_input, opts) { Err(UError(e)) => return Err(e), - Err(CantOpenChecksumFile | ImproperlyFormatted) | Ok(_) => continue, + Err(Failed | CantOpenChecksumFile) => failed = true, + Ok(_) => continue, } } - Ok(()) + if failed { + Err(USimpleError::new(1, "")) + } else { + Ok(()) + } } pub fn digest_reader( From 84bbd05092993596ff1916cf8dd04c354ab17d46 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Wed, 22 Jan 2025 00:16:26 +0100 Subject: [PATCH 5/5] test(cksum): replicate GNU's cksum-c.sh test --- tests/by-util/test_cksum.rs | 381 +++++++++++++++++++++++++++++++++++- 1 file changed, 380 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index b73ee1425d..d2e8ac4c67 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb +// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont use crate::common::util::TestScenario; @@ -1284,6 +1284,18 @@ fn test_several_files_error_mgmt() { .stderr_contains("incorrect: no properly "); } +#[test] +fn test_check_unknown_checksum_file() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("--check") + .arg("missing") + .fails() + .stderr_only("cksum: missing: No such file or directory\n"); +} + #[test] fn test_check_comment_line() { // A comment in a checksum file shall be discarded unnoticed. @@ -1811,6 +1823,373 @@ mod gnu_cksum_base64 { } } +/// This module reimplements the cksum-c.sh GNU test. +mod gnu_cksum_c { + use super::*; + + const INVALID_SUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaafdb57c725157cb40b5aee8d937b8351477e"; + + fn make_scene() -> TestScenario { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("input", "9\n7\n1\n4\n2\n6\n3\n5\n8\n10\n"); + + let algos: &[&[&str]] = &[ + &["-a", "sha384"], + &["-a", "blake2b"], + &["-a", "blake2b", "-l", "384"], + &["-a", "sm3"], + ]; + + for args in algos { + let result = scene.ucmd().args(args).succeeds(); + let stdout = result.stdout(); + at.append_bytes("CHECKSUMS", stdout); + } + + scene + } + + #[test] + #[ignore] + fn test_signed_checksums() { + todo!() + } + + #[test] + fn test_check_individual_digests_in_mixed_file() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--check") + .arg("-a") + .arg("sm3") + .arg("CHECKSUMS") + .succeeds(); + } + + #[test] + fn test_check_against_older_non_hex_formats() { + let scene = make_scene(); + + scene + .ucmd() + .arg("-c") + .arg("-a") + .arg("crc") + .arg("CHECKSUMS") + .fails(); + + let crc_cmd = scene.ucmd().arg("-a").arg("crc").arg("input").succeeds(); + let crc_cmd_out = crc_cmd.stdout(); + scene.fixtures.write_bytes("CHECKSUMS.crc", crc_cmd_out); + + scene.ucmd().arg("-c").arg("CHECKSUMS.crc").fails(); + } + + #[test] + fn test_status() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .no_output(); + } + + fn make_scene_with_comment() -> TestScenario { + let scene = make_scene(); + + scene + .fixtures + .append("CHECKSUMS", "# Very important comment\n"); + + scene + } + + #[test] + fn test_status_with_comment() { + let scene = make_scene_with_comment(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .no_output(); + } + + fn make_scene_with_invalid_line() -> TestScenario { + let scene = make_scene_with_comment(); + + scene.fixtures.append("CHECKSUMS", "invalid_line\n"); + + scene + } + + #[test] + fn test_check_strict() { + let scene = make_scene_with_invalid_line(); + + // without strict, succeeds + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .stderr_contains("1 line is improperly formatted"); + + // with strict, fails + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("1 line is improperly formatted"); + } + + fn make_scene_with_two_invalid_lines() -> TestScenario { + let scene = make_scene_with_comment(); + + scene + .fixtures + .append("CHECKSUMS", "invalid_line\ninvalid_line\n"); + + scene + } + + #[test] + fn test_check_strict_plural_checks() { + let scene = make_scene_with_two_invalid_lines(); + + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("2 lines are improperly formatted"); + } + + fn make_scene_with_incorrect_checksum() -> TestScenario { + let scene = make_scene_with_two_invalid_lines(); + + scene + .fixtures + .append("CHECKSUMS", &format!("SM3 (input) = {INVALID_SUM}\n")); + + scene + } + + #[test] + fn test_check_with_incorrect_checksum() { + let scene = make_scene_with_incorrect_checksum(); + + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stdout_contains("input: FAILED") + .stderr_contains("1 computed checksum did NOT match"); + + // also fails with strict + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stdout_contains("input: FAILED") + .stderr_contains("1 computed checksum did NOT match"); + } + + #[test] + fn test_status_with_errors() { + let scene = make_scene_with_incorrect_checksum(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .no_output(); + } + + #[test] + fn test_check_with_non_existing_file() { + let scene = make_scene(); + scene + .fixtures + .write("CHECKSUMS2", &format!("SM3 (input2) = {INVALID_SUM}\n")); + + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS2") + .fails() + .stdout_contains("input2: FAILED open or read") + .stderr_contains("1 listed file could not be read"); + + // also fails with strict + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS2") + .fails() + .stdout_contains("input2: FAILED open or read") + .stderr_contains("1 listed file could not be read"); + } + + fn make_scene_with_another_improperly_formatted() -> TestScenario { + let scene = make_scene_with_incorrect_checksum(); + + scene.fixtures.append( + "CHECKSUMS", + &format!("BLAKE2b (missing-file) = {INVALID_SUM}\n"), + ); + + scene + } + + #[test] + fn test_warn() { + let scene = make_scene_with_another_improperly_formatted(); + + scene + .ucmd() + .arg("--warn") + .arg("--check") + .arg("CHECKSUMS") + .run() + .stderr_contains("CHECKSUMS: 6: improperly formatted SM3 checksum line") + .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line"); + } + + fn make_scene_with_checksum_missing() -> TestScenario { + let scene = make_scene_with_another_improperly_formatted(); + + scene.fixtures.write( + "CHECKSUMS-missing", + &format!("SM3 (nonexistent) = {INVALID_SUM}\n"), + ); + + scene + } + + #[test] + fn test_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS-missing") + .fails() + .stdout_does_not_contain("nonexistent: No such file or directory") + .stdout_does_not_contain("nonexistent: FAILED open or read") + .stderr_contains("CHECKSUMS-missing: no file was verified"); + } + + #[test] + fn test_status_and_warn() { + let scene = make_scene_with_checksum_missing(); + + // --status before --warn + scene + .ucmd() + .arg("--status") + .arg("--warn") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line") + .stderr_contains("WARNING: 3 lines are improperly formatted") + .stderr_contains("WARNING: 1 computed checksum did NOT match"); + + // --warn before --status (status hides the results) + scene + .ucmd() + .arg("--warn") + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_does_not_contain("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line") + .stderr_does_not_contain("WARNING: 3 lines are improperly formatted") + .stderr_does_not_contain("WARNING: 1 computed checksum did NOT match"); + } + + #[test] + fn test_status_and_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--status") + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .no_output(); + } + + #[test] + fn test_status_warn_and_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--status") + .arg("--warn") + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS-missing") + .fails() + .stderr_contains("CHECKSUMS-missing: no file was verified") + .stdout_does_not_contain("nonexistent: No such file or directory"); + } + + #[test] + fn test_check_several_files_dont_exist() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--check") + .arg("non-existing-1") + .arg("non-existing-2") + .fails() + .stderr_contains("non-existing-1: No such file or directory") + .stderr_contains("non-existing-2: No such file or directory"); + } + + #[test] + fn test_check_several_files_empty() { + let scene = make_scene(); + scene.fixtures.touch("empty-1"); + scene.fixtures.touch("empty-2"); + + scene + .ucmd() + .arg("--check") + .arg("empty-1") + .arg("empty-2") + .fails() + .stderr_contains("empty-1: no properly formatted checksum lines found") + .stderr_contains("empty-2: no properly formatted checksum lines found"); + } +} + /// The tests in this module check the behavior of cksum when given different /// checksum formats and algorithms in the same file, while specifying an /// algorithm on CLI or not.