diff --git a/Cargo.lock b/Cargo.lock index 41f3c0802f9..b4b25fc28f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,6 +608,7 @@ dependencies = [ "uu_ln", "uu_logname", "uu_ls", + "uu_md5sum", "uu_mkdir", "uu_mkfifo", "uu_mknod", @@ -3101,6 +3102,17 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_checksum_common" +version = "0.5.0" +dependencies = [ + "clap", + "codspeed-divan-compat", + "fluent", + "tempfile", + "uucore", +] + [[package]] name = "uu_chgrp" version = "0.5.0" @@ -3147,6 +3159,7 @@ dependencies = [ "codspeed-divan-compat", "fluent", "tempfile", + "uu_checksum_common", "uucore", ] @@ -3529,6 +3542,18 @@ dependencies = [ "uutils_term_grid", ] +[[package]] +name = "uu_md5sum" +version = "0.5.0" +dependencies = [ + "clap", + "codspeed-divan-compat", + "fluent", + "tempfile", + "uu_checksum_common", + "uucore", +] + [[package]] name = "uu_mkdir" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 6a5796a46c6..1f915fb77ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ feat_common_core = [ "basenc", "cat", "cksum", + "md5sum", "comm", "cp", "csplit", @@ -407,6 +408,7 @@ uucore = { version = "0.5.0", package = "uucore", path = "src/uucore" } uucore_procs = { version = "0.5.0", package = "uucore_procs", path = "src/uucore_procs" } uu_ls = { version = "0.5.0", path = "src/uu/ls" } uu_base32 = { version = "0.5.0", path = "src/uu/base32" } +uu_checksum_common = { version = "0.5.0", path = "src/uu/checksum_common" } uutests = { version = "0.5.0", package = "uutests", path = "tests/uutests" } [dependencies] @@ -436,6 +438,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" } +md5sum = { optional = true, version = "0.5.0", package = "uu_md5sum", path = "src/uu/md5sum" } 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 d3430e7e2e5..6118e9ac5ab 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -84,6 +84,7 @@ PROGS := \ basename \ cat \ cksum \ + md5sum \ comm \ cp \ csplit \ @@ -186,7 +187,6 @@ SELINUX_PROGS := \ HASHSUM_PROGS := \ b2sum \ - md5sum \ sha1sum \ sha224sum \ sha256sum \ @@ -223,6 +223,7 @@ TEST_PROGS := \ chmod \ chown \ cksum \ + md5sum \ comm \ cp \ csplit \ diff --git a/build.rs b/build.rs index 9b35eac5eb4..aea2500b2dc 100644 --- a/build.rs +++ b/build.rs @@ -79,7 +79,6 @@ pub fn main() { phf_map.entry(krate, format!("({krate}::uumain, {krate}::uu_app_custom)")); let map_value = format!("({krate}::uumain, {krate}::uu_app_common)"); - phf_map.entry("md5sum", map_value.clone()); phf_map.entry("sha1sum", map_value.clone()); phf_map.entry("sha224sum", map_value.clone()); phf_map.entry("sha256sum", map_value.clone()); diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 2b519a989f3..5b788ee831c 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1569,12 +1569,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uu_checksum_common" +version = "0.5.0" +dependencies = [ + "clap", + "fluent", + "uucore", +] + [[package]] name = "uu_cksum" version = "0.5.0" dependencies = [ "clap", "fluent", + "uu_checksum_common", "uucore", ] diff --git a/src/common/validation.rs b/src/common/validation.rs index 057e8a9120d..c627e351dad 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -51,7 +51,7 @@ 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" => { + "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "b2sum" => { "hashsum" } @@ -98,7 +98,6 @@ mod tests { fn test_get_canonical_util_name() { // Test a few key aliases assert_eq!(get_canonical_util_name("["), "test"); - assert_eq!(get_canonical_util_name("md5sum"), "hashsum"); assert_eq!(get_canonical_util_name("dir"), "ls"); // Test passthrough case diff --git a/src/uu/checksum_common/Cargo.toml b/src/uu/checksum_common/Cargo.toml new file mode 100644 index 00000000000..079a46b4649 --- /dev/null +++ b/src/uu/checksum_common/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "uu_checksum_common" +description = "Base for checksum utils" +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/lib.rs" + +[dependencies] +clap = { workspace = true } +uucore = { workspace = true, features = [ + "checksum", + "encoding", + "sum", + "hardware", +] } +fluent = { workspace = true } + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } + +# [[bench]] +# name = "b2sum_bench" +# harness = false diff --git a/src/uu/checksum_common/LICENSE b/src/uu/checksum_common/LICENSE new file mode 120000 index 00000000000..5853aaea53b --- /dev/null +++ b/src/uu/checksum_common/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/uu/checksum_common/locales/en-US.ftl b/src/uu/checksum_common/locales/en-US.ftl new file mode 100644 index 00000000000..0dddfb19c65 --- /dev/null +++ b/src/uu/checksum_common/locales/en-US.ftl @@ -0,0 +1,19 @@ +ck-common-after-help = With no FILE or when FILE is -, read standard input + +# checksum argument help messages +ck-common-help-algorithm = select the digest type to use. See DIGEST below +ck-common-help-untagged = create a reversed style checksum, without digest type +ck-common-help-tag-default = create a BSD style checksum (default) +ck-common-help-tag = create a BSD style checksum +ck-common-help-text = read in text mode (default) +ck-common-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 +ck-common-help-check = read checksums from the FILEs and check them +ck-common-help-base64 = emit base64-encoded digests, not hexadecimal +ck-common-help-raw = emit a raw binary digest, not hexadecimal +ck-common-help-zero = end each output line with NUL, not newline, and disable file name escaping +ck-common-help-strict = exit non-zero for improperly formatted checksum lines +ck-common-help-warn = warn about improperly formatted checksum lines +ck-common-help-status = don't output anything, status code shows success +ck-common-help-quiet = don't print OK for each successfully verified file +ck-common-help-ignore-missing = don't fail or report status for missing files +ck-common-help-debug = print CPU hardware capability detection info used by cksum diff --git a/src/uu/checksum_common/locales/fr-FR.ftl b/src/uu/checksum_common/locales/fr-FR.ftl new file mode 100644 index 00000000000..0b22519cad4 --- /dev/null +++ b/src/uu/checksum_common/locales/fr-FR.ftl @@ -0,0 +1,19 @@ +ck-common-after-help = Sans FICHIER ou quand FICHER est -, lit l'entrée standard + +# Messages d'aide d'arguments checksum +ck-common-help-algorithm = sélectionner le type de condensé à utiliser. Voir DIGEST ci-dessous +ck-common-help-untagged = créer une somme de contrôle de style inversé, sans type de condensé +ck-common-help-tag-default = créer une somme de contrôle de style BSD (par défaut) +ck-common-help-tag = créer une somme de contrôle de style BSD +ck-common-help-text = lire en mode texte (par défaut) +ck-common-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 +ck-common-help-raw = émettre un condensé binaire brut, pas hexadécimal +ck-common-help-strict = sortir avec un code non-zéro pour les lignes de somme de contrôle mal formatées +ck-common-help-check = lire les sommes de hachage des FICHIERs et les vérifier +ck-common-help-base64 = émettre un condensé base64, pas hexadécimal +ck-common-help-warn = avertir des lignes de somme de contrôle mal formatées +ck-common-help-status = ne rien afficher, le code de statut indique le succès +ck-common-help-quiet = ne pas afficher OK pour chaque fichier vérifié avec succès +ck-common-help-ignore-missing = ne pas échouer ou signaler le statut pour les fichiers manquants +ck-common-help-zero = terminer chaque ligne de sortie avec NUL, pas un saut de ligne, et désactiver l'échappement des noms de fichiers +ck-common-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/checksum_common/src/cli.rs b/src/uu/checksum_common/src/cli.rs new file mode 100644 index 00000000000..cf042a8eee9 --- /dev/null +++ b/src/uu/checksum_common/src/cli.rs @@ -0,0 +1,231 @@ +// 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 clap::{Arg, ArgAction, Command}; +use uucore::{checksum::SUPPORTED_ALGORITHMS, translate}; + +/// List of all options that can be encountered in checksum utils +pub mod options { + // cksum-specific + pub const ALGORITHM: &str = "algorithm"; + pub const DEBUG: &str = "debug"; + + // positional arg + 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"; +} + +/// `ChecksumCommand` is a convenience trait to more easily declare checksum +/// CLI interfaces with +pub trait ChecksumCommand { + fn with_algo(self) -> Self; + + fn with_length(self) -> Self; + + fn with_check_and_opts(self) -> Self; + + fn with_binary(self, require_untagged: bool) -> Self; + + fn with_text(self, require_untagged: bool, is_default: bool) -> Self; + + fn with_tag(self, is_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!("ck-common-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!("ck-common-help-length")) + .action(ArgAction::Set), + ) + } + + fn with_check_and_opts(self) -> Self { + self.arg( + Arg::new(options::CHECK) + .short('c') + .long(options::CHECK) + .help(translate!("ck-common-help-check")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::WARN) + .short('w') + .long("warn") + .help(translate!("ck-common-help-warn")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::QUIET]) + .requires(options::CHECK), + ) + .arg( + Arg::new(options::STATUS) + .long("status") + .help(translate!("ck-common-help-status")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]) + .requires(options::CHECK), + ) + .arg( + Arg::new(options::QUIET) + .long(options::QUIET) + .help(translate!("ck-common-help-quiet")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::STATUS]) + .requires(options::CHECK), + ) + .arg( + Arg::new(options::IGNORE_MISSING) + .long(options::IGNORE_MISSING) + .help(translate!("ck-common-help-ignore-missing")) + .action(ArgAction::SetTrue) + .requires(options::CHECK), + ) + .arg( + Arg::new(options::STRICT) + .long(options::STRICT) + .help(translate!("ck-common-help-strict")) + .action(ArgAction::SetTrue) + .requires(options::CHECK), + ) + } + + fn with_binary(self, require_untagged: bool) -> Self { + let mut arg = Arg::new(options::BINARY) + .long(options::BINARY) + .short('b') + .hide(true) + .action(ArgAction::SetTrue) + .overrides_with(options::TEXT); + + if require_untagged { + arg = arg.requires(options::UNTAGGED); + }; + + self.arg(arg) + } + + fn with_text(self, require_untagged: bool, is_default: bool) -> Self { + let mut arg = Arg::new(options::TEXT) + .long(options::TEXT) + .short('t') + .action(ArgAction::SetTrue); + + if is_default { + arg = arg.help(translate!("ck-common-help-text")); + } else { + arg = arg.hide(true); + } + + if require_untagged { + arg = arg.requires(options::UNTAGGED) + } + + self.arg(arg) + } + + fn with_tag(self, default: bool) -> Self { + let mut arg = Arg::new(options::TAG) + .long(options::TAG) + .action(ArgAction::SetTrue) + .overrides_with(options::BINARY) + .overrides_with(options::TEXT); + arg = if default { + arg.help(translate!("ck-common-help-tag-default")) + } else { + arg.help(translate!("ck-common-help-tag")) + }; + + self.arg(arg) + } + + fn with_untagged(self) -> Self { + self.arg( + Arg::new(options::UNTAGGED) + .long(options::UNTAGGED) + .help(translate!("ck-common-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!("ck-common-help-raw")) + .action(ArgAction::SetTrue), + ) + } + + fn with_base64(self) -> Self { + self.arg( + Arg::new(options::BASE64) + .long(options::BASE64) + .help(translate!("ck-common-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!("ck-common-help-zero")) + .action(ArgAction::SetTrue), + ) + } + + fn with_debug(self) -> Self { + self.arg( + Arg::new(options::DEBUG) + .long(options::DEBUG) + .help(translate!("ck-common-help-debug")) + .action(ArgAction::SetTrue), + ) + } +} diff --git a/src/uu/checksum_common/src/lib.rs b/src/uu/checksum_common/src/lib.rs new file mode 100644 index 00000000000..7191b34eb09 --- /dev/null +++ b/src/uu/checksum_common/src/lib.rs @@ -0,0 +1,171 @@ +// 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 std::ffi::OsString; + +use clap::builder::ValueParser; +use clap::{Arg, ArgAction, ArgMatches, Command, ValueHint}; + +use uucore::checksum::compute::{ + ChecksumComputeOptions, OutputFormat, perform_checksum_computation, +}; +use uucore::checksum::validate::{self, ChecksumValidateOptions, ChecksumVerbose}; +use uucore::checksum::{AlgoKind, ChecksumError, SizedAlgoKind}; +use uucore::error::UResult; +use uucore::line_ending::LineEnding; +use uucore::{crate_version, format_usage, localized_help_template, util_name}; + +mod cli; +pub use cli::ChecksumCommand; +pub use cli::options; + +/// Entrypoint for standalone checksums accepting the `--length` argument +/// +/// Note: Ideally, we wouldn't require a `cmd` to be passed to the function, +/// but for localization purposes, the standalone binaries must declare their +/// command (with about and usage) themselves, otherwise calling --help from +/// the multicall binary results in an unformatted output. +pub fn standalone_with_length_main( + algo: AlgoKind, + cmd: Command, + args: impl uucore::Args, + validate_len: fn(&str) -> UResult>, +) -> UResult<()> { + let matches = uucore::clap_localization::handle_clap_result(cmd, args)?; + let algo = Some(algo); + + let length = matches + .get_one::(options::LENGTH) + .map(String::as_str) + .map(validate_len) + .transpose()? + .flatten(); + + let format = OutputFormat::from_standalone(std::env::args_os()); + + checksum_main(algo, length, matches, format?) +} + +/// Entrypoint for standalone checksums *NOT* accepting the `--length` argument +pub fn standalone_main(algo: AlgoKind, cmd: Command, args: impl uucore::Args) -> UResult<()> { + let matches = uucore::clap_localization::handle_clap_result(cmd, args)?; + let algo = Some(algo); + + let format = OutputFormat::from_standalone(std::env::args_os()); + + checksum_main(algo, None, matches, format?) +} + +/// Base command processing for all the checksum executables. +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()) + .default_value("-") + .hide_default_value(true) + .value_hint(ValueHint::FilePath), + ) +} + +/// Command processing for standalone checksums accepting the `--length` +/// argument +pub fn standalone_checksum_app_with_length(about: String, usage: String) -> Command { + default_checksum_app(about, usage) + .with_binary(/* needs_untagged */ false) + .with_check_and_opts() + .with_length() + .with_tag(false) + .with_text(/* needs_untagged */ false, true) + .with_zero() +} + +/// Command processing for standalone checksums *NOT* accepting the `--length` +/// argument +pub fn standalone_checksum_app(about: String, usage: String) -> Command { + default_checksum_app(about, usage) + .with_binary(/* needs_untagged */ false) + .with_check_and_opts() + .with_tag(false) + .with_text(/* needs_untagged */ false, true) + .with_zero() +} + +/// This is the common entrypoint to all checksum utils. Performs some +/// validation on arguments and proceeds in computing or checking mode. +pub fn checksum_main( + algo: Option, + length: Option, + matches: ArgMatches, + output_format: OutputFormat, +) -> UResult<()> { + let check = matches.get_flag(options::CHECK); + + let ignore_missing = matches.get_flag(options::IGNORE_MISSING); + let warn = matches.get_flag(options::WARN); + let quiet = matches.get_flag(options::QUIET); + let strict = matches.get_flag(options::STRICT); + let status = matches.get_flag(options::STATUS); + + // clap provides the default value -. So we unwrap() safety. + let files = matches + .get_many::(options::FILE) + .unwrap() + .map(|s| s.as_os_str()); + + 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 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/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 8403972731a..d33761a18c7 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -25,6 +25,7 @@ uucore = { workspace = true, features = [ "sum", "hardware", ] } +uu_checksum_common = { workspace = true } fluent = { workspace = true } [dev-dependencies] 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 447e90954f7..39f1e1fece2 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -5,23 +5,16 @@ // spell-checker:ignore (ToDO) fname, algo, bitlen -use clap::builder::ValueParser; -use clap::{Arg, ArgAction, Command}; -use std::ffi::OsString; -use uucore::checksum::compute::{ - ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, -}; -use uucore::checksum::validate::{ - ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, -}; +use clap::Command; +use uu_checksum_common::{ChecksumCommand, checksum_main, default_checksum_app, options}; + +use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind, calculate_blake2b_length_str, - sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, calculate_blake2b_length_str, 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 @@ -47,26 +40,6 @@ 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"; -} - /// cksum has a bunch of legacy behavior. We handle this in this function to /// make sure they are self contained and "easier" to understand. /// @@ -101,14 +74,6 @@ fn maybe_sanitize_length( 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 ignore_missing = matches.get_flag(options::IGNORE_MISSING); - let warn = matches.get_flag(options::WARN); - let quiet = matches.get_flag(options::QUIET); - let strict = matches.get_flag(options::STRICT); - let status = matches.get_flag(options::STATUS); - let algo_cli = matches .get_one::(options::ALGORITHM) .map(AlgoKind::from_cksum) @@ -120,209 +85,36 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let length = maybe_sanitize_length(algo_cli, input_length)?; - // clap provides the default value -. So we unwrap() safety. - let files = matches - .get_many::(options::FILE) - .unwrap() - .map(|s| s.as_os_str()); - - 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); - } - - // Not --check + let output_format = OutputFormat::from_cksum( + algo_cli.unwrap_or(AlgoKind::Crc), + // Making TAG default at clap blocks --untagged + /* tag */ + !matches.get_flag(options::UNTAGGED), + /* binary */ matches.get_flag(options::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 = !matches.get_flag(options::UNTAGGED); // Making TAG default at clap blocks --untagged - let binary = matches.get_flag(options::BINARY); - - 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()) - .default_value("-") - .hide_default_value(true) - .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) - .overrides_with(options::BINARY) - .overrides_with(options::TEXT), - ) - .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) - .requires(options::CHECK), - ) - .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) - .requires(options::UNTAGGED), - ) - .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]) - .requires(options::CHECK), - ) - .arg( - Arg::new(options::STATUS) - .long("status") - .help(translate!("cksum-help-status")) - .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::QUIET]) - .requires(options::CHECK), - ) - .arg( - Arg::new(options::QUIET) - .long(options::QUIET) - .help(translate!("cksum-help-quiet")) - .action(ArgAction::SetTrue) - .overrides_with_all([options::WARN, options::STATUS]) - .requires(options::CHECK), - ) - .arg( - Arg::new(options::IGNORE_MISSING) - .long(options::IGNORE_MISSING) - .help(translate!("cksum-help-ignore-missing")) - .action(ArgAction::SetTrue) - .requires(options::CHECK), - ) - .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_untagged() + .with_tag(true) + .with_length() + .with_raw() + .with_check_and_opts() + .with_base64() + .with_text(true, false) + .with_binary(true) + .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 3bc6dcff5b2..90d6dbe5877 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 ignore_missing = matches.get_flag("ignore-missing"); @@ -196,16 +186,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero")); + let output_format = OutputFormat::from_standalone(std::env::args_os())?; 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, line_ending, }; diff --git a/src/uu/md5sum/Cargo.toml b/src/uu/md5sum/Cargo.toml new file mode 100644 index 00000000000..0d360859e6d --- /dev/null +++ b/src/uu/md5sum/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "uu_md5sum" +description = "md5sum ~ (uutils) Print or check the BLAKE2b checksums" +repository = "https://github.com/uutils/coreutils/tree/main/src/uu/md5sum" +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/md5sum.rs" + +[dependencies] +clap = { workspace = true } +uu_checksum_common = { 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 = "md5sum" +path = "src/main.rs" + +# [[bench]] +# name = "b2sum_bench" +# harness = false diff --git a/src/uu/md5sum/LICENSE b/src/uu/md5sum/LICENSE new file mode 120000 index 00000000000..5853aaea53b --- /dev/null +++ b/src/uu/md5sum/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/uu/md5sum/locales/en-US.ftl b/src/uu/md5sum/locales/en-US.ftl new file mode 100644 index 00000000000..81c3ac5ce35 --- /dev/null +++ b/src/uu/md5sum/locales/en-US.ftl @@ -0,0 +1,3 @@ +md5sum-about = Print or check the MD5 checksums +md5sum-usage = md5sum [OPTIONS] [FILE]... +md5sum-after-help = With no FILE or when FILE is -, read standard input diff --git a/src/uu/md5sum/locales/fr-FR.ftl b/src/uu/md5sum/locales/fr-FR.ftl new file mode 100644 index 00000000000..8da43df3665 --- /dev/null +++ b/src/uu/md5sum/locales/fr-FR.ftl @@ -0,0 +1,2 @@ +md5sum-about = Afficher le MD5 et la taille de chaque fichier +md5sum-usage = md5sum [OPTION]... [FICHIER]... diff --git a/src/uu/md5sum/src/main.rs b/src/uu/md5sum/src/main.rs new file mode 100644 index 00000000000..d5509656f93 --- /dev/null +++ b/src/uu/md5sum/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_md5sum); diff --git a/src/uu/md5sum/src/md5sum.rs b/src/uu/md5sum/src/md5sum.rs new file mode 100644 index 00000000000..f8fe960eb2c --- /dev/null +++ b/src/uu/md5sum/src/md5sum.rs @@ -0,0 +1,24 @@ +// 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 uu_checksum_common::{standalone_checksum_app, standalone_main}; + +use uucore::checksum::AlgoKind; +use uucore::error::UResult; +use uucore::translate; + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + standalone_main(AlgoKind::Md5, uu_app(), args) +} + +#[inline] +pub fn uu_app() -> Command { + standalone_checksum_app(translate!("md5sum-about"), translate!("md5sum-usage")) +} diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index c08765af40e..c5b0cf6e4b6 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 Self::Raw; + } + + // Then, if the algo is legacy, takes precedence over the rest + if algo.is_legacy() { + return Self::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 { + Self::Tagged(digest_format) + } else { + let reading_mode = if binary { + ReadingMode::Binary + } else { + ReadingMode::Text + }; + Self::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(args: impl Iterator) -> 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(Self::Tagged(DigestFormat::Hexadecimal)) } else { - ReadingMode::Text - }; - OutputFormat::Untagged(digest_format, reading_mode) + Ok(Self::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 7cf7fe129fa..f656ee88547 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -394,6 +394,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 7931a69205e..027a3e47d76 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -173,9 +173,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" - } + "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "b2sum" => "hashsum", "dir" => "ls", // dir is an alias for ls diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index ec9a78b433c..19b9be9f10e 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -156,6 +156,11 @@ fn create_bundle( // Then, try to load utility-specific strings from the utility's locale directory try_add_resource_from(get_locales_dir(util_name).ok()); + // checksum binaries also require fluent files from the checksum_common crate + if ["cksum", "md5sum"].contains(&util_name) { + try_add_resource_from(get_locales_dir("checksum_common").ok()); + } + // If we have at least one resource, return the bundle if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) { Ok(bundle) diff --git a/tests/by-util/test_md5sum.rs b/tests/by-util/test_md5sum.rs new file mode 100644 index 00000000000..858d3a2c4fc --- /dev/null +++ b/tests/by-util/test_md5sum.rs @@ -0,0 +1,810 @@ +// 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 uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; +// spell-checker:ignore checkfile, testf, ntestf +macro_rules! get_hash( + ($str:expr) => ( + $str.split(' ').collect::>()[0] + ); +); + +macro_rules! test_digest { + ($id:ident, $t:ident) => { + mod $id { + use uutests::util::*; + use uutests::util_name; + 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(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() + .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(&["--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("--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(&["a", "b", "c"]) + .fails() + .stdout_contains("a\n") + .stdout_contains("c\n") + .stderr_contains("b: No such file or directory"); + } + } + }; +} + +test_digest! {md5, md5} + +#[test] +fn test_check_md5_ignore_missing() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + at.write( + "testf.sha1", + "14758f1afd44c09b7992073ccf00b43d testf\n14758f1afd44c09b7992073ccf00b43d testf2\n", + ); + scene + .ccmd("md5sum") + .arg("-c") + .arg(at.subdir.join("testf.sha1")) + .fails() + .stdout_contains("testf2: FAILED open or read"); + + scene + .ccmd("md5sum") + .arg("-c") + .arg("--ignore-missing") + .arg(at.subdir.join("testf.sha1")) + .succeeds() + .stdout_is("testf: OK\n") + .stderr_is(""); + + scene + .ccmd("md5sum") + .arg("--ignore-missing") + .arg(at.subdir.join("testf.sha1")) + .fails() + .stderr_contains("required argument"); +} + +// Asterisk `*` is a reserved paths character on win32, nor the path can end with a whitespace. +// ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions +#[test] +fn test_check_md5sum() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + { + for f in &["a", " b", "*c", "dd", " "] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + f5b61709718c1ecf8db1aea8547d4698 *c\n\ + b064a020db8018f18ff5ae367d01b212 dd\n\ + d784fa8b6d98d27699781bd9a7cf19f0 ", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\n*c: OK\ndd: OK\n : OK\n") + .stderr_is(""); + } + #[cfg(windows)] + { + for f in &["a", " b", "dd"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + b064a020db8018f18ff5ae367d01b212 dd", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\ndd: OK\n") + .stderr_is(""); + } +} + +// GNU also supports one line sep +#[test] +fn test_check_md5sum_only_one_space() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + for f in ["a", " b", "c"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + 2cd6ee2c70b0bde53fbe6cac3c8b8bb1 c\n", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_only("a: OK\n b: OK\nc: OK\n"); +} + +#[test] +fn test_check_md5sum_reverse_bsd() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + { + for f in &["a", " b", "*c", "dd", " "] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + f5b61709718c1ecf8db1aea8547d4698 *c\n\ + b064a020db8018f18ff5ae367d01b212 dd\n\ + d784fa8b6d98d27699781bd9a7cf19f0 ", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\n*c: OK\ndd: OK\n : OK\n") + .stderr_is(""); + } + #[cfg(windows)] + { + for f in &["a", " b", "dd"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + b064a020db8018f18ff5ae367d01b212 dd", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\ndd: OK\n") + .stderr_is(""); + } +} + +#[test] +fn test_check_md5sum_mixed_format() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + { + for f in &[" b", "*c", "dd", " "] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "bf35d7536c785cf06730d5a40301eba2 b\n\ + f5b61709718c1ecf8db1aea8547d4698 *c\n\ + b064a020db8018f18ff5ae367d01b212 dd\n\ + d784fa8b6d98d27699781bd9a7cf19f0 ", + ); + } + #[cfg(windows)] + { + for f in &[" b", "dd"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "bf35d7536c785cf06730d5a40301eba2 b\n\ + b064a020db8018f18ff5ae367d01b212 dd", + ); + } + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .fails_with_code(1); +} + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails_with_code(1); +} + +#[test] +fn test_conflicting_arg() { + new_ucmd!().arg("--tag").arg("--check").fails_with_code(1); + new_ucmd!().arg("--tag").arg("--text").fails_with_code(1); +} + +#[test] +#[cfg(not(windows))] +fn test_with_escape_filename() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + let filename = "a\nb"; + at.touch(filename); + let result = scene.ccmd("md5sum").arg("--text").arg(filename).succeeds(); + let stdout = result.stdout_str(); + println!("stdout {stdout}"); + assert!(stdout.starts_with('\\')); + assert!(stdout.trim().ends_with("a\\nb")); +} + +#[test] +#[cfg(not(windows))] +fn test_with_escape_filename_zero_text() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + let filename = "a\nb"; + at.touch(filename); + let result = scene + .ccmd("md5sum") + .arg("--text") + .arg("--zero") + .arg(filename) + .succeeds(); + let stdout = result.stdout_str(); + println!("stdout {stdout}"); + assert!(!stdout.starts_with('\\')); + assert!(stdout.contains("a\nb")); +} + +#[test] +fn test_check_empty_line() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write( + "in.md5", + "d41d8cd98f00b204e9800998ecf8427e f\n\nd41d8cd98f00b204e9800998ecf8427e f\ninvalid\n\n", + ); + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stderr_contains("WARNING: 1 line is improperly formatted"); +} + +#[test] +#[cfg(not(windows))] +fn test_check_with_escape_filename() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + + let filename = "a\nb"; + at.touch(filename); + let result = scene.ccmd("md5sum").arg("--tag").arg(filename).succeeds(); + let stdout = result.stdout_str(); + println!("stdout {stdout}"); + assert!(stdout.starts_with("\\MD5")); + assert!(stdout.contains("a\\nb")); + at.write("check.md5", stdout); + let result = scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5") + .succeeds(); + result.stdout_is("\\a\\nb: OK\n"); +} + +#[test] +fn test_check_strict_error() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write( + "in.md5", + "ERR\nERR\nd41d8cd98f00b204e9800998ecf8427e f\nERR\n", + ); + scene + .ccmd("md5sum") + .arg("--check") + .arg("--strict") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_contains("WARNING: 3 lines are improperly formatted"); +} + +#[test] +fn test_check_warn() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write( + "in.md5", + "d41d8cd98f00b204e9800998ecf8427e f\nd41d8cd98f00b204e9800998ecf8427e f\ninvalid\n", + ); + scene + .ccmd("md5sum") + .arg("--check") + .arg("--warn") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stderr_contains("in.md5: 3: improperly formatted MD5 checksum line") + .stderr_contains("WARNING: 1 line is improperly formatted"); + + // with strict, we should fail the execution + scene + .ccmd("md5sum") + .arg("--check") + .arg("--strict") + .arg(at.subdir.join("in.md5")) + .fails(); +} + +#[test] +fn test_check_status() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "MD5(f)= d41d8cd98f00b204e9800998ecf8427f\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg("--status") + .arg(at.subdir.join("in.md5")) + .fails() + .no_output(); +} + +#[test] +fn test_check_status_code() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427f f\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg("--status") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_is("") + .stdout_is(""); +} + +#[test] +fn test_sha1_with_md5sum_should_fail() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("f.sha1", "SHA1 (f) = d41d8cd98f00b204e9800998ecf8427e\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("f.sha1")) + .fails() + .stderr_contains("f.sha1: no properly formatted checksum lines found") + .stderr_does_not_contain("WARNING: 1 line is improperly formatted"); +} + +#[test] +// Disabled on Windows because of the "*" +#[cfg(not(windows))] +fn test_check_one_two_space_star() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("empty"); + + // with one space, the "*" is removed + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427e *empty\n"); + + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stdout_is("empty: OK\n"); + + // with two spaces, the "*" is not removed + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427e *empty\n"); + // First should fail as *empty doesn't exit + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .fails() + .stdout_is("*empty: FAILED open or read\n"); + + at.touch("*empty"); + // Should pass as we have the file + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stdout_is("*empty: OK\n"); +} + +#[test] +// Disabled on Windows because of the "*" +#[cfg(not(windows))] +fn test_check_space_star_or_not() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("a"); + at.touch("*c"); + + // with one space, the "*" is removed + at.write( + "in.md5", + "d41d8cd98f00b204e9800998ecf8427e *c\n + d41d8cd98f00b204e9800998ecf8427e a\n", + ); + + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .fails() + .stdout_contains("c: FAILED") + .stdout_does_not_contain("a: FAILED") + .stderr_contains("WARNING: 1 line is improperly formatted"); + + at.write( + "in.md5", + "d41d8cd98f00b204e9800998ecf8427e a\n + d41d8cd98f00b204e9800998ecf8427e *c\n", + ); + + // First should fail as *empty doesn't exit + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stdout_contains("a: OK") + .stderr_contains("WARNING: 1 line is improperly formatted"); +} + +#[test] +fn test_check_no_backslash_no_space() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "MD5(f)= d41d8cd98f00b204e9800998ecf8427e\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stdout_is("f: OK\n"); +} + +#[test] +fn test_incomplete_format() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "MD5 (\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_contains("no properly formatted checksum lines found"); +} + +#[test] +fn test_start_error() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "ERR\nd41d8cd98f00b204e9800998ecf8427e f\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg("--strict") + .arg(at.subdir.join("in.md5")) + .fails() + .stdout_is("f: OK\n") + .stderr_contains("WARNING: 1 line is improperly formatted"); +} + +#[test] +fn test_check_check_ignore_no_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427f missing\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg("--ignore-missing") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_contains("in.md5: no file was verified"); +} + +#[test] +fn test_check_directory_error() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("d"); + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427f d\n"); + #[cfg(not(windows))] + let err_msg = "md5sum: d: Is a directory\n"; + #[cfg(windows)] + let err_msg = "md5sum: d: Permission denied\n"; + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_contains(err_msg); +} + +#[test] +#[cfg(not(windows))] +fn test_continue_after_directory_error() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("d"); + at.touch("file"); + at.touch("no_read_perms"); + at.set_mode("no_read_perms", 200); + + let (out, err_msg) = ( + "d41d8cd98f00b204e9800998ecf8427e file\n", + [ + "md5sum: d: Is a directory", + "md5sum: dne: No such file or directory", + "md5sum: no_read_perms: Permission denied\n", + ] + .join("\n"), + ); + + scene + .ccmd("md5sum") + .arg("d") + .arg("dne") + .arg("no_read_perms") + .arg("file") + .fails() + .stdout_is(out) + .stderr_is(err_msg); +} + +#[test] +fn test_check_quiet() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427e f\n"); + scene + .ccmd("md5sum") + .arg("--quiet") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .no_output(); + + // incorrect md5 + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427f f\n"); + scene + .ccmd("md5sum") + .arg("--quiet") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .fails() + .stdout_contains("f: FAILED") + .stderr_contains("WARNING: 1 computed checksum did NOT match"); + + scene + .ccmd("md5sum") + .arg("--quiet") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_contains("required argument"); + scene + .ccmd("md5sum") + .arg("--strict") + .arg(at.subdir.join("in.md5")) + .fails() + .stderr_contains("required argument"); +} + +#[test] +fn test_star_to_start() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("f"); + at.write("in.md5", "d41d8cd98f00b204e9800998ecf8427e *f\n"); + scene + .ccmd("md5sum") + .arg("--check") + .arg(at.subdir.join("in.md5")) + .succeeds() + .stdout_only("f: OK\n"); +} + +#[test] +fn test_check_md5_comment_line() { + // A comment in a checksum file shall be discarded unnoticed. + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo", "foo-content\n"); + at.write( + "MD5SUM", + "\ + # This is a comment\n\ + 8411029f3f5b781026a93db636aca721 foo\n\ + # next comment is empty\n#", + ); + + scene + .ccmd("md5sum") + .arg("--check") + .arg("MD5SUM") + .succeeds() + .stdout_contains("foo: OK") + .no_stderr(); +} + +#[test] +fn test_check_md5_comment_only() { + // A file only filled with comments is equivalent to an empty file, + // and therefore produces an error. + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo", "foo-content\n"); + at.write("MD5SUM", "# This is a comment\n"); + + scene + .ccmd("md5sum") + .arg("--check") + .arg("MD5SUM") + .fails() + .stderr_contains("no properly formatted checksum lines found"); +} + +#[test] +fn test_check_md5_comment_leading_space() { + // A file only filled with comments is equivalent to an empty file, + // and therefore produces an error. + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("foo", "foo-content\n"); + at.write( + "MD5SUM", + " # This is a comment\n\ + 8411029f3f5b781026a93db636aca721 foo\n", + ); + + scene + .ccmd("md5sum") + .arg("--check") + .arg("MD5SUM") + .succeeds() + .stdout_contains("foo: OK") + .stderr_contains("WARNING: 1 line is improperly formatted"); +} + +#[test] +fn test_help_shows_correct_utility_name() { + // Test md5sum + new_ucmd!() + .arg("--help") + .succeeds() + .stdout_contains("Usage: md5sum") + .stdout_does_not_contain("Usage: hashsum"); +} diff --git a/tests/fixtures/md5sum/input.txt b/tests/fixtures/md5sum/input.txt new file mode 100644 index 00000000000..8c01d89ae06 --- /dev/null +++ b/tests/fixtures/md5sum/input.txt @@ -0,0 +1 @@ +hello, world \ No newline at end of file diff --git a/tests/fixtures/md5sum/md5.checkfile b/tests/fixtures/md5sum/md5.checkfile new file mode 100644 index 00000000000..328e1bd9454 --- /dev/null +++ b/tests/fixtures/md5sum/md5.checkfile @@ -0,0 +1 @@ +e4d7f1b4ed2e42d15898f4b27b019da4 input.txt diff --git a/tests/fixtures/md5sum/md5.expected b/tests/fixtures/md5sum/md5.expected new file mode 100644 index 00000000000..9a78d4ab7d7 --- /dev/null +++ b/tests/fixtures/md5sum/md5.expected @@ -0,0 +1 @@ +e4d7f1b4ed2e42d15898f4b27b019da4 \ No newline at end of file diff --git a/tests/tests.rs b/tests/tests.rs index 9ffdfd4a312..1b2a2131b20 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -68,6 +68,10 @@ mod test_cksum; #[path = "by-util/test_comm.rs"] mod test_comm; +#[cfg(feature = "md5sum")] +#[path = "by-util/test_md5sum.rs"] +mod test_md5sum; + #[cfg(feature = "cp")] #[path = "by-util/test_cp.rs"] mod test_cp;