From deaf44afafb48a813acd7836083b5ddd3b94684f Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Sat, 20 Dec 2025 17:46:42 +0100 Subject: [PATCH 1/2] hashsum: Get rid of non-GNU `--bits` argument --- src/uu/hashsum/src/hashsum.rs | 60 ++-- tests/by-util/test_hashsum.rs | 279 ++++++++++++------ ...ke128_256.checkfile => shake128.checkfile} | 0 ...hake128_256.expected => shake128.expected} | 0 ...ke256_512.checkfile => shake256.checkfile} | 0 ...hake256_512.expected => shake256.expected} | 0 6 files changed, 214 insertions(+), 125 deletions(-) rename tests/fixtures/hashsum/{shake128_256.checkfile => shake128.checkfile} (100%) rename tests/fixtures/hashsum/{shake128_256.expected => shake128.expected} (100%) rename tests/fixtures/hashsum/{shake256_512.checkfile => shake256.checkfile} (100%) rename tests/fixtures/hashsum/{shake256_512.expected => shake256.expected} (100%) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 047d6889c86..19e8ad9db04 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -7,7 +7,6 @@ use std::ffi::{OsStr, OsString}; use std::iter; -use std::num::ParseIntError; use std::path::Path; use clap::builder::ValueParser; @@ -19,7 +18,10 @@ use uucore::checksum::compute::{ use uucore::checksum::validate::{ ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, }; -use uucore::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length_str}; +use uucore::checksum::{ + AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length_str, + sanitize_sha2_sha3_length_str, +}; use uucore::error::UResult; use uucore::line_ending::LineEnding; use uucore::{format_usage, translate}; @@ -74,9 +76,11 @@ fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<(AlgoKind, Optio set_or_err((AlgoKind::Blake3, None))?; } if matches.get_flag("sha3") { - match matches.get_one::("bits") { - Some(bits @ (224 | 256 | 384 | 512)) => set_or_err((AlgoKind::Sha3, Some(*bits)))?, - Some(bits) => return Err(ChecksumError::InvalidLengthForSha(bits.to_string()).into()), + match matches.get_one::(options::LENGTH) { + Some(len) => set_or_err(( + AlgoKind::Sha3, + Some(sanitize_sha2_sha3_length_str(AlgoKind::Sha3, len)?), + ))?, None => return Err(ChecksumError::LengthRequired("SHA3".into()).into()), } } @@ -93,16 +97,10 @@ fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<(AlgoKind, Optio set_or_err((AlgoKind::Sha3, Some(512)))?; } if matches.get_flag("shake128") { - match matches.get_one::("bits") { - Some(bits) => set_or_err((AlgoKind::Shake128, Some(*bits)))?, - None => return Err(ChecksumError::LengthRequired("SHAKE128".into()).into()), - } + set_or_err((AlgoKind::Shake128, Some(128)))?; } if matches.get_flag("shake256") { - match matches.get_one::("bits") { - Some(bits) => set_or_err((AlgoKind::Shake256, Some(*bits)))?, - None => return Err(ChecksumError::LengthRequired("SHAKE256".into()).into()), - } + set_or_err((AlgoKind::Shake256, Some(256)))?; } if alg.is_none() { @@ -112,11 +110,6 @@ fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<(AlgoKind, Optio Ok(alg.unwrap()) } -// TODO: return custom error type -fn parse_bit_num(arg: &str) -> Result { - arg.parse() -} - #[uucore::main] pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { // if there is no program name for some reason, default to "hashsum" @@ -139,17 +132,16 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { // least somewhat better from a user's perspective. let matches = uucore::clap_localization::handle_clap_result(command, args)?; - let input_length: Option<&String> = if binary_name == "b2sum" { - matches.get_one::(options::LENGTH) + let length: Option = if binary_name == "b2sum" { + if let Some(len) = matches.get_one::(options::LENGTH) { + calculate_blake2b_length_str(len)? + } else { + None + } } else { None }; - let length = match input_length { - Some(length) => calculate_blake2b_length_str(length)?, - None => None, - }; - let (algo_kind, length) = if is_hashsum_bin { create_algorithm_from_flags(&matches)? } else { @@ -371,24 +363,8 @@ fn uu_app_opt_length(command: Command) -> Command { ) } -pub fn uu_app_bits() -> Command { - uu_app_opt_bits(uu_app_common()) -} - -fn uu_app_opt_bits(command: Command) -> Command { - // Needed for variable-length output sums (e.g. SHAKE) - command.arg( - Arg::new("bits") - .long("bits") - .help(translate!("hashsum-help-bits")) - .value_name("BITS") - // XXX: should we actually use validators? they're not particularly efficient - .value_parser(parse_bit_num), - ) -} - pub fn uu_app_custom() -> Command { - let mut command = uu_app_opt_bits(uu_app_common()); + let mut command = uu_app_opt_length(uu_app_common()); let algorithms = &[ ("md5", translate!("hashsum-help-md5")), ("sha1", translate!("hashsum-help-sha1")), diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index e39fe429efb..2f1719b0eca 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -16,86 +16,206 @@ macro_rules! get_hash( ); macro_rules! test_digest { - ($($id:ident $t:ident $size:expr)*) => ($( - - mod $id { - use uutests::util::*; - use uutests::util_name; - static DIGEST_ARG: &'static str = concat!("--", stringify!($t)); - static BITS_ARG: &'static str = concat!("--bits=", stringify!($size)); - static EXPECTED_FILE: &'static str = concat!(stringify!($id), ".expected"); - static CHECK_FILE: &'static str = concat!(stringify!($id), ".checkfile"); - static INPUT_FILE: &'static str = "input.txt"; - - #[test] - fn test_single_file() { - let ts = TestScenario::new(util_name!()); - assert_eq!(ts.fixtures.read(EXPECTED_FILE), - get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).arg(INPUT_FILE).succeeds().no_stderr().stdout_str())); + ($id:ident, $t:ident) => { + mod $id { + use uutests::util::*; + use uutests::util_name; + static DIGEST_ARG: &'static str = concat!("--", stringify!($t)); + static EXPECTED_FILE: &'static str = concat!(stringify!($id), ".expected"); + static CHECK_FILE: &'static str = concat!(stringify!($id), ".checkfile"); + static INPUT_FILE: &'static str = "input.txt"; + + #[test] + fn test_single_file() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(DIGEST_ARG) + .arg(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_stdin() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(DIGEST_ARG) + .pipe_in_fixture(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_check() { + let ts = TestScenario::new(util_name!()); + println!("File content='{}'", ts.fixtures.read(INPUT_FILE)); + println!("Check file='{}'", ts.fixtures.read(CHECK_FILE)); + + ts.ucmd() + .args(&[DIGEST_ARG, "--check", CHECK_FILE]) + .succeeds() + .no_stderr() + .stdout_is("input.txt: OK\n"); + } + + #[test] + fn test_zero() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(DIGEST_ARG) + .arg("--zero") + .arg(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_missing_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("a", "file1\n"); + at.write("c", "file3\n"); + + ts.ucmd() + .args(&[DIGEST_ARG, "a", "b", "c"]) + .fails() + .stdout_contains("a\n") + .stdout_contains("c\n") + .stderr_contains("b: No such file or directory"); + } } + }; +} - #[test] - fn test_stdin() { - let ts = TestScenario::new(util_name!()); - assert_eq!(ts.fixtures.read(EXPECTED_FILE), - get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).pipe_in_fixture(INPUT_FILE).succeeds().no_stderr().stdout_str())); - } - - #[test] - fn test_check() { - let ts = TestScenario::new(util_name!()); - println!("File content='{}'", ts.fixtures.read(INPUT_FILE)); - println!("Check file='{}'", ts.fixtures.read(CHECK_FILE)); - - ts.ucmd() - .args(&[DIGEST_ARG, BITS_ARG, "--check", CHECK_FILE]) - .succeeds() - .no_stderr() - .stdout_is("input.txt: OK\n"); - } - - #[test] - fn test_zero() { - let ts = TestScenario::new(util_name!()); - assert_eq!(ts.fixtures.read(EXPECTED_FILE), - get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).arg("--zero").arg(INPUT_FILE).succeeds().no_stderr().stdout_str())); - } - - #[test] - fn test_missing_file() { - let ts = TestScenario::new(util_name!()); - let at = &ts.fixtures; - - at.write("a", "file1\n"); - at.write("c", "file3\n"); - - ts.ucmd() - .args(&[DIGEST_ARG, BITS_ARG, "a", "b", "c"]) - .fails() - .stdout_contains("a\n") - .stdout_contains("c\n") - .stderr_contains("b: No such file or directory"); +macro_rules! test_digest_with_len { + ($id:ident, $t:ident, $size:expr) => { + mod $id { + use uutests::util::*; + use uutests::util_name; + static DIGEST_ARG: &'static str = concat!("--", stringify!($t)); + static LENGTH_ARG: &'static str = concat!("--length=", stringify!($size)); + static EXPECTED_FILE: &'static str = concat!(stringify!($id), ".expected"); + static CHECK_FILE: &'static str = concat!(stringify!($id), ".checkfile"); + static INPUT_FILE: &'static str = "input.txt"; + + #[test] + fn test_single_file() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(DIGEST_ARG) + .arg(LENGTH_ARG) + .arg(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_stdin() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(DIGEST_ARG) + .arg(LENGTH_ARG) + .pipe_in_fixture(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_check() { + let ts = TestScenario::new(util_name!()); + println!("File content='{}'", ts.fixtures.read(INPUT_FILE)); + println!("Check file='{}'", ts.fixtures.read(CHECK_FILE)); + + ts.ucmd() + .args(&[DIGEST_ARG, LENGTH_ARG, "--check", CHECK_FILE]) + .succeeds() + .no_stderr() + .stdout_is("input.txt: OK\n"); + } + + #[test] + fn test_zero() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(DIGEST_ARG) + .arg(LENGTH_ARG) + .arg("--zero") + .arg(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_missing_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("a", "file1\n"); + at.write("c", "file3\n"); + + ts.ucmd() + .args(&[DIGEST_ARG, LENGTH_ARG, "a", "b", "c"]) + .fails() + .stdout_contains("a\n") + .stdout_contains("c\n") + .stderr_contains("b: No such file or directory"); + } } - } - )*) + }; } -test_digest! { - md5 md5 128 - sha1 sha1 160 - sha224 sha224 224 - sha256 sha256 256 - sha384 sha384 384 - sha512 sha512 512 - sha3_224 sha3 224 - sha3_256 sha3 256 - sha3_384 sha3 384 - sha3_512 sha3 512 - shake128_256 shake128 256 - shake256_512 shake256 512 - b2sum b2sum 512 - b3sum b3sum 256 -} +test_digest! {md5, md5} +test_digest! {sha1, sha1} +test_digest! {b3sum, b3sum} +test_digest! {shake128, shake128} +test_digest! {shake256, shake256} + +test_digest_with_len! {sha224, sha224, 224} +test_digest_with_len! {sha256, sha256, 256} +test_digest_with_len! {sha384, sha384, 384} +test_digest_with_len! {sha512, sha512, 512} +test_digest_with_len! {sha3_224, sha3, 224} +test_digest_with_len! {sha3_256, sha3, 256} +test_digest_with_len! {sha3_384, sha3, 384} +test_digest_with_len! {sha3_512, sha3, 512} +test_digest_with_len! {b2sum, b2sum, 512} #[test] fn test_check_sha1() { @@ -1037,7 +1157,6 @@ fn test_sha256_binary() { get_hash!( ts.ucmd() .arg("--sha256") - .arg("--bits=256") .arg("binary.png") .succeeds() .no_stderr() @@ -1054,7 +1173,6 @@ fn test_sha256_stdin_binary() { get_hash!( ts.ucmd() .arg("--sha256") - .arg("--bits=256") .pipe_in_fixture("binary.png") .succeeds() .no_stderr() @@ -1068,12 +1186,7 @@ fn test_sha256_stdin_binary() { #[cfg_attr(windows, ignore = "Discussion is in #9168")] fn test_check_sha256_binary() { new_ucmd!() - .args(&[ - "--sha256", - "--bits=256", - "--check", - "binary.sha256.checkfile", - ]) + .args(&["--sha256", "--check", "binary.sha256.checkfile"]) .succeeds() .no_stderr() .stdout_is("binary.png: OK\n"); diff --git a/tests/fixtures/hashsum/shake128_256.checkfile b/tests/fixtures/hashsum/shake128.checkfile similarity index 100% rename from tests/fixtures/hashsum/shake128_256.checkfile rename to tests/fixtures/hashsum/shake128.checkfile diff --git a/tests/fixtures/hashsum/shake128_256.expected b/tests/fixtures/hashsum/shake128.expected similarity index 100% rename from tests/fixtures/hashsum/shake128_256.expected rename to tests/fixtures/hashsum/shake128.expected diff --git a/tests/fixtures/hashsum/shake256_512.checkfile b/tests/fixtures/hashsum/shake256.checkfile similarity index 100% rename from tests/fixtures/hashsum/shake256_512.checkfile rename to tests/fixtures/hashsum/shake256.checkfile diff --git a/tests/fixtures/hashsum/shake256_512.expected b/tests/fixtures/hashsum/shake256.expected similarity index 100% rename from tests/fixtures/hashsum/shake256_512.expected rename to tests/fixtures/hashsum/shake256.expected From 9f2c7fb7be5f3000a60751496238f7d3cd7ae831 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Sat, 20 Dec 2025 19:24:45 +0100 Subject: [PATCH 2/2] checksum: Move b2sum to a standalone binary --- Cargo.lock | 12 + Cargo.toml | 2 + GNUmakefile | 3 +- build.rs | 1 - src/uu/b2sum/Cargo.toml | 41 +++ src/uu/b2sum/LICENSE | 1 + src/uu/b2sum/locales/en-US.ftl | 3 + src/uu/b2sum/locales/fr-FR.ftl | 2 + src/uu/b2sum/src/b2sum.rs | 36 ++ src/uu/b2sum/src/main.rs | 1 + src/uu/cksum/locales/en-US.ftl | 16 - src/uu/cksum/locales/fr-FR.ftl | 16 - src/uu/cksum/src/cksum.rs | 307 +++-------------- src/uu/hashsum/src/hashsum.rs | 20 +- src/uucore/locales/en-US.ftl | 20 ++ src/uucore/locales/fr-FR.ftl | 18 + src/uucore/src/lib/features/checksum/cli.rs | 326 ++++++++++++++++++ .../src/lib/features/checksum/compute.rs | 98 ++++-- src/uucore/src/lib/features/checksum/mod.rs | 3 + src/uucore/src/lib/lib.rs | 4 +- tests/by-util/test_b2sum.rs | 289 ++++++++++++++++ tests/by-util/test_hashsum.rs | 1 - tests/fixtures/b2sum/b2sum.checkfile | 1 + tests/fixtures/b2sum/b2sum.expected | 1 + tests/fixtures/b2sum/binary.png | Bin 0 -> 8055 bytes tests/fixtures/b2sum/input.txt | 1 + tests/tests.rs | 4 + 27 files changed, 884 insertions(+), 343 deletions(-) create mode 100644 src/uu/b2sum/Cargo.toml create mode 120000 src/uu/b2sum/LICENSE create mode 100644 src/uu/b2sum/locales/en-US.ftl create mode 100644 src/uu/b2sum/locales/fr-FR.ftl create mode 100644 src/uu/b2sum/src/b2sum.rs create mode 100644 src/uu/b2sum/src/main.rs create mode 100644 src/uucore/src/lib/features/checksum/cli.rs create mode 100644 tests/by-util/test_b2sum.rs create mode 100644 tests/fixtures/b2sum/b2sum.checkfile create mode 100644 tests/fixtures/b2sum/b2sum.expected create mode 100644 tests/fixtures/b2sum/binary.png create mode 100644 tests/fixtures/b2sum/input.txt diff --git a/Cargo.lock b/Cargo.lock index 52b5caadec0..00083dca4af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,6 +565,7 @@ dependencies = [ "time", "unindent", "uu_arch", + "uu_b2sum", "uu_base32", "uu_base64", "uu_basename", @@ -3012,6 +3013,17 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_b2sum" +version = "0.5.0" +dependencies = [ + "clap", + "codspeed-divan-compat", + "fluent", + "tempfile", + "uucore", +] + [[package]] name = "uu_base32" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index b388373a2aa..49e4ba01dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ feat_common_core = [ "basenc", "cat", "cksum", + "b2sum", "comm", "cp", "csplit", @@ -431,6 +432,7 @@ chmod = { optional = true, version = "0.5.0", package = "uu_chmod", path = "src/ chown = { optional = true, version = "0.5.0", package = "uu_chown", path = "src/uu/chown" } chroot = { optional = true, version = "0.5.0", package = "uu_chroot", path = "src/uu/chroot" } cksum = { optional = true, version = "0.5.0", package = "uu_cksum", path = "src/uu/cksum" } +b2sum = { optional = true, version = "0.5.0", package = "uu_b2sum", path = "src/uu/b2sum" } comm = { optional = true, version = "0.5.0", package = "uu_comm", path = "src/uu/comm" } cp = { optional = true, version = "0.5.0", package = "uu_cp", path = "src/uu/cp" } csplit = { optional = true, version = "0.5.0", package = "uu_csplit", path = "src/uu/csplit" } diff --git a/GNUmakefile b/GNUmakefile index 6f5eda35f30..0f9bdc73e1a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -84,6 +84,7 @@ PROGS := \ basename \ cat \ cksum \ + b2sum \ comm \ cp \ csplit \ @@ -185,7 +186,6 @@ SELINUX_PROGS := \ runcon HASHSUM_PROGS := \ - b2sum \ md5sum \ sha1sum \ sha224sum \ @@ -223,6 +223,7 @@ TEST_PROGS := \ chmod \ chown \ cksum \ + b2sum \ comm \ cp \ csplit \ diff --git a/build.rs b/build.rs index 9b35eac5eb4..626949ef676 100644 --- a/build.rs +++ b/build.rs @@ -85,7 +85,6 @@ pub fn main() { phf_map.entry("sha256sum", map_value.clone()); phf_map.entry("sha384sum", map_value.clone()); phf_map.entry("sha512sum", map_value.clone()); - phf_map.entry("b2sum", map_value.clone()); } _ => { phf_map.entry(krate, map_value.clone()); diff --git a/src/uu/b2sum/Cargo.toml b/src/uu/b2sum/Cargo.toml new file mode 100644 index 00000000000..d065f1a28f8 --- /dev/null +++ b/src/uu/b2sum/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "uu_b2sum" +description = "b2sum ~ (uutils) Print or check the BLAKE2b checksums" +repository = "https://github.com/uutils/coreutils/tree/main/src/uu/b2sum" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/b2sum.rs" + +[dependencies] +clap = { workspace = true } +uucore = { workspace = true, features = [ + "checksum", + "encoding", + "sum", + "hardware", +] } +fluent = { workspace = true } + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } + +[[bin]] +name = "b2sum" +path = "src/main.rs" + +# [[bench]] +# name = "b2sum_bench" +# harness = false diff --git a/src/uu/b2sum/LICENSE b/src/uu/b2sum/LICENSE new file mode 120000 index 00000000000..5853aaea53b --- /dev/null +++ b/src/uu/b2sum/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/uu/b2sum/locales/en-US.ftl b/src/uu/b2sum/locales/en-US.ftl new file mode 100644 index 00000000000..16b0c8b7788 --- /dev/null +++ b/src/uu/b2sum/locales/en-US.ftl @@ -0,0 +1,3 @@ +b2sum-about = Print or check the BLAKE2b checksums +b2sum-usage = b2sum [OPTIONS] [FILE]... +b2sum-after-help = With no FILE or when FILE is -, read standard input diff --git a/src/uu/b2sum/locales/fr-FR.ftl b/src/uu/b2sum/locales/fr-FR.ftl new file mode 100644 index 00000000000..7cb93e5d8d9 --- /dev/null +++ b/src/uu/b2sum/locales/fr-FR.ftl @@ -0,0 +1,2 @@ +b2sum-about = Afficher le BLAKE2b et la taille de chaque fichier +b2sum-usage = b2sum [OPTION]... [FICHIER]... diff --git a/src/uu/b2sum/src/b2sum.rs b/src/uu/b2sum/src/b2sum.rs new file mode 100644 index 00000000000..653f83443b5 --- /dev/null +++ b/src/uu/b2sum/src/b2sum.rs @@ -0,0 +1,36 @@ +// 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. + +// spell-checker:ignore (ToDO) algo + +use clap::Command; +use uucore::checksum::cli::{checksum_main, options, standalone_checksum_app_with_length}; +use uucore::checksum::compute::OutputFormat; +use uucore::checksum::{AlgoKind, calculate_blake2b_length_str}; +use uucore::error::UResult; +use uucore::translate; + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let algo = Some(AlgoKind::Blake2b); + + let length = matches + .get_one::(options::LENGTH) + .map(String::as_str) + .map(calculate_blake2b_length_str) + .transpose()? + .flatten(); + + let format = OutputFormat::from_standalone(std::env::args_os().into_iter()); + + checksum_main(algo, length, matches, format?) +} + +#[inline] +pub fn uu_app() -> Command { + standalone_checksum_app_with_length(translate!("b2sum-about"), translate!("b2sum-usage")) + .after_help(translate!("b2sum-after-help")) +} diff --git a/src/uu/b2sum/src/main.rs b/src/uu/b2sum/src/main.rs new file mode 100644 index 00000000000..422fa2fe709 --- /dev/null +++ b/src/uu/b2sum/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_b2sum); diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl index 834cd77b0ef..aece6fc5b72 100644 --- a/src/uu/cksum/locales/en-US.ftl +++ b/src/uu/cksum/locales/en-US.ftl @@ -12,19 +12,3 @@ cksum-after-help = DIGEST determines the digest algorithm and default output for - sha3: (only available through cksum) - blake2b: (equivalent to b2sum) - sm3: (only available through cksum) - -# Help messages -cksum-help-algorithm = select the digest type to use. See DIGEST below -cksum-help-untagged = create a reversed style checksum, without digest type -cksum-help-tag = create a BSD style checksum, undo --untagged (default) -cksum-help-length = digest length in bits; must not exceed the max for the blake2 algorithm and must be a multiple of 8 -cksum-help-raw = emit a raw binary digest, not hexadecimal -cksum-help-strict = exit non-zero for improperly formatted checksum lines -cksum-help-check = read hashsums from the FILEs and check them -cksum-help-base64 = emit a base64 digest, not hexadecimal -cksum-help-warn = warn about improperly formatted checksum lines -cksum-help-status = don't output anything, status code shows success -cksum-help-quiet = don't print OK for each successfully verified file -cksum-help-ignore-missing = don't fail or report status for missing files -cksum-help-zero = end each output line with NUL, not newline, and disable file name escaping -cksum-help-debug = print CPU hardware capability detection info used by cksum diff --git a/src/uu/cksum/locales/fr-FR.ftl b/src/uu/cksum/locales/fr-FR.ftl index 01136f606f9..bbc12e59cde 100644 --- a/src/uu/cksum/locales/fr-FR.ftl +++ b/src/uu/cksum/locales/fr-FR.ftl @@ -12,19 +12,3 @@ cksum-after-help = DIGEST détermine l'algorithme de condensé et le format de s - sha3 : (disponible uniquement via cksum) - blake2b : (équivalent à b2sum) - sm3 : (disponible uniquement via cksum) - -# Messages d'aide -cksum-help-algorithm = sélectionner le type de condensé à utiliser. Voir DIGEST ci-dessous -cksum-help-untagged = créer une somme de contrôle de style inversé, sans type de condensé -cksum-help-tag = créer une somme de contrôle de style BSD, annuler --untagged (par défaut) -cksum-help-length = longueur du condensé en bits ; ne doit pas dépasser le maximum pour l'algorithme blake2 et doit être un multiple de 8 -cksum-help-raw = émettre un condensé binaire brut, pas hexadécimal -cksum-help-strict = sortir avec un code non-zéro pour les lignes de somme de contrôle mal formatées -cksum-help-check = lire les sommes de hachage des FICHIERs et les vérifier -cksum-help-base64 = émettre un condensé base64, pas hexadécimal -cksum-help-warn = avertir des lignes de somme de contrôle mal formatées -cksum-help-status = ne rien afficher, le code de statut indique le succès -cksum-help-quiet = ne pas afficher OK pour chaque fichier vérifié avec succès -cksum-help-ignore-missing = ne pas échouer ou signaler le statut pour les fichiers manquants -cksum-help-zero = terminer chaque ligne de sortie avec NUL, pas un saut de ligne, et désactiver l'échappement des noms de fichiers -cksum-help-debug = afficher les informations de débogage sur la détection de la prise en charge matérielle du processeur diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 30eabcaac56..ef5fdc93d0f 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -5,24 +5,18 @@ // spell-checker:ignore (ToDO) fname, algo, bitlen -use clap::builder::ValueParser; -use clap::{Arg, ArgAction, Command}; -use std::ffi::{OsStr, OsString}; -use std::iter; -use uucore::checksum::compute::{ - ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, -}; -use uucore::checksum::validate::{ - ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, -}; +use std::ffi::OsStr; + +use clap::Command; +use uucore::checksum::cli::{ChecksumCommand, checksum_main, default_checksum_app}; +use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind, calculate_blake2b_length_str, + AlgoKind, ChecksumError, calculate_blake2b_length_str, cli::options, sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; -use uucore::line_ending::LineEnding; -use uucore::{format_usage, show_error, translate}; +use uucore::{show_error, translate}; /// Print CPU hardware capability detection information to stderr /// This matches GNU cksum's --debug behavior @@ -48,24 +42,28 @@ fn print_cpu_debug_info() { } } -mod options { - pub const ALGORITHM: &str = "algorithm"; - pub const FILE: &str = "file"; - pub const UNTAGGED: &str = "untagged"; - pub const TAG: &str = "tag"; - pub const LENGTH: &str = "length"; - 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"; - pub const STATUS: &str = "status"; - pub const WARN: &str = "warn"; - pub const IGNORE_MISSING: &str = "ignore-missing"; - pub const QUIET: &str = "quiet"; - pub const ZERO: &str = "zero"; - pub const DEBUG: &str = "debug"; +/// Sanitize the `--length` argument depending on `--algorithm` and `--length`. +fn maybe_sanitize_length( + algo_cli: Option, + input_length: Option<&str>, +) -> UResult> { + match (algo_cli, input_length) { + // No provided length is not a problem so far. + (_, None) => Ok(None), + + // For SHA2 and SHA3, if a length is provided, ensure it is correct. + (Some(algo @ (AlgoKind::Sha2 | AlgoKind::Sha3)), Some(s_len)) => { + sanitize_sha2_sha3_length_str(algo, s_len).map(Some) + } + + // For BLAKE2b, if a length is provided, validate it. + (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), + + // For any other provided algorithm, check if length is 0. + // Otherwise, this is an error. + (_, Some(len)) if len.parse::() == Ok(0_u32) => Ok(None), + (_, Some(_)) => Err(ChecksumError::LengthOnlyForBlake2bSha2Sha3.into()), + } } /// cksum has a bunch of legacy behavior. We handle this in this function to @@ -110,50 +108,10 @@ fn handle_tag_text_binary_flags>( Ok((tag, binary)) } -/// Sanitize the `--length` argument depending on `--algorithm` and `--length`. -fn maybe_sanitize_length( - algo_cli: Option, - input_length: Option<&str>, -) -> UResult> { - match (algo_cli, input_length) { - // No provided length is not a problem so far. - (_, None) => Ok(None), - - // For SHA2 and SHA3, if a length is provided, ensure it is correct. - (Some(algo @ (AlgoKind::Sha2 | AlgoKind::Sha3)), Some(s_len)) => { - sanitize_sha2_sha3_length_str(algo, s_len).map(Some) - } - - // For BLAKE2b, if a length is provided, validate it. - (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), - - // For any other provided algorithm, check if length is 0. - // Otherwise, this is an error. - (_, Some(len)) if len.parse::() == Ok(0_u32) => Ok(None), - (_, Some(_)) => Err(ChecksumError::LengthOnlyForBlake2bSha2Sha3.into()), - } -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let check = matches.get_flag(options::CHECK); - - let check_flag = |flag| match (check, matches.get_flag(flag)) { - (_, false) => Ok(false), - (true, true) => Ok(true), - (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), - }; - - // Each of the following flags are only expected in --check mode. - // If we encounter them otherwise, end with an error. - let ignore_missing = check_flag(options::IGNORE_MISSING)?; - let warn = check_flag(options::WARN)?; - let quiet = check_flag(options::QUIET)?; - let strict = check_flag(options::STRICT)?; - let status = check_flag(options::STATUS)?; - let algo_cli = matches .get_one::(options::ALGORITHM) .map(AlgoKind::from_cksum) @@ -165,199 +123,38 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let length = maybe_sanitize_length(algo_cli, input_length)?; - let files = matches.get_many::(options::FILE).map_or_else( - // No files given, read from stdin. - || Box::new(iter::once(OsStr::new("-"))) as Box>, - // At least one file given, read from them. - |files| Box::new(files.map(OsStr::new)) as Box>, - ); - - if check { - // cksum does not support '--check'ing legacy algorithms - if algo_cli.is_some_and(AlgoKind::is_legacy) { - return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); - } - - let text_flag = matches.get_flag(options::TEXT); - let binary_flag = matches.get_flag(options::BINARY); - let tag = matches.get_flag(options::TAG); - - if tag || binary_flag || text_flag { - return Err(ChecksumError::BinaryTextConflict.into()); - } - - // Execute the checksum validation based on the presence of files or the use of stdin - - let verbose = ChecksumVerbose::new(status, quiet, warn); - let opts = ChecksumValidateOptions { - ignore_missing, - strict, - verbose, - }; - - return perform_checksum_validation(files, algo_cli, length, opts); - } + let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; - // Not --check + let output_format = OutputFormat::from_cksum( + algo_cli.unwrap_or(AlgoKind::Crc), + tag, + binary, + /* raw: */ + matches.get_flag(options::RAW), + /* base64: */ + matches.get_flag(options::BASE64), + ); // Print hardware debug info if requested if matches.get_flag(options::DEBUG) { print_cpu_debug_info(); } - // Set the default algorithm to CRC when not '--check'ing. - let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); - - let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; - - let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; - let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); - - let opts = ChecksumComputeOptions { - algo_kind: algo, - output_format: figure_out_output_format( - algo, - tag, - binary, - matches.get_flag(options::RAW), - matches.get_flag(options::BASE64), - ), - line_ending, - }; - - perform_checksum_computation(opts, files)?; - - Ok(()) + checksum_main(algo_cli, length, matches, output_format) } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) - .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) - .about(translate!("cksum-about")) - .override_usage(format_usage(&translate!("cksum-usage"))) - .infer_long_args(true) - .args_override_self(true) - .arg( - Arg::new(options::FILE) - .hide(true) - .action(ArgAction::Append) - .value_parser(ValueParser::os_string()) - .value_hint(clap::ValueHint::FilePath), - ) - .arg( - Arg::new(options::ALGORITHM) - .long(options::ALGORITHM) - .short('a') - .help(translate!("cksum-help-algorithm")) - .value_name("ALGORITHM") - .value_parser(SUPPORTED_ALGORITHMS), - ) - .arg( - Arg::new(options::UNTAGGED) - .long(options::UNTAGGED) - .help(translate!("cksum-help-untagged")) - .action(ArgAction::SetTrue) - .overrides_with(options::TAG), - ) - .arg( - Arg::new(options::TAG) - .long(options::TAG) - .help(translate!("cksum-help-tag")) - .action(ArgAction::SetTrue) - .overrides_with(options::UNTAGGED), - ) - .arg( - Arg::new(options::LENGTH) - .long(options::LENGTH) - .short('l') - .help(translate!("cksum-help-length")) - .action(ArgAction::Set), - ) - .arg( - Arg::new(options::RAW) - .long(options::RAW) - .help(translate!("cksum-help-raw")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::STRICT) - .long(options::STRICT) - .help(translate!("cksum-help-strict")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::CHECK) - .short('c') - .long(options::CHECK) - .help(translate!("cksum-help-check")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::BASE64) - .long(options::BASE64) - .help(translate!("cksum-help-base64")) - .action(ArgAction::SetTrue) - // Even though this could easily just override an earlier '--raw', - // GNU cksum does not permit these flags to be combined: - .conflicts_with(options::RAW), - ) - .arg( - Arg::new(options::TEXT) - .long(options::TEXT) - .short('t') - .hide(true) - .overrides_with(options::BINARY) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::BINARY) - .long(options::BINARY) - .short('b') - .hide(true) - .overrides_with(options::TEXT) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::WARN) - .short('w') - .long("warn") - .help(translate!("cksum-help-warn")) - .action(ArgAction::SetTrue) - .overrides_with_all([options::STATUS, options::QUIET]), - ) - .arg( - Arg::new(options::STATUS) - .long("status") - .help(translate!("cksum-help-status")) - .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::QUIET]), - ) - .arg( - Arg::new(options::QUIET) - .long(options::QUIET) - .help(translate!("cksum-help-quiet")) - .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::STATUS]), - ) - .arg( - Arg::new(options::IGNORE_MISSING) - .long(options::IGNORE_MISSING) - .help(translate!("cksum-help-ignore-missing")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::ZERO) - .long(options::ZERO) - .short('z') - .help(translate!("cksum-help-zero")) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::DEBUG) - .long(options::DEBUG) - .help(translate!("cksum-help-debug")) - .action(ArgAction::SetTrue), - ) + default_checksum_app(translate!("cksum-about"), translate!("cksum-usage")) + .with_algo() + .with_length() + .with_check() + .with_untagged() + .with_tag(true) + .with_raw() + .with_base64() + .with_text(false) + .with_binary() + .with_zero() + .with_debug() .after_help(translate!("cksum-after-help")) } diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 19e8ad9db04..c950df1239b 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -13,7 +13,7 @@ use clap::builder::ValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; use uucore::checksum::compute::{ - ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, + ChecksumComputeOptions, OutputFormat, perform_checksum_computation, }; use uucore::checksum::validate::{ ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, @@ -121,9 +121,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let args = iter::once(program.clone()).chain(args); - // Default binary in Windows, text mode otherwise - let binary_flag_default = cfg!(windows); - let (command, is_hashsum_bin) = uu_app(&binary_name); // FIXME: this should use try_get_matches_from() and crash!(), but at the moment that just @@ -148,13 +145,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { (AlgoKind::from_bin_name(&binary_name)?, length) }; - let binary = if matches.get_flag("binary") { - true - } else if matches.get_flag("text") { - false - } else { - binary_flag_default - }; let check = matches.get_flag("check"); let check_flag = |flag| match (check, matches.get_flag(flag)) { @@ -208,13 +198,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let opts = ChecksumComputeOptions { algo_kind: algo, - output_format: figure_out_output_format( - algo, - matches.get_flag(options::TAG), - binary, - /* raw */ false, - /* base64: */ false, - ), + output_format: OutputFormat::from_standalone(std::env::args_os().into_iter())?, line_ending, }; diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 384e4a83de9..e201ea64e0d 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -73,3 +73,23 @@ checksum-failed-open-file = { $count -> *[other] { $count } listed files could not be read } checksum-error-algo-bad-format = { $file }: { $line }: improperly formatted { $algo } checksum line + +# checksum argument help messages +checksum-help-algorithm = select the digest type to use. See DIGEST below +checksum-help-untagged = create a reversed style checksum, without digest type +checksum-help-tag-default = create a BSD style checksum (default) +checksum-help-tag = create a BSD style checksum +checksum-help-text = read in text mode (default) +checksum-help-length = digest length in bits; must not exceed the max size and must be a multiple of 8 for blake2b; must be 224, 256, 384, or 512 for sha2 or sha3 +checksum-help-check = read checksums from the FILEs and check them +checksum-help-base64 = emit base64-encoded digests, not hexadecimal +checksum-help-raw = emit a raw binary digest, not hexadecimal +checksum-help-zero = end each output line with NUL, not newline, and disable file name escaping + +checksum-help-strict = exit non-zero for improperly formatted checksum lines +checksum-help-warn = warn about improperly formatted checksum lines +checksum-help-status = don't output anything, status code shows success +checksum-help-quiet = don't print OK for each successfully verified file +checksum-help-ignore-missing = don't fail or report status for missing files + +checksum-help-debug = print CPU hardware capability detection info used by cksum diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index 4c844e9b122..6161397bea8 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -73,3 +73,21 @@ checksum-failed-open-file = { $count -> *[other] { $count } fichiers passés n'ont pas pu être lu } checksum-error-algo-bad-format = { $file }: { $line }: ligne invalide pour { $algo } + +# Messages d'aide d'arguments checksum +checksum-help-algorithm = sélectionner le type de condensé à utiliser. Voir DIGEST ci-dessous +checksum-help-untagged = créer une somme de contrôle de style inversé, sans type de condensé +checksum-help-tag-default = créer une somme de contrôle de style BSD (par défaut) +checksum-help-tag = créer une somme de contrôle de style BSD +checksum-help-text = lire en mode texte (par défaut) +checksum-help-length = longueur du condensé en bits ; ne doit pas dépasser le maximum pour l'algorithme blake2 et doit être un multiple de 8 +checksum-help-raw = émettre un condensé binaire brut, pas hexadécimal +checksum-help-strict = sortir avec un code non-zéro pour les lignes de somme de contrôle mal formatées +checksum-help-check = lire les sommes de hachage des FICHIERs et les vérifier +checksum-help-base64 = émettre un condensé base64, pas hexadécimal +checksum-help-warn = avertir des lignes de somme de contrôle mal formatées +checksum-help-status = ne rien afficher, le code de statut indique le succès +checksum-help-quiet = ne pas afficher OK pour chaque fichier vérifié avec succès +checksum-help-ignore-missing = ne pas échouer ou signaler le statut pour les fichiers manquants +checksum-help-zero = terminer chaque ligne de sortie avec NUL, pas un saut de ligne, et désactiver l'échappement des noms de fichiers +checksum-help-debug = afficher les informations de débogage sur la détection de la prise en charge matérielle du processeur diff --git a/src/uucore/src/lib/features/checksum/cli.rs b/src/uucore/src/lib/features/checksum/cli.rs new file mode 100644 index 00000000000..3b23f1cec24 --- /dev/null +++ b/src/uucore/src/lib/features/checksum/cli.rs @@ -0,0 +1,326 @@ +use std::ffi::{OsStr, OsString}; + +use clap::builder::ValueParser; +use clap::{Arg, ArgAction, ArgMatches, Command, ValueHint}; + +use crate::checksum::compute::{ + ChecksumComputeOptions, OutputFormat, perform_checksum_computation, +}; +use crate::checksum::validate::{ChecksumValidateOptions, ChecksumVerbose}; +use crate::checksum::{AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind}; +use crate::error::UResult; +use crate::line_ending::LineEnding; +use crate::{crate_version, format_usage, localized_help_template, translate, util_name}; + +pub mod options { + // cksum-specific + pub const ALGORITHM: &str = "algorithm"; + pub const DEBUG: &str = "debug"; + + pub const FILE: &str = "file"; + + pub const UNTAGGED: &str = "untagged"; + pub const TAG: &str = "tag"; + pub const LENGTH: &str = "length"; + pub const RAW: &str = "raw"; + pub const BASE64: &str = "base64"; + pub const CHECK: &str = "check"; + pub const TEXT: &str = "text"; + pub const BINARY: &str = "binary"; + pub const ZERO: &str = "zero"; + + // check-specific + pub const STRICT: &str = "strict"; + pub const STATUS: &str = "status"; + pub const WARN: &str = "warn"; + pub const IGNORE_MISSING: &str = "ignore-missing"; + pub const QUIET: &str = "quiet"; +} + +pub trait ChecksumCommand { + fn with_algo(self) -> Self; + + fn with_length(self) -> Self; + + fn with_check(self) -> Self; + + fn with_binary(self) -> Self; + + fn with_text(self, default: bool) -> Self; + + fn with_tag(self, default: bool) -> Self; + + fn with_untagged(self) -> Self; + + fn with_raw(self) -> Self; + + fn with_base64(self) -> Self; + + fn with_zero(self) -> Self; + + fn with_debug(self) -> Self; +} + +impl ChecksumCommand for Command { + fn with_algo(self) -> Self { + self.arg( + Arg::new(options::ALGORITHM) + .long(options::ALGORITHM) + .short('a') + .help(translate!("checksum-help-algorithm")) + .value_name("ALGORITHM") + .value_parser(SUPPORTED_ALGORITHMS), + ) + } + + fn with_length(self) -> Self { + self.arg( + Arg::new(options::LENGTH) + .long(options::LENGTH) + .short('l') + .help(translate!("checksum-help-length")) + .action(ArgAction::Set), + ) + } + + fn with_check(self) -> Self { + self.arg( + Arg::new(options::CHECK) + .short('c') + .long(options::CHECK) + .help(translate!("checksum-help-check")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::WARN) + .short('w') + .long("warn") + .help(translate!("checksum-help-warn")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::QUIET]), + ) + .arg( + Arg::new(options::STATUS) + .long("status") + .help(translate!("checksum-help-status")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]), + ) + .arg( + Arg::new(options::QUIET) + .long(options::QUIET) + .help(translate!("checksum-help-quiet")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::STATUS]), + ) + .arg( + Arg::new(options::IGNORE_MISSING) + .long(options::IGNORE_MISSING) + .help(translate!("checksum-help-ignore-missing")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::STRICT) + .long(options::STRICT) + .help(translate!("checksum-help-strict")) + .action(ArgAction::SetTrue), + ) + } + + fn with_binary(self) -> Self { + self.arg( + Arg::new(options::BINARY) + .long(options::BINARY) + .short('b') + .hide(true) + .overrides_with(options::TEXT) + .action(ArgAction::SetTrue), + ) + } + + fn with_text(self, default: bool) -> Self { + let mut arg = Arg::new(options::TEXT) + .long(options::TEXT) + .short('t') + .action(ArgAction::SetTrue); + if default { + arg = arg.help(translate!("checksum-help-text")); + } else { + arg = arg.hide(true); + } + self.arg(arg) + } + + fn with_tag(self, default: bool) -> Self { + self.arg( + Arg::new(options::TAG) + .long(options::TAG) + .help(if default { + translate!("checksum-help-tag-default") + } else { + translate!("checksum-help-tag") + }) + .action(ArgAction::SetTrue), + ) + } + + fn with_untagged(self) -> Self { + self.arg( + Arg::new(options::UNTAGGED) + .long(options::UNTAGGED) + .help(translate!("checksum-help-untagged")) + .action(ArgAction::SetTrue) + .overrides_with(options::TAG), + ) + } + + fn with_raw(self) -> Self { + self.arg( + Arg::new(options::RAW) + .long(options::RAW) + .help(translate!("checksum-help-raw")) + .action(ArgAction::SetTrue), + ) + } + + fn with_base64(self) -> Self { + self.arg( + Arg::new(options::BASE64) + .long(options::BASE64) + .help(translate!("checksum-help-base64")) + .action(ArgAction::SetTrue) + // Even though this could easily just override an earlier '--raw', + // GNU cksum does not permit these flags to be combined: + .conflicts_with(options::RAW), + ) + } + + fn with_zero(self) -> Self { + self.arg( + Arg::new(options::ZERO) + .long(options::ZERO) + .short('z') + .help(translate!("checksum-help-zero")) + .action(ArgAction::SetTrue), + ) + } + + fn with_debug(self) -> Self { + self.arg( + Arg::new(options::DEBUG) + .long(options::DEBUG) + .help(translate!("checksum-help-debug")) + .action(ArgAction::SetTrue), + ) + } +} + +pub fn default_checksum_app(about: String, usage: String) -> Command { + Command::new(util_name()) + .version(crate_version!()) + .help_template(localized_help_template(util_name())) + .about(about) + .override_usage(format_usage(&usage)) + .infer_long_args(true) + .args_override_self(true) + .arg( + Arg::new(options::FILE) + .hide(true) + .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) + .value_hint(ValueHint::FilePath), + ) +} + +pub fn standalone_checksum_app(about: String, usage: String) -> Command { + default_checksum_app(about, usage) + .with_binary() + .with_check() + .with_tag(false) + .with_text(true) + .with_zero() +} + +pub fn standalone_checksum_app_with_length(about: String, usage: String) -> Command { + default_checksum_app(about, usage) + .with_binary() + .with_check() + .with_length() + .with_tag(false) + .with_text(true) + .with_zero() +} + +pub fn checksum_main( + algo: Option, + length: Option, + matches: ArgMatches, + output_format: OutputFormat, +) -> UResult<()> { + let check = matches.get_flag(options::CHECK); + + let check_flag = |flag| match (check, matches.get_flag(flag)) { + (_, false) => Ok(false), + (true, true) => Ok(true), + (false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())), + }; + + // Each of the following flags are only expected in --check mode. + // If we encounter them otherwise, end with an error. + let ignore_missing = check_flag(options::IGNORE_MISSING)?; + let warn = check_flag(options::WARN)?; + let quiet = check_flag(options::QUIET)?; + let strict = check_flag(options::STRICT)?; + let status = check_flag(options::STATUS)?; + + let files = matches.get_many::(options::FILE).map_or_else( + // No files given, read from stdin. + || Box::new(std::iter::once(OsStr::new("-"))) as Box>, + // At least one file given, read from them. + |files| Box::new(files.map(OsStr::new)) as Box>, + ); + + if check { + // cksum does not support '--check'ing legacy algorithms + if algo.is_some_and(AlgoKind::is_legacy) { + return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); + } + + let text_flag = matches.get_flag(options::TEXT); + let binary_flag = matches.get_flag(options::BINARY); + let tag = matches.get_flag(options::TAG); + + if tag || binary_flag || text_flag { + return Err(ChecksumError::BinaryTextConflict.into()); + } + + // Execute the checksum validation based on the presence of files or the use of stdin + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumValidateOptions { + ignore_missing, + strict, + verbose, + }; + + return super::validate::perform_checksum_validation(files, algo, length, opts); + } + + // Not --check + + // Set the default algorithm to CRC when not '--check'ing. + let algo_kind = algo.unwrap_or(AlgoKind::Crc); + + let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; + let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); + + let opts = ChecksumComputeOptions { + algo_kind: algo, + output_format, + line_ending, + }; + + perform_checksum_computation(opts, files)?; + + Ok(()) +} diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index c08765af40e..2e0358fabe4 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -5,12 +5,12 @@ // spell-checker:ignore bitlen -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::fs::File; use std::io::{self, BufReader, Read, Write}; use std::path::Path; -use crate::checksum::{ChecksumError, SizedAlgoKind, digest_reader, escape_filename}; +use crate::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, digest_reader, escape_filename}; use crate::error::{FromIo, UResult, USimpleError}; use crate::line_ending::LineEnding; use crate::sum::DigestOutput; @@ -103,42 +103,76 @@ impl OutputFormat { fn is_raw(&self) -> bool { *self == Self::Raw } -} -/// Use already-processed arguments to decide the output format. -pub fn figure_out_output_format( - algo: SizedAlgoKind, - tag: bool, - binary: bool, - raw: bool, - base64: bool, -) -> OutputFormat { - // Raw output format takes precedence over anything else. - if raw { - return OutputFormat::Raw; - } + /// Find the correct output format for cksum. + pub fn from_cksum(algo: AlgoKind, tag: bool, binary: bool, raw: bool, base64: bool) -> Self { + // Raw output format takes precedence over anything else. + if raw { + return OutputFormat::Raw; + } + + // Then, if the algo is legacy, takes precedence over the rest + if algo.is_legacy() { + return OutputFormat::Legacy; + } - // Then, if the algo is legacy, takes precedence over the rest - if algo.is_legacy() { - return OutputFormat::Legacy; + let digest_format = if base64 { + DigestFormat::Base64 + } else { + DigestFormat::Hexadecimal + }; + + // After that, decide between tagged and untagged output + if tag { + OutputFormat::Tagged(digest_format) + } else { + let reading_mode = if binary { + ReadingMode::Binary + } else { + ReadingMode::Text + }; + OutputFormat::Untagged(digest_format, reading_mode) + } } - let digest_format = if base64 { - DigestFormat::Base64 - } else { - DigestFormat::Hexadecimal - }; + /// Find the correct output format for a standalone checksum util (b2sum, + /// md5sum, etc) + /// + /// Since standalone utils can't use the Raw or Legacy output format, it is + /// decided only using the --tag, --binary and --text arguments. + pub fn from_standalone<'a, I: Iterator>(args: I) -> UResult { + let mut text = true; + let mut tag = false; + + for arg in args { + if arg == "--" { + break; + } else if arg == "--tag" { + tag = true; + text = false; + } else if arg == "--binary" || arg == "-b" { + text = false; + } else if arg == "--text" || arg == "-t" { + // Finding a `--text` after `--tag` is an error. + if tag { + return Err(ChecksumError::TextAfterTag.into()); + } + text = true; + } + } - // After that, decide between tagged and untagged output - if tag { - OutputFormat::Tagged(digest_format) - } else { - let reading_mode = if binary { - ReadingMode::Binary + if tag { + Ok(OutputFormat::Tagged(DigestFormat::Hexadecimal)) } else { - ReadingMode::Text - }; - OutputFormat::Untagged(digest_format, reading_mode) + Ok(OutputFormat::Untagged( + DigestFormat::Hexadecimal, + if text { + ReadingMode::Text + } else { + ReadingMode::Binary + }, + )) + } } } diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 2f3d28b4121..ec363e5643f 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -19,6 +19,7 @@ use crate::sum::{ Sha3_256, Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, }; +pub mod cli; pub mod compute; pub mod validate; @@ -397,6 +398,8 @@ pub enum ChecksumError { BinaryTextConflict, #[error("--text mode is only supported with --untagged")] TextWithoutUntagged, + #[error("--tag does not support --text mode")] + TextAfterTag, #[error("--check is not supported with --algorithm={{bsd,sysv,crc,crc32b}}")] AlgorithmNotSupportedWithCheck, #[error("You cannot combine multiple hash algorithms!")] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 29686ccdea5..5d92b4da6a7 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -170,9 +170,7 @@ pub fn get_canonical_util_name(util_name: &str) -> &str { "[" => "test", // hashsum aliases - all these hash commands are aliases for hashsum - "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "b2sum" => { - "hashsum" - } + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" => "hashsum", "dir" => "ls", // dir is an alias for ls diff --git a/tests/by-util/test_b2sum.rs b/tests/by-util/test_b2sum.rs new file mode 100644 index 00000000000..88c785f5b4a --- /dev/null +++ b/tests/by-util/test_b2sum.rs @@ -0,0 +1,289 @@ +// 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 rstest::rstest; + +use uutests::util::TestScenario; +use uutests::{new_ucmd, util_name}; +// spell-checker:ignore checkfile, nonames, testf, ntestf +macro_rules! get_hash( + ($str:expr) => ( + $str.split(' ').collect::>()[0] + ); +); + +macro_rules! test_digest_with_len { + ($id:ident, $t:ident, $size:expr) => { + mod $id { + use uutests::util::*; + use uutests::util_name; + static LENGTH_ARG: &'static str = concat!("--length=", stringify!($size)); + static EXPECTED_FILE: &'static str = concat!(stringify!($id), ".expected"); + static CHECK_FILE: &'static str = concat!(stringify!($id), ".checkfile"); + static INPUT_FILE: &'static str = "input.txt"; + + #[test] + fn test_single_file() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(LENGTH_ARG) + .arg(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_stdin() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(LENGTH_ARG) + .pipe_in_fixture(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_check() { + let ts = TestScenario::new(util_name!()); + println!("File content='{}'", ts.fixtures.read(INPUT_FILE)); + println!("Check file='{}'", ts.fixtures.read(CHECK_FILE)); + + ts.ucmd() + .args(&[LENGTH_ARG, "--check", CHECK_FILE]) + .succeeds() + .no_stderr() + .stdout_is("input.txt: OK\n"); + } + + #[test] + fn test_zero() { + let ts = TestScenario::new(util_name!()); + assert_eq!( + ts.fixtures.read(EXPECTED_FILE), + get_hash!( + ts.ucmd() + .arg(LENGTH_ARG) + .arg("--zero") + .arg(INPUT_FILE) + .succeeds() + .no_stderr() + .stdout_str() + ) + ); + } + + #[test] + fn test_missing_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("a", "file1\n"); + at.write("c", "file3\n"); + + ts.ucmd() + .args(&[LENGTH_ARG, "a", "b", "c"]) + .fails() + .stdout_contains("a\n") + .stdout_contains("c\n") + .stderr_contains("b: No such file or directory"); + } + } + }; +} + +test_digest_with_len! {b2sum, b2sum, 512} + +#[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_duplicate() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + + scene + .ccmd("b2sum") + .arg("--length=123") + .arg("--length=128") + .arg("testf") + .succeeds() + .stdout_contains("d6d45901dec53e65d2b55fb6e2ab67b0"); +} + +#[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_with_code(1) + .stderr_contains("b2sum: invalid length: '9'") + .stderr_contains("b2sum: length is not a multiple of 8"); +} + +#[rstest] +#[case("513")] +#[case("1024")] +#[case("18446744073709552000")] +fn test_invalid_b2sum_length_option_too_large(#[case] len: &str) { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + + scene + .ccmd("b2sum") + .arg("--length") + .arg(len) + .arg(at.subdir.join("testf")) + .fails_with_code(1) + .no_stdout() + .stderr_contains(format!("b2sum: invalid length: '{len}'")) + .stderr_contains("b2sum: maximum digest length for 'BLAKE2b' is 512 bits"); +} + +#[test] +fn test_check_b2sum_tag_output() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + + scene + .ccmd("b2sum") + .arg("--length=0") + .arg("--tag") + .arg("f") + .succeeds() + .stdout_only("BLAKE2b (f) = 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce\n"); + + scene + .ccmd("b2sum") + .arg("--length=128") + .arg("--tag") + .arg("f") + .succeeds() + .stdout_only("BLAKE2b-128 (f) = cae66941d9efbd404e4d88758ea67670\n"); +} + +#[test] +fn test_check_b2sum_verify() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a", "a\n"); + + scene + .ccmd("b2sum") + .arg("--tag") + .arg("a") + .succeeds() + .stdout_only("BLAKE2b (a) = bedfbb90d858c2d67b7ee8f7523be3d3b54004ef9e4f02f2ad79a1d05bfdfe49b81e3c92ebf99b504102b6bf003fa342587f5b3124c205f55204e8c4b4ce7d7c\n"); + + scene + .ccmd("b2sum") + .arg("--tag") + .arg("-l") + .arg("128") + .arg("a") + .succeeds() + .stdout_only("BLAKE2b-128 (a) = b93e0fc7bb21633c08bba07c5e71dc00\n"); +} + +#[test] +fn test_check_b2sum_strict_check() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("f"); + + let checksums = [ + "2e f\n", + "e4a6a0577479b2b4 f\n", + "cae66941d9efbd404e4d88758ea67670 f\n", + "246c0442cd564aced8145b8b60f1370aa7 f\n", + "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8 f\n", + "4ded8c5fc8b12f3273f877ca585a44ad6503249a2b345d6d9c0e67d85bcb700db4178c0303e93b8f4ad758b8e2c9fd8b3d0c28e585f1928334bb77d36782e8 f\n", + "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce f\n", + ]; + + at.write("ck", &checksums.join("")); + + let output = "f: OK\n".to_string().repeat(checksums.len()); + + scene + .ccmd("b2sum") + .arg("-c") + .arg(at.subdir.join("ck")) + .succeeds() + .stdout_only(&output); + + scene + .ccmd("b2sum") + .arg("--strict") + .arg("-c") + .arg(at.subdir.join("ck")) + .succeeds() + .stdout_only(&output); +} + +#[test] +fn test_help_shows_correct_utility_name() { + // Test b2sum + new_ucmd!() + .arg("--help") + .succeeds() + .stdout_contains("Usage: b2sum") + .stdout_does_not_contain("Usage: hashsum"); +} diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 2f1719b0eca..55f8f5e3774 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -215,7 +215,6 @@ test_digest_with_len! {sha3_224, sha3, 224} test_digest_with_len! {sha3_256, sha3, 256} test_digest_with_len! {sha3_384, sha3, 384} test_digest_with_len! {sha3_512, sha3, 512} -test_digest_with_len! {b2sum, b2sum, 512} #[test] fn test_check_sha1() { diff --git a/tests/fixtures/b2sum/b2sum.checkfile b/tests/fixtures/b2sum/b2sum.checkfile new file mode 100644 index 00000000000..9d6781cc895 --- /dev/null +++ b/tests/fixtures/b2sum/b2sum.checkfile @@ -0,0 +1 @@ +7355dd5276c21cfe0c593b5063b96af3f96a454b33216f58314f44c3ade92e9cd6cec4210a0836246780e9baf927cc50b9a3d7073e8f9bd12780fddbcb930c6d input.txt diff --git a/tests/fixtures/b2sum/b2sum.expected b/tests/fixtures/b2sum/b2sum.expected new file mode 100644 index 00000000000..a0dae0db450 --- /dev/null +++ b/tests/fixtures/b2sum/b2sum.expected @@ -0,0 +1 @@ +7355dd5276c21cfe0c593b5063b96af3f96a454b33216f58314f44c3ade92e9cd6cec4210a0836246780e9baf927cc50b9a3d7073e8f9bd12780fddbcb930c6d \ No newline at end of file diff --git a/tests/fixtures/b2sum/binary.png b/tests/fixtures/b2sum/binary.png new file mode 100644 index 0000000000000000000000000000000000000000..6c4161338f200299744af6dff35884ac5c94516b GIT binary patch literal 8055 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