diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 460ab6f19..5678a58b3 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -65,7 +65,28 @@ fn detect_algo( "sha256sum" => ("SHA256", Box::new(Sha256::new()) as Box, 256), "sha384sum" => ("SHA384", Box::new(Sha384::new()) as Box, 384), "sha512sum" => ("SHA512", Box::new(Sha512::new()) as Box, 512), - "b2sum" => ("BLAKE2", Box::new(Blake2b::new()) as Box, 512), + "b2sum" => match matches.get_one::("length") { + // by default, blake2 uses 64 bytes (512 bits) + // --length=0 falls back to default behavior + Some(0) | None => ("BLAKE2", Box::new(Blake2b::new()) as Box, 512), + Some(length_in_bits) => { + if *length_in_bits > 512 { + crash!(1, "Invalid length (maximum digest length is 512 bits)") + } + + // blake2 output size must be a multiple of 8 bits + if length_in_bits % 8 == 0 { + let length_in_bytes = length_in_bits / 8; + ( + "BLAKE2", + Box::new(Blake2b::with_output_bytes(length_in_bytes)), + *length_in_bits, + ) + } else { + crash!(1, "Invalid length (expected a multiple of 8)") + } + } + }, "b3sum" => ("BLAKE3", Box::new(Blake3::new()) as Box, 256), "sha3sum" => match matches.get_one::("bits") { Some(224) => ( @@ -379,6 +400,21 @@ pub fn uu_app_common() -> Command { ) } +pub fn uu_app_length() -> Command { + uu_app_opt_length(uu_app_common()) +} + +fn uu_app_opt_length(command: Command) -> Command { + command.arg( + Arg::new("length") + .short('l') + .long("length") + .help("digest length in bits; must not exceed the max for the blake2 algorithm (512) and must be a multiple of 8") + .value_name("BITS") + .value_parser(parse_bit_num), + ) +} + pub fn uu_app_b3sum() -> Command { uu_app_b3sum_opts(uu_app_common()) } @@ -454,7 +490,7 @@ fn uu_app(binary_name: &str) -> Command { uu_app_common() } // b2sum supports the md5sum options plus -l/--length. - "b2sum" => uu_app_common(), // TODO: Implement -l/--length + "b2sum" => uu_app_length(), // These have never been part of GNU Coreutils, but can function with the same // options as md5sum. "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" => uu_app_common(), diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 339414630..c1cfaf5f8 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -38,10 +38,25 @@ pub trait Digest { } } -pub struct Blake2b(blake2b_simd::State); +/// first element of the tuple is the blake2b state +/// second is the number of output bits +pub struct Blake2b(blake2b_simd::State, usize); + +impl Blake2b { + /// Return a new Blake2b instance with a custom output bytes length + pub fn with_output_bytes(output_bytes: usize) -> Self { + let mut params = blake2b_simd::Params::new(); + params.hash_length(output_bytes); + + let state = params.to_state(); + Self(state, output_bytes * 8) + } +} + impl Digest for Blake2b { fn new() -> Self { - Self(blake2b_simd::State::new()) + // by default, Blake2b output is 512 bits long (= 64B) + Self::with_output_bytes(64) } fn hash_update(&mut self, input: &[u8]) { @@ -54,11 +69,11 @@ impl Digest for Blake2b { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::with_output_bytes(self.output_bytes()); } fn output_bits(&self) -> usize { - 512 + self.1 } } diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index a227b9fc3..645f11ef2 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -125,6 +125,70 @@ fn test_check_sha1() { .stderr_is(""); } +#[test] +fn test_check_b2sum_length_option_0() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + at.write("testf.b2sum", "9e2bf63e933e610efee4a8d6cd4a9387e80860edee97e27db3b37a828d226ab1eb92a9cdd8ca9ca67a753edaf8bd89a0558496f67a30af6f766943839acf0110 testf\n"); + + scene + .ccmd("b2sum") + .arg("--length=0") + .arg("-c") + .arg(at.subdir.join("testf.b2sum")) + .succeeds() + .stdout_only("testf: OK\n"); +} + +#[test] +fn test_check_b2sum_length_option_8() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + at.write("testf.b2sum", "6a testf\n"); + + scene + .ccmd("b2sum") + .arg("--length=8") + .arg("-c") + .arg(at.subdir.join("testf.b2sum")) + .succeeds() + .stdout_only("testf: OK\n"); +} + +#[test] +fn test_invalid_b2sum_length_option_not_multiple_of_8() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + + scene + .ccmd("b2sum") + .arg("--length=9") + .arg(at.subdir.join("testf")) + .fails() + .code_is(1); +} + +#[test] +fn test_invalid_b2sum_length_option_too_large() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + + scene + .ccmd("b2sum") + .arg("--length=513") + .arg(at.subdir.join("testf")) + .fails() + .code_is(1); +} + #[test] fn test_check_file_not_found_warning() { let scene = TestScenario::new(util_name!());