diff --git a/Cargo.lock b/Cargo.lock index 6237a36609f..e3919220ddb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,7 @@ dependencies = [ "regex", "rlimit", "rstest", + "rstest_reuse", "rustc-hash", "selinux", "sha1", @@ -2558,6 +2559,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstest_reuse" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" +dependencies = [ + "quote", + "rand 0.8.5", + "syn", +] + [[package]] name = "rust-ini" version = "0.21.3" diff --git a/Cargo.toml b/Cargo.toml index 92013b0af57..9ef80cd072c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -432,6 +432,7 @@ rayon = "1.10" regex = "1.10.4" rlimit = "0.11.0" rstest = "0.26.0" +rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" same-file = "1.0.6" @@ -640,6 +641,7 @@ uucore = { workspace = true, features = [ walkdir.workspace = true hex-literal = "1.0.0" rstest.workspace = true +rstest_reuse.workspace = true [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = [ diff --git a/src/uu/b2sum/src/b2sum.rs b/src/uu/b2sum/src/b2sum.rs index 6df276f2393..ddd5fe3e9e6 100644 --- a/src/uu/b2sum/src/b2sum.rs +++ b/src/uu/b2sum/src/b2sum.rs @@ -9,18 +9,14 @@ use clap::Command; use uu_checksum_common::{standalone_checksum_app_with_length, standalone_with_length_main}; -use uucore::checksum::{AlgoKind, calculate_blake2b_length_str}; +use uucore::checksum::{AlgoKind, calculate_blake_length_str}; use uucore::error::UResult; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - standalone_with_length_main( - AlgoKind::Blake2b, - uu_app(), - args, - calculate_blake2b_length_str, - ) + let calculate_blake2b_length = |s: &str| calculate_blake_length_str(AlgoKind::Blake2b, s); + standalone_with_length_main(AlgoKind::Blake2b, uu_app(), args, calculate_blake2b_length) } #[inline] diff --git a/src/uu/cksum/benches/cksum_bench.rs b/src/uu/cksum/benches/cksum_bench.rs index 5c2d3d9c614..81f70bbb3d1 100644 --- a/src/uu/cksum/benches/cksum_bench.rs +++ b/src/uu/cksum/benches/cksum_bench.rs @@ -104,7 +104,7 @@ bench_algorithm!(cksum_sha224, "sha224"); bench_algorithm!(cksum_sha256, "sha256"); bench_algorithm!(cksum_sha384, "sha384"); bench_algorithm!(cksum_sha512, "sha512"); -// broken. benchmarking error messages issues/10002 bench_algorithm!(cksum_blake3, "blake3"); +bench_algorithm!(cksum_blake3, "blake3"); bench_shake_algorithm!(cksum_shake128, "shake128", Shake128); bench_shake_algorithm!(cksum_shake256, "shake256", Shake256); diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e484c8323ad..19898def4f9 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -12,7 +12,7 @@ use uu_checksum_common::{ChecksumCommand, checksum_main, default_checksum_app, o use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, calculate_blake2b_length_str, sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, calculate_blake_length_str, sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; @@ -67,8 +67,10 @@ fn maybe_sanitize_length( Err(_) => Err(ChecksumError::InvalidLength(len.into()).into()), }, - // For BLAKE2b, if a length is provided, validate it. - (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), + // For BLAKE, if a length is provided, validate it. + (Some(algo @ (AlgoKind::Blake2b | AlgoKind::Blake3)), Some(len)) => { + calculate_blake_length_str(algo, len) + } // For any other provided algorithm, check if length is 0. // Otherwise, this is an error. diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 1f77b7e635f..9433bbf3682 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -115,6 +115,8 @@ impl AlgoKind { ALGORITHM_OPTIONS_SHA384 => Sha384, ALGORITHM_OPTIONS_SHA512 => Sha512, + // Extensions not in GNU as of version 9.10 + ALGORITHM_OPTIONS_BLAKE3 => Blake3, ALGORITHM_OPTIONS_SHAKE128 => Shake128, ALGORITHM_OPTIONS_SHAKE256 => Shake256, _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), @@ -245,11 +247,11 @@ pub enum SizedAlgoKind { Md5, Sm3, Sha1, - Blake3, Sha2(ShaLength), Sha3(ShaLength), - // Note: we store Blake2b's length as BYTES. + // Note: we store Blake*'s length as BYTES. Blake2b(Option), + Blake3(Option), // Shake* length are stored in bits. Shake128(Option), Shake256(Option), @@ -267,7 +269,6 @@ impl SizedAlgoKind { | ak::Md5 | ak::Sm3 | ak::Sha1 - | ak::Blake3 | ak::Sha224 | ak::Sha256 | ak::Sha384 @@ -282,8 +283,8 @@ impl SizedAlgoKind { (ak::Md5, _) => Ok(Self::Md5), (ak::Sm3, _) => Ok(Self::Sm3), (ak::Sha1, _) => Ok(Self::Sha1), - (ak::Blake3, _) => Ok(Self::Blake3), + (ak::Blake3, l) => Ok(Self::Blake3(l)), (ak::Shake128, l) => Ok(Self::Shake128(l)), (ak::Shake256, l) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), @@ -293,7 +294,8 @@ impl SizedAlgoKind { } // [`calculate_blake2b_length`] expects a length in bits but we // have a length in bytes. - (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length_str( + (algo @ ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake_length_str( + algo, &(8 * l).to_string(), )?)), (ak::Blake2b, None) => Ok(Self::Blake2b(None)), @@ -310,11 +312,16 @@ impl SizedAlgoKind { Self::Md5 => "MD5".into(), Self::Sm3 => "SM3".into(), Self::Sha1 => "SHA1".into(), - Self::Blake3 => "BLAKE3".into(), Self::Sha2(len) => format!("SHA{}", len.as_usize()), Self::Sha3(len) => format!("SHA3-{}", len.as_usize()), Self::Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), Self::Blake2b(None) => "BLAKE2b".into(), + Self::Blake3(byte_len) => { + format!( + "BLAKE3-{}", + byte_len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8 + ) + } Self::Shake128(opt_bit_len) => format!( "SHAKE128-{}", opt_bit_len.unwrap_or(Shake128::DEFAULT_BIT_SIZE) @@ -339,7 +346,6 @@ impl SizedAlgoKind { Self::Md5 => Box::new(Md5::default()), Self::Sm3 => Box::new(Sm3::default()), Self::Sha1 => Box::new(Sha1::default()), - Self::Blake3 => Box::new(Blake3::default()), Self::Sha2(Len224) => Box::new(Sha224::default()), Self::Sha2(Len256) => Box::new(Sha256::default()), Self::Sha2(Len384) => Box::new(Sha384::default()), @@ -351,6 +357,9 @@ impl SizedAlgoKind { Self::Blake2b(len_opt) => { Box::new(len_opt.map(Blake2b::with_output_bytes).unwrap_or_default()) } + Self::Blake3(len_opt) => { + Box::new(len_opt.map(Blake3::with_output_bytes).unwrap_or_default()) + } Self::Shake128(len_opt) => { Box::new(len_opt.map(Shake128::with_output_bits).unwrap_or_default()) } @@ -369,7 +378,7 @@ impl SizedAlgoKind { Self::Md5 => 128, Self::Sm3 => 512, Self::Sha1 => 160, - Self::Blake3 => 256, + Self::Blake3(len) => len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8, Self::Sha2(len) => len.as_usize(), Self::Sha3(len) => len.as_usize(), Self::Blake2b(len) => len.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE * 8), @@ -486,20 +495,22 @@ pub fn digest_reader( Ok((digest.result(), output_size)) } -/// Calculates the length of the digest. -pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { +/// Calculates the BYTE length of the digest. +pub fn calculate_blake_length_str(algo: AlgoKind, bit_length: &str) -> UResult> { + debug_assert!(matches!(algo, AlgoKind::Blake2b | AlgoKind::Blake3)); + // Blake2b's length is parsed in an u64. match bit_length.parse::() { Ok(0) => Ok(None), // Error cases - Ok(n) if n > 512 => { + Ok(n) if n > 512 && algo == AlgoKind::Blake2b => { show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) } Err(e) if *e.kind() == IntErrorKind::PosOverflow => { show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) } Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), @@ -632,10 +643,19 @@ mod tests { #[test] fn test_calculate_blake2b_length() { - assert_eq!(calculate_blake2b_length_str("0").unwrap(), None); - assert!(calculate_blake2b_length_str("10").is_err()); - assert!(calculate_blake2b_length_str("520").is_err()); - assert_eq!(calculate_blake2b_length_str("512").unwrap(), None); - assert_eq!(calculate_blake2b_length_str("256").unwrap(), Some(32)); + assert_eq!( + calculate_blake_length_str(AlgoKind::Blake2b, "0").unwrap(), + None + ); + assert!(calculate_blake_length_str(AlgoKind::Blake2b, "10").is_err()); + assert!(calculate_blake_length_str(AlgoKind::Blake2b, "520").is_err()); + assert_eq!( + calculate_blake_length_str(AlgoKind::Blake2b, "512").unwrap(), + None + ); + assert_eq!( + calculate_blake_length_str(AlgoKind::Blake2b, "256").unwrap(), + Some(32) + ); } } diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 17b1c7bf522..fcfba47e8c0 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -614,6 +614,7 @@ fn identify_algo_name_and_length( algo_name_input: Option, last_algo: &mut Option, ) -> Result<(AlgoKind, Option), LineCheckError> { + use AlgoKind as ak; let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); let Ok(line_algo) = AlgoKind::from_cksum(algo_from_line.to_lowercase()) else { // Unknown algorithm @@ -629,24 +630,20 @@ fn identify_algo_name_and_length( match (algo_name_input, line_algo) { (l, r) if l == r => (), // Edge case for SHA2, which matches SHA(224|256|384|512) - ( - AlgoKind::Sha2, - AlgoKind::Sha224 | AlgoKind::Sha256 | AlgoKind::Sha384 | AlgoKind::Sha512, - ) => (), + (ak::Sha2, ak::Sha224 | ak::Sha256 | ak::Sha384 | ak::Sha512) => (), _ => return Err(LineCheckError::ImproperlyFormatted), } } let bytes = if let Some(bitlen) = line_info.algo_bit_len { match line_algo { - AlgoKind::Blake2b if bitlen % 8 == 0 => Some(bitlen / 8), - AlgoKind::Sha2 | AlgoKind::Sha3 if [224, 256, 384, 512].contains(&bitlen) => { - Some(bitlen) - } - AlgoKind::Shake128 | AlgoKind::Shake256 => Some(bitlen), + ak::Blake2b | ak::Blake3 if bitlen % 8 == 0 => Some(bitlen / 8), + ak::Sha2 | ak::Sha3 if [224, 256, 384, 512].contains(&bitlen) => Some(bitlen), + ak::Shake128 | ak::Shake256 => Some(bitlen), // Either - // the algo based line is provided with a bit length - // with an algorithm that does not support it (only Blake2B does). + // the algo based line is provided with a bit length with an + // algorithm that does not support it (only Blake2b, Blake3, sha2, + // and sha3 do). // // eg: MD5-128 (foo.txt) = fffffffff // ^ This is illegal @@ -654,9 +651,12 @@ fn identify_algo_name_and_length( // the given length is wrong because it's not a multiple of 8. _ => return Err(LineCheckError::ImproperlyFormatted), } - } else if line_algo == AlgoKind::Blake2b { + } else if line_algo == ak::Blake2b { // Default length with BLAKE2b, Some(64) + } else if line_algo == ak::Blake3 { + // Default length with BLAKE3, + Some(32) } else { None }; @@ -741,7 +741,7 @@ fn process_algo_based_line( // If the digest bitlen is known, we can check the format of the expected // checksum with it. let digest_char_length_hint = match (algo_kind, algo_byte_len) { - (AlgoKind::Blake2b, Some(byte_len)) => Some(byte_len), + (AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len), (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len.div_ceil(8)), (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE.div_ceil(8)), (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE.div_ceil(8)), @@ -764,6 +764,7 @@ fn process_non_algo_based_line( cli_algo_length: Option, opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { + use AlgoKind as ak; let mut filename_to_check = line_info.filename.as_slice(); if filename_to_check.starts_with(b"*") && line_number == 0 @@ -778,16 +779,16 @@ fn process_non_algo_based_line( // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. - let (algo_kind, algo_byte_len) = match cli_algo_kind { - AlgoKind::Blake2b => (AlgoKind::Blake2b, Some(expected_checksum.len())), - algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { + let algo_byte_len = match cli_algo_kind { + ak::Blake2b | ak::Blake3 => Some(expected_checksum.len()), + ak::Sha2 | ak::Sha3 => { // multiplication by 8 to get the number of bits - (algo, Some(expected_checksum.len() * 8)) + Some(expected_checksum.len() * 8) } - _ => (cli_algo_kind, cli_algo_length), + _ => cli_algo_length, }; - let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(cli_algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 279643a962b..af122ced1a3 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -122,25 +122,48 @@ impl Digest for Blake2b { } } -#[derive(Default)] -pub struct Blake3(blake3::Hasher); +pub struct Blake3 { + digest: blake3::Hasher, + byte_size: usize, +} + +impl Blake3 { + /// Default length for the BLAKE3 digest in bytes. + pub const DEFAULT_BYTE_SIZE: usize = 32; + + pub fn with_output_bytes(output_bytes: usize) -> Self { + Self { + digest: blake3::Hasher::new(), + byte_size: output_bytes, + } + } +} + +impl Default for Blake3 { + fn default() -> Self { + Self { + digest: blake3::Hasher::default(), + byte_size: Self::DEFAULT_BYTE_SIZE, + } + } +} impl Digest for Blake3 { fn hash_update(&mut self, input: &[u8]) { - self.0.update(input); + self.digest.update(input); } fn hash_finalize(&mut self, out: &mut [u8]) { - let hash_result = &self.0.finalize(); - out.copy_from_slice(hash_result.as_bytes()); + let mut hash_result = self.digest.finalize_xof(); + hash_result.fill(out); } fn reset(&mut self) { - *self = Self::default(); + *self = Self::with_output_bytes(self.output_bytes()); } fn output_bits(&self) -> usize { - 256 + self.byte_size * 8 } } diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 67f96e90762..1ab5bb45285 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -5,6 +5,7 @@ // spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont checkfile use rstest::rstest; +use rstest_reuse::{apply, template}; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -3287,3 +3288,112 @@ fn test_check_shake256_no_length() { .fails() .stderr_only("cksum: 'standard input': no properly formatted checksum lines found\n"); } + +#[template] +#[rstest] +#[case::no_length( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + None +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + Some(0) +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", + Some(256) +)] +#[case( + b"foo", + "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9791074b7511b59d31c71c62f5a745689fa6c", + Some(400) +)] +#[case(b"foo", "04e0bb39f3", Some(40))] +#[case(b"foo", "04e0", Some(16))] +#[case(b"foo", "04", Some(8))] +fn test_blake3(#[case] input: &[u8], #[case] expected: &str, #[case] length: Option) {} + +#[apply(test_blake3)] +fn test_compute_blake3( + #[case] input: &[u8], + #[case] expected: &str, + #[case] length: Option, +) { + let length_args: &[String] = if let Some(len) = length { + &["-l".into(), len.to_string()] + } else { + &[] + }; + + new_ucmd!() + .arg("-a") + .arg("blake3") + .args(length_args) + .pipe_in(input) + .succeeds() + .stdout_only(format!( + "BLAKE3{} (-) = {expected}\n", + match length { + Some(0) | None => "-256".into(), + Some(i) => format!("-{i}"), + } + )); + + // with --untagged + new_ucmd!() + .arg("-a") + .arg("blake3") + .arg("--untagged") + .args(length_args) + .pipe_in(input) + .succeeds() + .stdout_only(format!("{expected} -\n")); +} + +#[apply(test_blake3)] +fn test_check_blake3_tagged( + #[case] input: &[u8], + #[case] digest: &str, + #[case] opt_len: Option, +) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("FILE", input); + + let len = match opt_len { + Some(0) => "-256".into(), + Some(i) => format!("-{i}"), + None => String::new(), + }; + + let tagged = format!("BLAKE3{len} (FILE) = {digest}",); + + ucmd.arg("-c") + .arg("-a") + .arg("blake3") + .pipe_in(tagged) + .succeeds() + .stdout_only("FILE: OK\n"); +} + +#[apply(test_blake3)] +#[allow(clippy::used_underscore_binding)] +fn test_check_blake3_untagged( + #[case] input: &[u8], + #[case] digest: &str, + #[case] _opt_len: Option, +) { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("FILE", input); + + let untagged = format!("{digest} FILE"); + + ucmd.arg("-c") + .arg("-a") + .arg("blake3") + .pipe_in(untagged) + .succeeds() + .stdout_only("FILE: OK\n"); +}