diff --git a/Cargo.lock b/Cargo.lock index 93bb57148..672e0827b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2494,6 +2494,7 @@ version = "0.0.26" dependencies = [ "clap", "hex", + "regex", "uucore", ] diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index f6a5a138a..208c01901 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -16,8 +16,9 @@ path = "src/cksum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["encoding", "sum"] } +uucore = { workspace = true, features = ["checksum", "encoding", "sum"] } hex = { workspace = true } +regex = { workspace = true } [[bin]] name = "cksum" diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index a50abada5..9268a40bb 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -5,15 +5,18 @@ // spell-checker:ignore (ToDO) fname, algo use clap::{crate_version, value_parser, Arg, ArgAction, Command}; -use hex::decode; -use hex::encode; +use regex::Regex; use std::error::Error; use std::ffi::OsStr; use std::fmt::Display; use std::fs::File; +use std::io::BufRead; use std::io::{self, stdin, stdout, BufReader, Read, Write}; use std::iter; use std::path::Path; +use uucore::checksum::cksum_output; +use uucore::display::Quotable; +use uucore::error::set_exit_code; use uucore::{ encoding, error::{FromIo, UError, UResult, USimpleError}, @@ -40,6 +43,20 @@ const ALGORITHM_OPTIONS_SHA512: &str = "sha512"; const ALGORITHM_OPTIONS_BLAKE2B: &str = "blake2b"; const ALGORITHM_OPTIONS_SM3: &str = "sm3"; +const SUPPORTED_ALGO: [&str; 11] = [ + ALGORITHM_OPTIONS_SYSV, + ALGORITHM_OPTIONS_BSD, + ALGORITHM_OPTIONS_CRC, + ALGORITHM_OPTIONS_MD5, + ALGORITHM_OPTIONS_SHA1, + ALGORITHM_OPTIONS_SHA224, + ALGORITHM_OPTIONS_SHA256, + ALGORITHM_OPTIONS_SHA384, + ALGORITHM_OPTIONS_SHA512, + ALGORITHM_OPTIONS_BLAKE2B, + ALGORITHM_OPTIONS_SM3, +]; + #[derive(Debug)] enum CkSumError { RawMultipleFiles, @@ -73,10 +90,10 @@ impl Display for CkSumError { } fn detect_algo( - program: &str, + algo: &str, length: Option, ) -> (&'static str, Box, usize) { - match program { + match algo { ALGORITHM_OPTIONS_SYSV => ( ALGORITHM_OPTIONS_SYSV, Box::new(SYSV::new()) as Box, @@ -205,7 +222,7 @@ where ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => { sum_hex.parse::().unwrap().to_be_bytes().to_vec() } - _ => decode(sum_hex).unwrap(), + _ => hex::decode(sum_hex).unwrap(), }; // Cannot handle multiple files anyway, output immediately. stdout().write_all(&bytes)?; @@ -214,7 +231,8 @@ where OutputFormat::Hexadecimal => sum_hex, OutputFormat::Base64 => match options.algo_name { ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => sum_hex, - _ => encoding::encode(encoding::Format::Base64, &decode(sum_hex).unwrap()).unwrap(), + _ => encoding::encode(encoding::Format::Base64, &hex::decode(sum_hex).unwrap()) + .unwrap(), }, }; // The BSD checksum output is 5 digit integer @@ -299,7 +317,7 @@ fn digest_read( // Assume it's SHAKE. result_str() doesn't work with shake (as of 8/30/2016) let mut bytes = vec![0; (output_bits + 7) / 8]; digest.hash_finalize(&mut bytes); - Ok((encode(bytes), output_size)) + Ok((hex::encode(bytes), output_size)) } } @@ -312,6 +330,7 @@ mod options { pub const RAW: &str = "raw"; pub const BASE64: &str = "base64"; pub const CHECK: &str = "check"; + pub const STRICT: &str = "strict"; pub const TEXT: &str = "text"; pub const BINARY: &str = "binary"; } @@ -353,17 +372,45 @@ fn had_reset(args: &[String]) -> bool { } } +/// Calculates the length of the digest for the given algorithm. +fn calculate_blake2b_length(length: usize) -> UResult> { + match length { + 0 => Ok(None), + n if n % 8 != 0 => { + uucore::show_error!("invalid length: \u{2018}{length}\u{2019}"); + Err(io::Error::new(io::ErrorKind::InvalidInput, "length is not a multiple of 8").into()) + } + n if n > 512 => { + uucore::show_error!("invalid length: \u{2018}{length}\u{2019}"); + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "maximum digest length for \u{2018}BLAKE2b\u{2019} is 512 bits", + ) + .into()) + } + n => { + // Divide by 8, as our blake2b implementation expects bytes instead of bits. + if n == 512 { + // When length is 512, it is blake2b's default. + // So, don't show it + Ok(None) + } else { + Ok(Some(n / 8)) + } + } + } +} + /*** * cksum has a bunch of legacy behavior. * We handle this in this function to make sure they are self contained * and "easier" to understand */ -fn handle_tag_text_binary_flags(matches: &clap::ArgMatches, check: bool) -> UResult<(bool, bool)> { +fn handle_tag_text_binary_flags(matches: &clap::ArgMatches) -> UResult<(bool, bool)> { let untagged: bool = matches.get_flag(options::UNTAGGED); let tag: bool = matches.get_flag(options::TAG); let tag: bool = tag || !untagged; - let text_flag: bool = matches.get_flag(options::TEXT); let binary_flag: bool = matches.get_flag(options::BINARY); let args: Vec = std::env::args().collect(); @@ -371,70 +418,209 @@ fn handle_tag_text_binary_flags(matches: &clap::ArgMatches, check: bool) -> URes let asterisk: bool = prompt_asterisk(tag, binary_flag, had_reset); - if (binary_flag || text_flag) && check { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "the --binary and --text options are meaningless when verifying checksums", - ) - .into()); - } Ok((tag, asterisk)) } +/*** + * Do the checksum validation (can be strict or not) +*/ +fn perform_checksum_validation<'a, I>( + files: I, + strict: bool, + algo_name_input: Option<&str>, +) -> UResult<()> +where + I: Iterator, +{ + // Regexp to handle the two input formats: + // 1. [-] () = + // algo must be uppercase or b (for blake2b) + // 2. [* ] + let regex_pattern = r"^\s*\\?(?P(?:[A-Z0-9]+|BLAKE2b))(?:-(?P\d+))?\s?\((?P.*)\) = (?P[a-fA-F0-9]+)$|^(?P[a-fA-F0-9]+)\s[* ](?P.*)"; + let re = Regex::new(regex_pattern).unwrap(); + + // if cksum has several input files, it will print the result for each file + for filename_input in files { + let mut bad_format = 0; + let mut failed_cksum = 0; + let mut failed_open_file = 0; + let mut properly_formatted = false; + let input_is_stdin = filename_input == OsStr::new("-"); + + let file: Box = if input_is_stdin { + Box::new(stdin()) // Use stdin if "-" is specified + } else { + match File::open(filename_input) { + Ok(f) => Box::new(f), + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "{}: No such file or directory", + filename_input.to_string_lossy() + ), + ) + .into()); + } + } + }; + let reader = BufReader::new(file); + + // for each line in the input, check if it is a valid checksum line + for line in reader.lines() { + let line = line.unwrap_or_else(|_| String::new()); + if let Some(caps) = re.captures(&line) { + properly_formatted = true; + + // Determine what kind of file input we had + // we need it for case "--check -a sm3 " when is + // [-] () = + let algo_based_format = + caps.name("filename1").is_some() && caps.name("checksum1").is_some(); + + let filename_to_check = caps + .name("filename1") + .or(caps.name("filename2")) + .unwrap() + .as_str(); + let expected_checksum = caps + .name("checksum1") + .or(caps.name("checksum2")) + .unwrap() + .as_str(); + + // If the algo_name is provided, we use it, otherwise we try to detect it + let (algo_name, length) = if algo_based_format { + // When the algo-based format is matched, extract details from regex captures + let algorithm = caps.name("algo").map_or("", |m| m.as_str()).to_lowercase(); + if !SUPPORTED_ALGO.contains(&algorithm.as_str()) { + // Not supported algo, leave early + properly_formatted = false; + continue; + } + + let bits = caps.name("bits").map_or(Some(None), |m| { + let bits_value = m.as_str().parse::().unwrap(); + if bits_value % 8 == 0 { + Some(Some(bits_value / 8)) + } else { + properly_formatted = false; + None // Return None to signal a parsing or divisibility issue + } + }); + + if bits.is_none() { + // If bits is None, we have a parsing or divisibility issue + // Exit the loop outside of the closure + continue; + } + + (algorithm, bits.unwrap()) + } else if let Some(a) = algo_name_input { + // When a specific algorithm name is input, use it and default bits to None + (a.to_lowercase(), None) + } else { + // Default case if no algorithm is specified and non-algo based format is matched + (String::new(), None) + }; + + if algo_based_format && algo_name_input.map_or(false, |input| algo_name != input) { + bad_format += 1; + continue; + } + + 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, bits) = detect_algo(&algo_name, length); + + // manage the input file + let file_to_check: Box = if filename_to_check == "-" { + Box::new(stdin()) // Use stdin if "-" is specified in the checksum file + } else { + match File::open(filename_to_check) { + Ok(f) => Box::new(f), + Err(err) => { + // yes, we have both stderr and stdout here + show!(err.map_err_context(|| filename_to_check.to_string())); + println!("{}: FAILED open or read", filename_to_check); + failed_open_file += 1; + // we could not open the file but we want to continue + continue; + } + } + }; + let mut file_reader = BufReader::new(file_to_check); + // Read the file and calculate the checksum + let (calculated_checksum, _) = + digest_read(&mut algo, &mut file_reader, bits).unwrap(); + + // Do the checksum validation + if expected_checksum == calculated_checksum { + println!("{}: OK", filename_to_check); + } else { + println!("{}: FAILED", filename_to_check); + failed_cksum += 1; + } + } else { + if line.is_empty() { + continue; + } + bad_format += 1; + } + } + + // not a single line correctly formatted found + // return an error + if !properly_formatted { + let filename = filename_input.to_string_lossy(); + uucore::show_error!( + "{}: no properly formatted checksum lines found", + if input_is_stdin { + "standard input" + } else { + &filename + } + .maybe_quote() + ); + set_exit_code(1); + } + // strict means that we should have an exit code. + if strict && bad_format > 0 { + set_exit_code(1); + } + + // if we have any failed checksum verification, we set an exit code + if failed_cksum > 0 || failed_open_file > 0 { + set_exit_code(1); + } + + // if any incorrectly formatted line, show it + cksum_output(bad_format, failed_cksum, failed_open_file); + } + Ok(()) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let algo_name: &str = match matches.get_one::(options::ALGORITHM) { - Some(v) => v, - None => ALGORITHM_OPTIONS_CRC, - }; - - let input_length = matches.get_one::(options::LENGTH); let check = matches.get_flag(options::CHECK); - let length = if let Some(length) = input_length { - match length.to_owned() { - 0 => None, - n if n % 8 != 0 => { - // GNU's implementation seem to use these quotation marks - // in their error messages, so we do the same. - uucore::show_error!("invalid length: \u{2018}{length}\u{2019}"); - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "length is not a multiple of 8", - ) - .into()); - } - n if n > 512 => { - uucore::show_error!("invalid length: \u{2018}{length}\u{2019}"); - - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "maximum digest length for \u{2018}BLAKE2b\u{2019} is 512 bits", - ) - .into()); - } - n => { - if algo_name != ALGORITHM_OPTIONS_BLAKE2B { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "--length is only supported with --algorithm=blake2b", - ) - .into()); - } - - // Divide by 8, as our blake2b implementation expects bytes - // instead of bits. - Some(n / 8) + let algo_name: &str = match matches.get_one::(options::ALGORITHM) { + Some(v) => v, + None => { + if check { + // if we are doing a --check, we should not default to crc + "" + } else { + ALGORITHM_OPTIONS_CRC } } - } else { - None }; - let (tag, asterisk) = handle_tag_text_binary_flags(&matches, check)?; - if ["bsd", "crc", "sysv"].contains(&algo_name) && check { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -443,6 +629,51 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .into()); } + let input_length = matches.get_one::(options::LENGTH); + + let length = match input_length { + Some(length) => { + if algo_name == ALGORITHM_OPTIONS_BLAKE2B { + calculate_blake2b_length(*length)? + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "--length is only supported with --algorithm=blake2b", + ) + .into()); + } + } + None => None, + }; + + if check { + let text_flag: bool = matches.get_flag(options::TEXT); + let binary_flag: bool = matches.get_flag(options::BINARY); + let strict = matches.get_flag(options::STRICT); + + if (binary_flag || text_flag) && check { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "the --binary and --text options are meaningless when verifying checksums", + ) + .into()); + } + // Determine the appropriate algorithm option to pass + let algo_option = if algo_name.is_empty() { + None + } else { + Some(algo_name) + }; + + // Execute the checksum validation based on the presence of files or the use of stdin + return match matches.get_many::(options::FILE) { + Some(files) => perform_checksum_validation(files.map(OsStr::new), strict, algo_option), + None => perform_checksum_validation(iter::once(OsStr::new("-")), strict, algo_option), + }; + } + + let (tag, asterisk) = handle_tag_text_binary_flags(&matches)?; + let (name, algo, bits) = detect_algo(algo_name, length); let output_format = if matches.get_flag(options::RAW) { @@ -490,19 +721,7 @@ pub fn uu_app() -> Command { .short('a') .help("select the digest type to use. See DIGEST below") .value_name("ALGORITHM") - .value_parser([ - ALGORITHM_OPTIONS_SYSV, - ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_MD5, - ALGORITHM_OPTIONS_SHA1, - ALGORITHM_OPTIONS_SHA224, - ALGORITHM_OPTIONS_SHA256, - ALGORITHM_OPTIONS_SHA384, - ALGORITHM_OPTIONS_SHA512, - ALGORITHM_OPTIONS_BLAKE2B, - ALGORITHM_OPTIONS_SM3, - ]), + .value_parser(SUPPORTED_ALGO), ) .arg( Arg::new(options::UNTAGGED) @@ -535,12 +754,12 @@ pub fn uu_app() -> Command { .help("emit a raw binary digest, not hexadecimal") .action(ArgAction::SetTrue), ) - /*.arg( + .arg( Arg::new(options::STRICT) .long(options::STRICT) .help("exit non-zero for improperly formatted checksum lines") .action(ArgAction::SetTrue), - )*/ + ) .arg( Arg::new(options::CHECK) .short('c') @@ -580,6 +799,7 @@ pub fn uu_app() -> Command { #[cfg(test)] mod tests { use super::had_reset; + use crate::calculate_blake2b_length; use crate::prompt_asterisk; #[test] @@ -646,4 +866,16 @@ mod tests { assert!(prompt_asterisk(false, true, false)); assert!(!prompt_asterisk(false, false, false)); } + + #[test] + fn test_calculate_length() { + assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); + assert_eq!(calculate_blake2b_length(512).unwrap(), None); + assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); + calculate_blake2b_length(255).unwrap_err(); + + calculate_blake2b_length(33).unwrap_err(); + + calculate_blake2b_length(513).unwrap_err(); + } } diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index f3799aedb..d450e3cef 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -16,7 +16,7 @@ path = "src/hashsum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["sum"] } +uucore = { workspace = true, features = ["checksum", "sum"] } memchr = { workspace = true } regex = { workspace = true } hex = { workspace = true } diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 892f3a15b..6aaa2047b 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -12,7 +12,6 @@ use clap::{Arg, ArgMatches, Command}; use hex::encode; use regex::Captures; use regex::Regex; -use std::cmp::Ordering; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::fs::File; @@ -20,6 +19,8 @@ use std::io::{self, stdin, BufRead, BufReader, Read}; use std::iter; use std::num::ParseIntError; use std::path::Path; +use uucore::checksum::cksum_output; +use uucore::display::Quotable; use uucore::error::USimpleError; use uucore::error::{set_exit_code, FromIo, UError, UResult}; use uucore::sum::{ @@ -27,7 +28,6 @@ use uucore::sum::{ Sha3_384, Sha3_512, Sha512, Shake128, Shake256, }; use uucore::util_name; -use uucore::{display::Quotable, show_warning_caps}; use uucore::{format_usage, help_about, help_usage}; const NAME: &str = "hashsum"; @@ -835,35 +835,7 @@ where } if !options.status && !skip_summary { - match bad_format.cmp(&1) { - Ordering::Equal => { - show_warning_caps!("{} line is improperly formatted", bad_format); - } - Ordering::Greater => { - show_warning_caps!("{} lines are improperly formatted", bad_format); - } - Ordering::Less => {} - }; - - match failed_cksum.cmp(&1) { - Ordering::Equal => { - show_warning_caps!("{} computed checksum did NOT match", failed_cksum); - } - Ordering::Greater => { - show_warning_caps!("{} computed checksums did NOT match", failed_cksum); - } - Ordering::Less => {} - }; - - match failed_open_file.cmp(&1) { - Ordering::Equal => { - show_warning_caps!("{} listed file could not be read", failed_open_file); - } - Ordering::Greater => { - show_warning_caps!("{} listed files could not be read", failed_open_file); - } - Ordering::Less => {} - } + cksum_output(bad_format, failed_cksum, failed_open_file); } Ok(()) diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 2e7140b6b..81d202398 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -75,6 +75,7 @@ default = [] # * non-default features backup-control = [] colors = [] +checksum = [] encoding = ["data-encoding", "data-encoding-macro", "z85", "thiserror"] entries = ["libc"] fs = ["dunce", "libc", "winapi-util", "windows-sys"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 423ff34ba..ef7b17b31 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -6,6 +6,8 @@ #[cfg(feature = "backup-control")] pub mod backup_control; +#[cfg(feature = "checksum")] +pub mod checksum; #[cfg(feature = "colors")] pub mod colors; #[cfg(feature = "encoding")] diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs new file mode 100644 index 000000000..1652ce47c --- /dev/null +++ b/src/uucore/src/lib/features/checksum.rs @@ -0,0 +1,27 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::show_warning_caps; + +#[allow(clippy::comparison_chain)] +pub fn cksum_output(bad_format: i32, failed_cksum: i32, failed_open_file: i32) { + if bad_format == 1 { + show_warning_caps!("{} line is improperly formatted", bad_format); + } else if bad_format > 1 { + show_warning_caps!("{} lines are improperly formatted", bad_format); + } + + if failed_cksum == 1 { + show_warning_caps!("{} computed checksum did NOT match", failed_cksum); + } else if failed_cksum > 1 { + show_warning_caps!("{} computed checksums did NOT match", failed_cksum); + } + + if failed_open_file == 1 { + show_warning_caps!("{} listed file could not be read", failed_open_file); + } else if failed_open_file > 1 { + show_warning_caps!("{} listed files could not be read", failed_open_file); + } +} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index e891cc404..366c420d9 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -37,6 +37,8 @@ pub use crate::parser::shortcut_value_parser; // * feature-gated modules #[cfg(feature = "backup-control")] pub use crate::features::backup_control; +#[cfg(feature = "checksum")] +pub use crate::features::checksum; #[cfg(feature = "colors")] pub use crate::features::colors; #[cfg(feature = "encoding")] diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index bed35c95e..19ea2badd 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -80,6 +80,21 @@ fn test_nonexisting_file() { .stderr_contains(format!("cksum: {file_name}: No such file or directory")); } +#[test] +fn test_nonexisting_file_out() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write( + "f", + "MD5 (nonexistent) = e5773576fc75ff0f8eba14f61587ae28\n", + ); + + ucmd.arg("-c") + .arg("f") + .fails() + .stdout_contains("nonexistent: FAILED open or read") + .stderr_contains("cksum: nonexistent: No such file or directory"); +} + #[test] fn test_one_nonexisting_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -316,6 +331,16 @@ fn test_length_with_wrong_algorithm() { .no_stdout() .stderr_contains("cksum: --length is only supported with --algorithm=blake2b") .code_is(1); + + new_ucmd!() + .arg("--length=16") + .arg("--algorithm=md5") + .arg("-c") + .arg("foo.sums") + .fails() + .no_stdout() + .stderr_contains("cksum: --length is only supported with --algorithm=blake2b") + .code_is(1); } #[test] @@ -325,7 +350,7 @@ fn test_length_not_supported() { .arg("lorem_ipsum.txt") .fails() .no_stdout() - .stderr_is_fixture("unsupported_length.expected") + .stderr_contains("--length is only supported with --algorithm=blake2b") .code_is(1); } @@ -337,7 +362,9 @@ fn test_length() { .arg("lorem_ipsum.txt") .arg("alice_in_wonderland.txt") .succeeds() - .stdout_is_fixture("supported_length.expected"); + .stdout_contains( + "BLAKE2b-16 (lorem_ipsum.txt) = 7e2f\nBLAKE2b-16 (alice_in_wonderland.txt) = a546", + ); } #[test] @@ -474,11 +501,23 @@ fn test_all_algorithms_fail_on_folder() { #[cfg(unix)] #[test] -fn test_dev_null() { +fn test_check_error_incorrect_format() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; + at.write("checksum", "e5773576fc75ff0f8eba14f61587ae28 README.md"); - at.touch("f"); + scene + .ucmd() + .arg("-c") + .arg("checksum") + .fails() + .stderr_contains("no properly formatted checksum lines found"); +} + +#[cfg(unix)] +#[test] +fn test_dev_null() { + let scene = TestScenario::new(util_name!()); scene .ucmd() @@ -490,6 +529,33 @@ fn test_dev_null() { .stdout_contains("d41d8cd98f00b204e9800998ecf8427e "); } +#[cfg(unix)] +#[test] +fn test_blake2b_512() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + + scene + .ucmd() + .arg("-a") + .arg("blake2b") + .arg("-l512") + .arg("f") + .succeeds() + .stdout_contains("BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"); + + // test also the read + at.write("checksum", "BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"); + scene + .ucmd() + .arg("--check") + .arg("checksum") + .succeeds() + .stdout_contains("f: OK"); +} + #[test] fn test_reset_binary() { let scene = TestScenario::new(util_name!()); @@ -663,3 +729,442 @@ fn test_conflicting_options() { ) .code_is(1); } + +#[test] +fn test_check_algo_err() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + + at.touch("f"); + + scene + .ucmd() + .arg("-a") + .arg("sm3") + .arg("--check") + .arg("f") + .fails() + .no_stdout() + .stderr_contains("cksum: f: no properly formatted checksum lines found") + .code_is(1); +} + +#[test] +fn test_check_pipe() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + + at.touch("f"); + + scene + .ucmd() + .arg("--check") + .arg("-") + .pipe_in("f") + .fails() + .no_stdout() + .stderr_contains("cksum: 'standard input': no properly formatted checksum lines found") + .code_is(1); +} + +#[test] +fn test_cksum_check_empty_line() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("CHECKSUM", "\ + SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\ + BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\ + BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\ + SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n\n"); + scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n") + .stderr_does_not_contain("line is improperly formatted"); +} + +#[test] +fn test_cksum_check_space() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("CHECKSUM", "\ + SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\ + BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\ + BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\ + SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n \n"); + scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n") + .stderr_contains("line is improperly formatted"); +} + +#[test] +fn test_cksum_check_leading_info() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("CHECKSUM", "\ + \\SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\ + \\BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\ + \\BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\ + \\SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n"); + scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n"); +} + +#[test] +fn test_cksum_check() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("CHECKSUM", "\ + SHA384 (f) = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\n\ + BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n\ + BLAKE2b-384 (f) = b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100\n\ + SM3 (f) = 1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b\n"); + scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n") + .stderr_does_not_contain("line is improperly formatted"); + scene + .ucmd() + .arg("--check") + .arg("--strict") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n") + .stderr_does_not_contain("line is improperly formatted"); + // inject invalid content + at.append("CHECKSUM", "incorrect data"); + scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n") + .stderr_contains("line is improperly formatted"); + scene + .ucmd() + .arg("--check") + .arg("--strict") + .arg("CHECKSUM") + .fails() + .stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n") + .stderr_contains("line is improperly formatted"); +} + +#[test] +fn test_cksum_check_case() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write( + "CHECKSUM", + "Sha1 (f) = da39a3ee5e6b4b0d3255bfef95601890afd80709\n", + ); + scene.ucmd().arg("--check").arg("CHECKSUM").fails(); +} + +#[test] +fn test_cksum_check_invalid() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let commands = [vec!["-a", "sha384"]]; + at.touch("f"); + at.touch("CHECKSUM"); + for command in &commands { + let result = scene.ucmd().args(command).arg("f").succeeds(); + at.append("CHECKSUM", result.stdout_str()); + } + // inject invalid content + at.append("CHECKSUM", "again incorrect data\naze\n"); + scene + .ucmd() + .arg("--check") + .arg("--strict") + .arg("CHECKSUM") + .fails() + .stdout_contains("f: OK\n") + .stderr_contains("2 lines"); + + // without strict, it passes + scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK\n") + .stderr_contains("2 lines"); +} + +#[test] +fn test_cksum_check_failed() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let commands = [vec!["-a", "sha384"]]; + at.touch("f"); + at.touch("CHECKSUM"); + for command in &commands { + let result = scene.ucmd().args(command).arg("f").succeeds(); + at.append("CHECKSUM", result.stdout_str()); + } + // inject invalid content + at.append("CHECKSUM", "again incorrect data\naze\nSM3 (input) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n"); + + let result = scene + .ucmd() + .arg("--check") + .arg("--strict") + .arg("CHECKSUM") + .fails(); + + assert!(result + .stderr_str() + .contains("input: No such file or directory")); + assert!(result + .stderr_str() + .contains("2 lines are improperly formatted\n")); + assert!(result + .stderr_str() + .contains("1 listed file could not be read\n")); + assert!(result.stdout_str().contains("f: OK\n")); + + // without strict + let result = scene.ucmd().arg("--check").arg("CHECKSUM").fails(); + + assert!(result + .stderr_str() + .contains("input: No such file or directory")); + assert!(result + .stderr_str() + .contains("2 lines are improperly formatted\n")); + assert!(result + .stderr_str() + .contains("1 listed file could not be read\n")); + assert!(result.stdout_str().contains("f: OK\n")); + + // tests with two files + at.touch("CHECKSUM2"); + at.write("f2", "42"); + for command in &commands { + let result = scene.ucmd().args(command).arg("f2").succeeds(); + at.append("CHECKSUM2", result.stdout_str()); + } + // inject invalid content + at.append("CHECKSUM2", "again incorrect data\naze\nSM3 (input2) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n"); + at.append("CHECKSUM2", "again incorrect data\naze\nSM3 (input2) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n"); + + let result = scene + .ucmd() + .arg("--check") + .arg("CHECKSUM") + .arg("CHECKSUM2") + .fails(); + println!("result.stderr_str() {}", result.stderr_str()); + println!("result.stdout_str() {}", result.stdout_str()); + assert!(result + .stderr_str() + .contains("input2: No such file or directory")); + assert!(result + .stderr_str() + .contains("4 lines are improperly formatted\n")); + assert!(result + .stderr_str() + .contains("2 listed files could not be read\n")); + assert!(result.stdout_str().contains("f: OK\n")); + assert!(result.stdout_str().contains("2: OK\n")); +} + +#[test] +fn test_check_md5_format() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.touch("empty"); + at.write("f", "d41d8cd98f00b204e9800998ecf8427e *empty\n"); + + scene + .ucmd() + .arg("-a") + .arg("md5") + .arg("--check") + .arg("f") + .succeeds() + .stdout_contains("empty: OK"); + + // with a second file + at.write("not-empty", "42"); + at.write("f2", "a1d0c6e83f027327d8461063f4ac58a6 *not-empty\n"); + + scene + .ucmd() + .arg("-a") + .arg("md5") + .arg("--check") + .arg("f") + .arg("f2") + .succeeds() + .stdout_contains("empty: OK") + .stdout_contains("not-empty: OK"); +} + +// Manage the mixed behavior +// cksum --check -a sm3 CHECKSUMS +// when CHECKSUM contains among other lines: +// SHA384 (input) = f392fd0ae43879ced890c665a1d47179116b5eddf6fb5b49f4982746418afdcbd54ba5eedcd422af3592f57f666da285 +#[test] +fn test_cksum_mixed() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let commands = [ + vec!["-a", "sha384"], + vec!["-a", "blake2b"], + vec!["-a", "blake2b", "-l", "384"], + vec!["-a", "sm3"], + ]; + at.touch("f"); + at.touch("CHECKSUM"); + for command in &commands { + let result = scene.ucmd().args(command).arg("f").succeeds(); + at.append("CHECKSUM", result.stdout_str()); + } + scene + .ucmd() + .arg("--check") + .arg("-a") + .arg("sm3") + .arg("CHECKSUM") + .succeeds() + .stdout_contains("f: OK") + .stderr_contains("3 lines are improperly formatted"); +} + +#[test] +fn test_cksum_garbage() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Incorrect data at the start + at.write( + "check-file", + "garbage MD5 (README.md) = e5773576fc75ff0f8eba14f61587ae28", + ); + scene + .ucmd() + .arg("--check") + .arg("check-file") + .fails() + .stderr_contains("check-file: no properly formatted checksum lines found"); + + // Incorrect data at the end + at.write( + "check-file", + "MD5 (README.md) = e5773576fc75ff0f8eba14f61587ae28 garbage", + ); + scene + .ucmd() + .arg("--check") + .arg("check-file") + .fails() + .stderr_contains("check-file: no properly formatted checksum lines found"); +} + +#[ignore = "Should fail on bits"] +#[test] +fn test_md5_bits() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write( + "f", + "MD5-65536 (README.md) = e5773576fc75ff0f8eba14f61587ae28\n", + ); + + ucmd.arg("-c") + .arg("f") + .fails() + .stderr_contains("f: no properly formatted checksum lines found"); +} + +#[test] +fn test_blake2b_bits() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write( + "f", + "BLAKE2b-257 (README.md) = f9a984b70cf9a7549920864860fd1131c9fb6c0552def0b6dcce1d87b4ec4c5d\n" + ); + + ucmd.arg("-c") + .arg("f") + .fails() + .stderr_contains("f: no properly formatted checksum lines found"); +} + +#[test] +fn test_bsd_case() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("f", "bSD (README.md) = 0000\n"); + + scene + .ucmd() + .arg("-c") + .arg("f") + .fails() + .stderr_contains("f: no properly formatted checksum lines found"); + at.write("f", "BsD (README.md) = 0000\n"); + + scene + .ucmd() + .arg("-c") + .arg("f") + .fails() + .stderr_contains("f: no properly formatted checksum lines found"); +} + +#[ignore = "Different output"] +#[test] +fn test_blake2d_tested_with_sha1() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write( + "f", + "BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n" + ); + + ucmd.arg("-a") + .arg("sha1") + .arg("-c") + .arg("f") + .fails() + .stderr_contains("f: no properly formatted checksum lines found"); +} + +#[test] +fn test_unknown_sha() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f", "SHA4 (README.md) = 00000000\n"); + + ucmd.arg("-c") + .arg("f") + .fails() + .stderr_contains("f: no properly formatted checksum lines found"); +} diff --git a/tests/fixtures/cksum/supported_length.expected b/tests/fixtures/cksum/supported_length.expected deleted file mode 100644 index a2edb23e9..000000000 --- a/tests/fixtures/cksum/supported_length.expected +++ /dev/null @@ -1,2 +0,0 @@ -BLAKE2b-16 (lorem_ipsum.txt) = 7e2f -BLAKE2b-16 (alice_in_wonderland.txt) = a546 diff --git a/tests/fixtures/cksum/unsupported_length.expected b/tests/fixtures/cksum/unsupported_length.expected deleted file mode 100644 index c2a05fec7..000000000 --- a/tests/fixtures/cksum/unsupported_length.expected +++ /dev/null @@ -1,2 +0,0 @@ -cksum: invalid length: ‘15’ -cksum: length is not a multiple of 8