diff --git a/Cargo.lock b/Cargo.lock index ef717e281..e763db08a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3239,6 +3239,17 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_checksum_common" +version = "0.6.0" +dependencies = [ + "clap", + "codspeed-divan-compat", + "fluent", + "tempfile", + "uucore", +] + [[package]] name = "uu_chgrp" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 398c60017..b30b0672c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -407,6 +407,7 @@ uucore = { version = "0.6.0", package = "uucore", path = "src/uucore" } uucore_procs = { version = "0.6.0", package = "uucore_procs", path = "src/uucore_procs" } uu_ls = { version = "0.6.0", path = "src/uu/ls" } uu_base32 = { version = "0.6.0", path = "src/uu/base32" } +uu_checksum_common = { version = "0.6.0", path = "src/uu/checksum_common" } uutests = { version = "0.6.0", package = "uutests", path = "tests/uutests" } [dependencies] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4094c806b..62bd4b157 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1692,12 +1692,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uu_checksum_common" +version = "0.6.0" +dependencies = [ + "clap", + "fluent", + "uucore", +] + [[package]] name = "uu_cksum" version = "0.6.0" dependencies = [ "clap", "fluent", + "uu_checksum_common", "uucore", ] diff --git a/src/uu/checksum_common/Cargo.toml b/src/uu/checksum_common/Cargo.toml new file mode 100644 index 000000000..079a46b46 --- /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 000000000..5853aaea5 --- /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 000000000..0dddfb19c --- /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 000000000..0b22519ca --- /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 000000000..a5e979e30 --- /dev/null +++ b/src/uu/checksum_common/src/cli.rs @@ -0,0 +1,215 @@ +// 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) -> Self; + + fn with_text(self, 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]), + ) + .arg( + Arg::new(options::STATUS) + .long("status") + .help(translate!("ck-common-help-status")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]), + ) + .arg( + Arg::new(options::QUIET) + .long(options::QUIET) + .help(translate!("ck-common-help-quiet")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::WARN]), + ) + .arg( + Arg::new(options::IGNORE_MISSING) + .long(options::IGNORE_MISSING) + .help(translate!("ck-common-help-ignore-missing")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::STRICT) + .long(options::STRICT) + .help(translate!("ck-common-help-strict")) + .action(ArgAction::SetTrue), + ) + } + + fn with_binary(self) -> Self { + self.arg( + Arg::new(options::BINARY) + .long(options::BINARY) + .short('b') + .hide(true) + .action(ArgAction::SetTrue), + ) + } + + fn with_text(self, is_default: bool) -> Self { + let mut arg = Arg::new(options::TEXT) + .long(options::TEXT) + .short('t') + .action(ArgAction::SetTrue); + + arg = if is_default { + arg.help(translate!("ck-common-help-text")) + } else { + arg.hide(true) + }; + + self.arg(arg) + } + + fn with_tag(self, default: bool) -> Self { + let mut arg = Arg::new(options::TAG) + .long(options::TAG) + .action(ArgAction::SetTrue); + + 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), + ) + } + + 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 000000000..1d5a27265 --- /dev/null +++ b/src/uu/checksum_common/src/lib.rs @@ -0,0 +1,207 @@ +// 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; + +/// Expands to generate the right `uumain` and `uu_app` functions +/// for standalone checksum binaries. +/// +/// Example: +/// ``` +/// use uu_checksum_common::declare_standalone; +/// use uucore::checksum::AlgoKind; +/// +/// declare_standalone!("sha512sum", AlgoKind::Sha512); +/// ``` +#[macro_export] +macro_rules! declare_standalone { + ($bin:literal, $kind:expr) => { + #[::uucore::main] + pub fn uumain(args: impl ::uucore::Args) -> ::uucore::error::UResult<()> { + ::uu_checksum_common::standalone_main($kind, uu_app(), args) + } + + #[inline] + pub fn uu_app() -> ::clap::Command { + ::uu_checksum_common::standalone_checksum_app( + ::uucore::translate!(concat!($bin, "-about")), + ::uucore::translate!(concat!($bin, "-usage")), + ) + } + }; +} + +/// 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() + .with_check_and_opts() + .with_length() + .with_tag(false) + .with_text(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() + .with_check_and_opts() + .with_tag(false) + .with_text(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("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("ignore-missing")?; + let warn = check_flag("warn")?; + let quiet = check_flag("quiet")?; + let strict = check_flag("strict")?; + let status = check_flag("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(()) +}