diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e7d73a3bb..2392660ee 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,8 +13,8 @@ use std::iter; use std::path::Path; use uucore::checksum::{ calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, - ChecksumError, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, + ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, + ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, }; use uucore::{ encoding, @@ -318,17 +318,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); - return perform_checksum_validation( - files.iter().copied(), - strict, - status, - warn, - binary_flag, + let opts = ChecksumOptions { + binary: binary_flag, ignore_missing, quiet, - algo_option, - length, - ); + status, + strict, + warn, + }; + + return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); } let (tag, asterisk) = handle_tag_text_binary_flags(&matches)?; diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 90c8c8adf..1d3a758f5 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -23,6 +23,7 @@ use uucore::checksum::digest_reader; use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::checksum::ChecksumError; +use uucore::checksum::ChecksumOptions; use uucore::checksum::HashAlgorithm; use uucore::error::{FromIo, UResult}; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; @@ -239,18 +240,21 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::>(), |files| files.map(OsStr::new).collect::>(), ); + let opts = ChecksumOptions { + binary, + ignore_missing, + quiet, + status, + strict, + warn, + }; // Execute the checksum validation return perform_checksum_validation( input.iter().copied(), - strict, - status, - warn, - binary, - ignore_missing, - quiet, Some(algo.name), Some(algo.bits), + opts, ); } else if quiet { return Err(ChecksumError::QuietNotCheck.into()); diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index a2de28bc5..e7a0a2653 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -75,6 +75,71 @@ struct ChecksumResult { pub failed_open_file: i32, } +/// Represents a reason for which the processing of a checksum line +/// could not proceed to digest comparison. +enum LineCheckError { + /// a generic UError was encountered in sub-functions + UError(Box), + /// the computed checksum digest differs from the expected one + DigestMismatch, + /// the line is empty or is a comment + Skipped, + /// the line has a formatting error + ImproperlyFormatted, + /// file exists but is impossible to read + CantOpenFile, + /// there is nothing at the given path + FileNotFound, + /// the given path leads to a directory + FileIsDirectory, +} + +impl From> for LineCheckError { + fn from(value: Box) -> Self { + Self::UError(value) + } +} + +impl From for LineCheckError { + fn from(value: ChecksumError) -> Self { + Self::UError(Box::new(value)) + } +} + +/// Represents an error that was encountered when processing a checksum file. +#[allow(clippy::enum_variant_names)] +enum FileCheckError { + /// a generic UError was encountered in sub-functions + UError(Box), + /// the error does not stop the processing of next files + NonCriticalError, + /// the error must stop the run of the program + CriticalError, +} + +impl From> for FileCheckError { + fn from(value: Box) -> Self { + Self::UError(value) + } +} + +impl From for FileCheckError { + fn from(value: ChecksumError) -> Self { + Self::UError(Box::new(value)) + } +} + +/// 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, +} + #[derive(Debug, Error)] pub enum ChecksumError { #[error("the --raw option is not supported with multiple files")] @@ -174,6 +239,8 @@ fn cksum_output(res: &ChecksumResult, status: bool) { } } +/// Represents the different outcomes that can happen to a file +/// that is being checked. #[derive(Debug, Clone, Copy)] enum FileChecksumResult { Ok, @@ -181,6 +248,28 @@ enum FileChecksumResult { CantOpen, } +impl FileChecksumResult { + /// Creates a `FileChecksumResult` from a digest comparison that + /// either succeeded or failed. + fn from_bool(checksum_correct: bool) -> Self { + if checksum_correct { + FileChecksumResult::Ok + } else { + FileChecksumResult::Failed + } + } + + /// The cli options might prevent to display on the outcome of the + /// comparison on STDOUT. + fn can_display(&self, opts: ChecksumOptions) -> bool { + match self { + FileChecksumResult::Ok => !opts.status && !opts.quiet, + FileChecksumResult::Failed => !opts.status, + FileChecksumResult::CantOpen => true, + } + } +} + impl Display for FileChecksumResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -198,10 +287,13 @@ fn print_file_report( filename: &[u8], result: FileChecksumResult, prefix: &str, + opts: ChecksumOptions, ) { - let _ = write!(w, "{prefix}"); - let _ = w.write_all(filename); - let _ = writeln!(w, ": {result}"); + if result.can_display(opts) { + let _ = write!(w, "{prefix}"); + let _ = w.write_all(filename); + let _ = writeln!(w, ": {result}"); + } } pub fn detect_algo(algo: &str, length: Option) -> UResult { @@ -390,45 +482,47 @@ fn get_expected_checksum( /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". fn get_file_to_check( filename: &OsStr, - ignore_missing: bool, - res: &mut ChecksumResult, -) -> Option> { + opts: ChecksumOptions, +) -> Result, LineCheckError> { let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error"); let filename_lossy = String::from_utf8_lossy(filename_bytes); if filename == "-" { - Some(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file + Ok(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file } else { - let mut failed_open = || { + let failed_open = || { print_file_report( std::io::stdout(), filename_bytes, FileChecksumResult::CantOpen, "", + opts, ); - res.failed_open_file += 1; }; match File::open(filename) { Ok(f) => { - if f.metadata().ok()?.is_dir() { + if f.metadata() + .map_err(|_| LineCheckError::CantOpenFile)? + .is_dir() + { show!(USimpleError::new( 1, format!("{filename_lossy}: Is a directory") )); // also regarded as a failed open failed_open(); - None + Err(LineCheckError::FileIsDirectory) } else { - Some(Box::new(f)) + Ok(Box::new(f)) } } Err(err) => { - if !ignore_missing { + if !opts.ignore_missing { // yes, we have both stderr and stdout here show!(err.map_err_context(|| filename_lossy.to_string())); failed_open(); } // we could not open the file but we want to continue - None + Err(LineCheckError::FileNotFound) } } } @@ -502,208 +596,251 @@ fn identify_algo_name_and_length( Some((algorithm, bits)) } +/// Parses a checksum line, detect the algorithm to use, read the file and produce +/// its digest, and compare it to the expected value. +/// +/// Returns `Ok(bool)` if the comparison happened, bool indicates if the digest +/// matched the expected. +/// If the comparison didn't happen, return a `LineChecksumError`. +#[allow(clippy::too_many_arguments)] +fn process_checksum_line( + filename_input: &OsStr, + line: &OsStr, + i: usize, + chosen_regex: &Regex, + is_algo_based_format: bool, + res: &mut ChecksumResult, + cli_algo_name: Option<&str>, + cli_algo_length: Option, + properly_formatted: &mut bool, + opts: ChecksumOptions, +) -> Result<(), LineCheckError> { + let line_bytes = os_str_as_bytes(line)?; + if let Some(caps) = chosen_regex.captures(line_bytes) { + *properly_formatted = true; + + let mut filename_to_check = caps.name("filename").unwrap().as_bytes(); + + if filename_to_check.starts_with(b"*") + && i == 0 + && chosen_regex.as_str() == SINGLE_SPACE_REGEX + { + // Remove the leading asterisk if present - only for the first line + filename_to_check = &filename_to_check[1..]; + } + + let expected_checksum = get_expected_checksum(filename_to_check, &caps, chosen_regex)?; + + // If the algo_name is provided, we use it, otherwise we try to detect it + let (algo_name, length) = if is_algo_based_format { + identify_algo_name_and_length(&caps, cli_algo_name, res, properly_formatted) + .unwrap_or((String::new(), None)) + } else if let Some(a) = cli_algo_name { + // When a specific algorithm name is input, use it and use the provided bits + // except when dealing with blake2b, where we will detect the length + if cli_algo_name == Some(ALGORITHM_OPTIONS_BLAKE2B) { + // division by 2 converts the length of the Blake2b checksum from hexadecimal + // characters to bytes, as each byte is represented by two hexadecimal characters. + let length = Some(expected_checksum.len() / 2); + (ALGORITHM_OPTIONS_BLAKE2B.to_string(), length) + } else { + (a.to_lowercase(), cli_algo_length) + } + } else { + // Default case if no algorithm is specified and non-algo based format is matched + (String::new(), None) + }; + + if algo_name.is_empty() { + // we haven't been able to detect the algo name. No point to continue + *properly_formatted = false; + + // TODO: return error? + return Err(LineCheckError::ImproperlyFormatted); + } + let mut algo = detect_algo(&algo_name, length)?; + + let (filename_to_check_unescaped, prefix) = unescape_filename(filename_to_check); + + let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?; + + // manage the input file + let file_to_check = get_file_to_check(&real_filename_to_check, opts)?; + let mut file_reader = BufReader::new(file_to_check); + + // Read the file and calculate the checksum + let create_fn = &mut algo.create_fn; + let mut digest = create_fn(); + let (calculated_checksum, _) = + digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bits).unwrap(); + + // Do the checksum validation + let checksum_correct = expected_checksum == calculated_checksum; + print_file_report( + std::io::stdout(), + filename_to_check, + FileChecksumResult::from_bool(checksum_correct), + prefix, + opts, + ); + + if checksum_correct { + Ok(()) + } else { + Err(LineCheckError::DigestMismatch) + } + } else { + if line.is_empty() || line_bytes.starts_with(b"#") { + // Don't show any warning for empty or commented lines. + return Err(LineCheckError::Skipped); + } + 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 + ); + } + + res.bad_format += 1; + Err(LineCheckError::ImproperlyFormatted) + } +} + +fn process_checksum_file( + filename_input: &OsStr, + cli_algo_name: Option<&str>, + cli_algo_length: Option, + opts: ChecksumOptions, +) -> Result<(), FileCheckError> { + let mut correct_format = 0; + let mut properly_formatted = false; + let mut res = ChecksumResult::default(); + let input_is_stdin = filename_input == OsStr::new("-"); + + let file: Box = if input_is_stdin { + // Use stdin if "-" is specified + Box::new(stdin()) + } else { + match get_input_file(filename_input) { + Ok(f) => f, + 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::NonCriticalError); + } + } + }; + + let reader = BufReader::new(file); + let lines = read_os_string_lines(reader).collect::>(); + + let Some((chosen_regex, is_algo_based_format)) = determine_regex(&lines) else { + let e = ChecksumError::NoProperlyFormattedChecksumLinesFound { + filename: get_filename_for_output(filename_input, input_is_stdin), + }; + show_error!("{e}"); + set_exit_code(1); + return Err(FileCheckError::NonCriticalError); + }; + + for (i, line) in lines.iter().enumerate() { + match process_checksum_line( + filename_input, + line, + i, + &chosen_regex, + is_algo_based_format, + &mut res, + cli_algo_name, + cli_algo_length, + &mut properly_formatted, + opts, + ) { + Ok(()) => correct_format += 1, + Err(LineCheckError::DigestMismatch) => res.failed_cksum += 1, + Err(LineCheckError::UError(e)) => return Err(e.into()), + Err(LineCheckError::Skipped) => continue, + Err(LineCheckError::ImproperlyFormatted) => (), + Err(LineCheckError::CantOpenFile | LineCheckError::FileIsDirectory) => { + res.failed_open_file += 1 + } + Err(LineCheckError::FileNotFound) => { + if !opts.ignore_missing { + res.failed_open_file += 1 + } + } + }; + } + + // not a single line correctly formatted found + // return an error + if !properly_formatted { + if !opts.status { + return Err(ChecksumError::NoProperlyFormattedChecksumLinesFound { + filename: get_filename_for_output(filename_input, input_is_stdin), + } + .into()); + } + set_exit_code(1); + return Err(FileCheckError::CriticalError); + } + + // if any incorrectly formatted line, show it + cksum_output(&res, opts.status); + + if opts.ignore_missing && correct_format == 0 { + // we have only bad format + // and we had ignore-missing + eprintln!( + "{}: {}: no file was verified", + util_name(), + filename_input.maybe_quote(), + ); + set_exit_code(1); + } + + // strict means that we should have an exit code. + if opts.strict && res.bad_format > 0 { + set_exit_code(1); + } + + // 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); + } + + Ok(()) +} + /*** * Do the checksum validation (can be strict or not) */ -#[allow(clippy::too_many_arguments)] pub fn perform_checksum_validation<'a, I>( files: I, - strict: bool, - status: bool, - warn: bool, - binary: bool, - ignore_missing: bool, - quiet: bool, algo_name_input: Option<&str>, length_input: Option, + opts: ChecksumOptions, ) -> UResult<()> where I: Iterator, { // if cksum has several input files, it will print the result for each file for filename_input in files { - let mut correct_format = 0; - let mut properly_formatted = false; - let mut res = ChecksumResult::default(); - let input_is_stdin = filename_input == OsStr::new("-"); - - let file: Box = if input_is_stdin { - // Use stdin if "-" is specified - Box::new(stdin()) - } else { - match get_input_file(filename_input) { - Ok(f) => f, - Err(e) => { - // Could not read the file, show the error and continue to the next file - show_error!("{e}"); - set_exit_code(1); - continue; - } - } - }; - - let reader = BufReader::new(file); - let lines = read_os_string_lines(reader).collect::>(); - - let Some((chosen_regex, is_algo_based_format)) = determine_regex(&lines) else { - let e = ChecksumError::NoProperlyFormattedChecksumLinesFound { - filename: get_filename_for_output(filename_input, input_is_stdin), - }; - show_error!("{e}"); - set_exit_code(1); - continue; - }; - - for (i, line) in lines.iter().enumerate() { - let line_bytes = os_str_as_bytes(line)?; - if let Some(caps) = chosen_regex.captures(line_bytes) { - properly_formatted = true; - - let mut filename_to_check = caps.name("filename").unwrap().as_bytes(); - - if filename_to_check.starts_with(b"*") - && i == 0 - && chosen_regex.as_str() == SINGLE_SPACE_REGEX - { - // Remove the leading asterisk if present - only for the first line - filename_to_check = &filename_to_check[1..]; - } - - let expected_checksum = - get_expected_checksum(filename_to_check, &caps, &chosen_regex)?; - - // If the algo_name is provided, we use it, otherwise we try to detect it - let (algo_name, length) = if is_algo_based_format { - identify_algo_name_and_length( - &caps, - algo_name_input, - &mut res, - &mut properly_formatted, - ) - .unwrap_or((String::new(), None)) - } else if let Some(a) = algo_name_input { - // When a specific algorithm name is input, use it and use the provided bits - // except when dealing with blake2b, where we will detect the length - if algo_name_input == Some(ALGORITHM_OPTIONS_BLAKE2B) { - // division by 2 converts the length of the Blake2b checksum from hexadecimal - // characters to bytes, as each byte is represented by two hexadecimal characters. - let length = Some(expected_checksum.len() / 2); - (ALGORITHM_OPTIONS_BLAKE2B.to_string(), length) - } else { - (a.to_lowercase(), length_input) - } - } else { - // Default case if no algorithm is specified and non-algo based format is matched - (String::new(), None) - }; - - if algo_name.is_empty() { - // we haven't been able to detect the algo name. No point to continue - properly_formatted = false; - continue; - } - let mut algo = detect_algo(&algo_name, length)?; - - let (filename_to_check_unescaped, prefix) = unescape_filename(filename_to_check); - - let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?; - - // manage the input file - let file_to_check = - match get_file_to_check(&real_filename_to_check, ignore_missing, &mut res) { - Some(file) => file, - None => continue, - }; - let mut file_reader = BufReader::new(file_to_check); - // Read the file and calculate the checksum - let create_fn = &mut algo.create_fn; - let mut digest = create_fn(); - let (calculated_checksum, _) = - digest_reader(&mut digest, &mut file_reader, binary, algo.bits).unwrap(); - - // Do the checksum validation - if expected_checksum == calculated_checksum { - if !quiet && !status { - print_file_report( - std::io::stdout(), - filename_to_check, - FileChecksumResult::Ok, - prefix, - ); - } - correct_format += 1; - } else { - if !status { - print_file_report( - std::io::stdout(), - filename_to_check, - FileChecksumResult::Failed, - prefix, - ); - } - res.failed_cksum += 1; - } - } else { - if line.is_empty() || line_bytes.starts_with(b"#") { - // Don't show any warning for empty or commented lines. - continue; - } - if warn { - let algo = if let Some(algo_name_input) = algo_name_input { - algo_name_input.to_uppercase() - } else { - "Unknown algorithm".to_string() - }; - eprintln!( - "{}: {}: {}: improperly formatted {} checksum line", - util_name(), - &filename_input.maybe_quote(), - i + 1, - algo - ); - } - - res.bad_format += 1; - } - } - - // not a single line correctly formatted found - // return an error - if !properly_formatted { - if !status { - return Err(ChecksumError::NoProperlyFormattedChecksumLinesFound { - filename: get_filename_for_output(filename_input, input_is_stdin), - } - .into()); - } - set_exit_code(1); - - return Ok(()); - } - - // if any incorrectly formatted line, show it - cksum_output(&res, status); - - if ignore_missing && correct_format == 0 { - // we have only bad format - // and we had ignore-missing - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); - set_exit_code(1); - } - - // strict means that we should have an exit code. - if strict && res.bad_format > 0 { - set_exit_code(1); - } - - // 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) && !ignore_missing { - set_exit_code(1); + use FileCheckError::*; + match process_checksum_file(filename_input, algo_name_input, length_input, opts) { + Err(UError(e)) => return Err(e), + Err(CriticalError) => break, + Err(NonCriticalError) | Ok(_) => continue, } } @@ -1139,6 +1276,8 @@ mod tests { #[test] fn test_print_file_report() { + let opts = ChecksumOptions::default(); + let cases: &[(&[u8], FileChecksumResult, &str, &[u8])] = &[ (b"filename", FileChecksumResult::Ok, "", b"filename: OK\n"), ( @@ -1169,7 +1308,7 @@ mod tests { for (filename, result, prefix, expected) in cases { let mut buffer: Vec = vec![]; - print_file_report(&mut buffer, filename, *result, prefix); + print_file_report(&mut buffer, filename, *result, prefix, opts); assert_eq!(&buffer, expected) } } diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 98366cbec..ee1e05292 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 +// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb use crate::common::util::TestScenario; @@ -1502,3 +1502,260 @@ mod check_utf8 { .stderr_contains("1 listed file could not be read"); } } + +#[ignore = "not yet implemented"] +#[test] +fn test_check_blake_length_guess() { + let correct_lines = [ + // Correct: The length is not explicit, but the checksum's size + // matches the default parameter. + "BLAKE2b (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d", + // Correct: The length is explicitly given, and the checksum's size + // matches the length. + "BLAKE2b-512 (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d", + // Correct: the checksum size is not default but + // the length is explicitly given. + "BLAKE2b-48 (foo.dat) = 171cdfdf84ed", + ]; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo.dat", "foo"); + + for line in correct_lines { + at.write("foo.sums", line); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .succeeds() + .stdout_is("foo.dat: OK\n"); + } + + // Incorrect lines + + // This is incorrect because the algorithm provides no length, + // and the checksum length is not default. + let incorrect = "BLAKE2b (foo.dat) = 171cdfdf84ed"; + at.write("foo.sums", incorrect); + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .fails() + .stderr_contains("foo.sums: no properly formatted checksum lines found"); +} + +#[ignore = "not yet implemented"] +#[test] +fn test_check_confusing_base64() { + let cksum = "BLAKE2b-48 (foo.dat) = fc1f97C4"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo.dat", "esq"); + at.write("foo.sums", cksum); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("foo.sums")) + .succeeds() + .stdout_is("foo.dat: OK\n"); +} + +/// This test checks that when a file contains several checksum lines +/// with different encoding, the decoding still works. +#[ignore = "not yet implemented"] +#[test] +fn test_check_mix_hex_base64() { + let b64 = "BLAKE2b-128 (foo1.dat) = BBNuJPhdRwRlw9tm5Y7VbA=="; + let hex = "BLAKE2b-128 (foo2.dat) = 04136e24f85d470465c3db66e58ed56c"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo1.dat", "foo"); + at.write("foo2.dat", "foo"); + + at.write("hex_b64", &format!("{hex}\n{b64}")); + at.write("b64_hex", &format!("{b64}\n{hex}")); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("hex_b64")) + .succeeds() + .stdout_only("foo2.dat: OK\nfoo1.dat: OK\n"); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("b64_hex")) + .succeeds() + .stdout_only("foo1.dat: OK\nfoo2.dat: OK\n"); +} + +#[ignore = "not yet implemented"] +#[test] +fn test_check_incorrectly_formatted_checksum_does_not_stop_processing() { + // The first line contains an incorrectly formatted checksum that can't be + // correctly decoded. This must not prevent the program from looking at the + // rest of the file. + let lines = [ + "BLAKE2b-56 (foo1) = GFYEQ7HhAw=", // Should be 2 '=' at the end + "BLAKE2b-56 (foo2) = 18560443b1e103", // OK + ]; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo1", "foo"); + at.write("foo2", "foo"); + at.write("sum", &lines.join("\n")); + + scene + .ucmd() + .arg("--check") + .arg(at.subdir.join("sum")) + .succeeds() + .stderr_contains("1 line is improperly formatted") + .stdout_contains("foo2: OK"); +} + +/// This module reimplements the cksum-base64.pl GNU test. +mod cksum_base64 { + use super::*; + use crate::common::util::log_info; + + const PAIRS: [(&str, &str); 11] = [ + ("sysv", "0 0 f"), + ("bsd", "00000 0 f"), + ("crc", "4294967295 0 f"), + ("md5", "1B2M2Y8AsgTpgAmY7PhCfg=="), + ("sha1", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="), + ("sha224", "0UoCjCo6K8lHYQK7KII0xBWisB+CjqYqxbPkLw=="), + ("sha256", "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="), + ( + "sha384", + "OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb", + ), + ( + "sha512", + "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" + ), + ( + "blake2b", + "eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg==" + ), + ("sm3", "GrIdg1XPoX+OYRlIMegajyK+yMco/vt0ftA161CCqis="), + ]; + + fn make_scene() -> TestScenario { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("f"); + + scene + } + + fn output_format(algo: &str, digest: &str) -> String { + if ["sysv", "bsd", "crc"].contains(&algo) { + digest.to_string() + } else { + format!("{} (f) = {}", algo.to_uppercase(), digest).replace("BLAKE2B", "BLAKE2b") + } + } + + #[test] + fn test_generating() { + // Ensure that each algorithm works with `--base64`. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + scene + .ucmd() + .arg("--base64") + .arg("-a") + .arg(algo) + .arg("f") + .succeeds() + .stdout_only(format!("{}\n", output_format(algo, digest))); + } + } + + #[test] + fn test_chk() { + // For each algorithm that accepts `--check`, + // ensure that it works with base64 digests. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + if ["sysv", "bsd", "crc"].contains(&algo) { + // These algorithms do not accept `--check` + continue; + } + + let line = output_format(algo, digest); + scene + .ucmd() + .arg("--check") + .arg("--strict") + .pipe_in(line) + .succeeds() + .stdout_only("f: OK\n"); + } + } + + #[test] + fn test_chk_eq1() { + // For digests ending with '=', ensure `--check` fails if '=' is removed. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + if !digest.ends_with('=') { + continue; + } + + let mut line = output_format(algo, digest); + if line.ends_with('=') { + line.pop(); + } + + log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), ""); + scene + .ucmd() + .arg("--check") + .pipe_in(line) + .fails() + .no_stdout() + .stderr_contains("no properly formatted checksum lines found"); + } + } + + #[test] + fn test_chk_eq2() { + // For digests ending with '==', + // ensure `--check` fails if '==' is removed. + let scene = make_scene(); + + for (algo, digest) in PAIRS { + if !digest.ends_with("==") { + continue; + } + + let line = output_format(algo, digest); + let line = line.trim_end_matches("=="); + + log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), ""); + scene + .ucmd() + .arg("--check") + .pipe_in(line) + .fails() + .no_stdout() + .stderr_contains("no properly formatted checksum lines found"); + } + } +}