Merge pull request #9135 from RenjiSann/cksum-reworks

cksum/hashsum: merge digest computation & various improvements
This commit is contained in:
Sylvestre Ledru
2025-11-20 06:18:56 +01:00
committed by GitHub
9 changed files with 1187 additions and 1190 deletions
+27 -310
View File
@@ -3,270 +3,25 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) fname, algo
// spell-checker:ignore (ToDO) fname, algo, bitlen
use clap::builder::ValueParser;
use clap::{Arg, ArgAction, Command};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{BufReader, Read, Write, stdin, stdout};
use std::iter;
use std::path::Path;
use uucore::checksum::compute::{
ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation,
};
use uucore::checksum::validate::{
ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation,
};
use uucore::checksum::{
ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC,
ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SHA2, ALGORITHM_OPTIONS_SHA3,
ALGORITHM_OPTIONS_SYSV, ChecksumError, ChecksumOptions, ChecksumVerbose, HashAlgorithm,
LEGACY_ALGORITHMS, SUPPORTED_ALGORITHMS, calculate_blake2b_length_str, detect_algo,
digest_reader, perform_checksum_validation, sanitize_sha2_sha3_length_str,
AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind, calculate_blake2b_length_str,
sanitize_sha2_sha3_length_str,
};
use uucore::translate;
use uucore::{
encoding,
error::{FromIo, UResult, USimpleError},
format_usage,
line_ending::LineEnding,
os_str_as_bytes, show,
sum::Digest,
};
struct Options {
algo_name: &'static str,
digest: Box<dyn Digest + 'static>,
output_bits: usize,
length: Option<usize>,
output_format: OutputFormat,
line_ending: LineEnding,
}
/// Reading mode used to compute digest.
///
/// On most linux systems, this is irrelevant, as there is no distinction
/// between text and binary files. Refer to GNU's cksum documentation for more
/// information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReadingMode {
Binary,
Text,
}
impl ReadingMode {
#[inline]
fn as_char(&self) -> char {
match self {
Self::Binary => '*',
Self::Text => ' ',
}
}
}
/// Whether to write the digest as hexadecimal or encoded in base64.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DigestFormat {
Hexadecimal,
Base64,
}
impl DigestFormat {
#[inline]
fn is_base64(&self) -> bool {
*self == Self::Base64
}
}
/// Holds the representation that shall be used for printing a checksum line
#[derive(Debug, PartialEq, Eq)]
enum OutputFormat {
/// Raw digest
Raw,
/// Selected for older algorithms which had their custom formatting
///
/// Default for crc, sysv, bsd
Legacy,
/// `$ALGO_NAME ($FILENAME) = $DIGEST`
Tagged(DigestFormat),
/// '$DIGEST $FLAG$FILENAME'
/// where 'flag' depends on the reading mode
///
/// Default for standalone checksum utilities
Untagged(DigestFormat, ReadingMode),
}
impl OutputFormat {
#[inline]
fn is_raw(&self) -> bool {
*self == Self::Raw
}
}
fn print_legacy_checksum(
options: &Options,
filename: &OsStr,
sum: &str,
size: usize,
) -> UResult<()> {
debug_assert!(LEGACY_ALGORITHMS.contains(&options.algo_name));
// Print the sum
match options.algo_name {
ALGORITHM_OPTIONS_SYSV => print!(
"{} {}",
sum.parse::<u16>().unwrap(),
size.div_ceil(options.output_bits),
),
ALGORITHM_OPTIONS_BSD => {
// The BSD checksum output is 5 digit integer
let bsd_width = 5;
print!(
"{:0bsd_width$} {:bsd_width$}",
sum.parse::<u16>().unwrap(),
size.div_ceil(options.output_bits),
);
}
ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => {
print!("{sum} {size}");
}
_ => unreachable!("Not a legacy algorithm"),
}
// Print the filename after a space if not stdin
if filename != "-" {
print!(" ");
let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?);
}
Ok(())
}
fn print_tagged_checksum(options: &Options, filename: &OsStr, sum: &String) -> UResult<()> {
// Print algo name and opening parenthesis.
print!(
"{} (",
match (options.algo_name, options.length) {
// Multiply the length by 8, as we want to print the length in bits.
(ALGORITHM_OPTIONS_BLAKE2B, Some(l)) => format!("BLAKE2b-{}", l * 8),
(ALGORITHM_OPTIONS_BLAKE2B, None) => "BLAKE2b".into(),
(name, _) => name.to_ascii_uppercase(),
}
);
// Print filename
let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?);
// Print closing parenthesis and sum
print!(") = {sum}");
Ok(())
}
fn print_untagged_checksum(
filename: &OsStr,
sum: &String,
reading_mode: ReadingMode,
) -> UResult<()> {
// Print checksum and reading mode flag
print!("{sum} {}", reading_mode.as_char());
// Print filename
let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?);
Ok(())
}
/// Calculate checksum
///
/// # Arguments
///
/// * `options` - CLI options for the assigning checksum algorithm
/// * `files` - A iterator of [`OsStr`] which is a bunch of files that are using for calculating checksum
fn cksum<'a, I>(mut options: Options, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
let mut files = files.peekable();
while let Some(filename) = files.next() {
// Check that in raw mode, we are not provided with several files.
if options.output_format.is_raw() && files.peek().is_some() {
return Err(Box::new(ChecksumError::RawMultipleFiles));
}
let filepath = Path::new(filename);
let stdin_buf;
let file_buf;
if filepath.is_dir() {
show!(USimpleError::new(
1,
translate!("cksum-error-is-directory", "file" => filepath.display())
));
continue;
}
// Handle the file input
let mut file = BufReader::new(if filename == "-" {
stdin_buf = stdin();
Box::new(stdin_buf) as Box<dyn Read>
} else {
file_buf = match File::open(filepath) {
Ok(file) => file,
Err(err) => {
show!(err.map_err_context(|| filepath.to_string_lossy().to_string()));
continue;
}
};
Box::new(file_buf) as Box<dyn Read>
});
let (sum_hex, sz) =
digest_reader(&mut options.digest, &mut file, false, options.output_bits)
.map_err_context(|| translate!("cksum-error-failed-to-read-input"))?;
// Encodes the sum if df is Base64, leaves as-is otherwise.
let encode_sum = |sum: String, df: DigestFormat| {
if df.is_base64() {
encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap())
} else {
sum
}
};
match options.output_format {
OutputFormat::Raw => {
let bytes = match options.algo_name {
ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => {
sum_hex.parse::<u32>().unwrap().to_be_bytes().to_vec()
}
ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => {
sum_hex.parse::<u16>().unwrap().to_be_bytes().to_vec()
}
_ => hex::decode(sum_hex).unwrap(),
};
// Cannot handle multiple files anyway, output immediately.
stdout().write_all(&bytes)?;
return Ok(());
}
OutputFormat::Legacy => {
print_legacy_checksum(&options, filename, &sum_hex, sz)?;
}
OutputFormat::Tagged(digest_format) => {
print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?;
}
OutputFormat::Untagged(digest_format, reading_mode) => {
print_untagged_checksum(
filename,
&encode_sum(sum_hex, digest_format),
reading_mode,
)?;
}
}
print!("{}", options.line_ending);
}
Ok(())
}
use uucore::error::UResult;
use uucore::line_ending::LineEnding;
use uucore::{format_usage, translate};
mod options {
pub const ALGORITHM: &str = "algorithm";
@@ -329,46 +84,9 @@ fn handle_tag_text_binary_flags<S: AsRef<OsStr>>(
Ok((tag, binary))
}
/// Use already-processed arguments to decide the output format.
fn figure_out_output_format(
algo: &HashAlgorithm,
tag: bool,
binary: bool,
raw: bool,
base64: bool,
) -> OutputFormat {
// 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 LEGACY_ALGORITHMS.contains(&algo.name) {
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)
}
}
/// Sanitize the `--length` argument depending on `--algorithm` and `--length`.
fn maybe_sanitize_length(
algo_cli: Option<&str>,
algo_cli: Option<AlgoKind>,
input_length: Option<&str>,
) -> UResult<Option<usize>> {
match (algo_cli, input_length) {
@@ -376,12 +94,12 @@ fn maybe_sanitize_length(
(_, None) => Ok(None),
// For SHA2 and SHA3, if a length is provided, ensure it is correct.
(Some(algo @ (ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3)), Some(s_len)) => {
(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(ALGORITHM_OPTIONS_BLAKE2B), Some(len)) => calculate_blake2b_length_str(len),
(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.
@@ -398,7 +116,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let algo_cli = matches
.get_one::<String>(options::ALGORITHM)
.map(String::as_str);
.map(AlgoKind::from_cksum)
.transpose()?;
let input_length = matches
.get_one::<String>(options::LENGTH)
@@ -415,7 +134,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
if check {
// cksum does not support '--check'ing legacy algorithms
if algo_cli.is_some_and(|algo_name| LEGACY_ALGORITHMS.contains(&algo_name)) {
if algo_cli.is_some_and(AlgoKind::is_legacy) {
return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into());
}
@@ -435,8 +154,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Execute the checksum validation based on the presence of files or the use of stdin
let verbose = ChecksumVerbose::new(status, quiet, warn);
let opts = ChecksumOptions {
binary: binary_flag,
let opts = ChecksumValidateOptions {
ignore_missing,
strict,
verbose,
@@ -448,31 +166,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Not --check
// Set the default algorithm to CRC when not '--check'ing.
let algo_name = algo_cli.unwrap_or(ALGORITHM_OPTIONS_CRC);
let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc);
let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?;
let algo = detect_algo(algo_name, length)?;
let algo = SizedAlgoKind::from_unsized(algo_kind, length)?;
let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO));
let output_format = figure_out_output_format(
&algo,
algo,
tag,
binary,
matches.get_flag(options::RAW),
matches.get_flag(options::BASE64),
);
let opts = Options {
algo_name: algo.name,
digest: (algo.create_fn)(),
output_bits: algo.bits,
length,
let opts = ChecksumComputeOptions {
algo_kind: algo,
output_format,
line_ending,
binary: false,
no_names: false,
};
cksum(opts, files)?;
perform_checksum_computation(opts, files)?;
Ok(())
}
+1 -1
View File
@@ -19,7 +19,7 @@ path = "src/hashsum.rs"
[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = ["checksum", "sum"] }
uucore = { workspace = true, features = ["checksum", "encoding", "sum"] }
fluent = { workspace = true }
[[bin]]
+65 -200
View File
@@ -3,53 +3,28 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) algo, algoname, regexes, nread, nonames
// spell-checker:ignore (ToDO) algo, algoname, bitlen, regexes, nread, nonames
use clap::ArgAction;
use clap::builder::ValueParser;
use clap::value_parser;
use clap::{Arg, ArgMatches, Command};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{BufReader, Read, stdin};
use std::iter;
use std::num::ParseIntError;
use std::path::Path;
use uucore::checksum::ChecksumError;
use uucore::checksum::ChecksumOptions;
use uucore::checksum::ChecksumVerbose;
use uucore::checksum::HashAlgorithm;
use uucore::checksum::calculate_blake2b_length;
use uucore::checksum::create_sha3;
use uucore::checksum::detect_algo;
use uucore::checksum::digest_reader;
use uucore::checksum::escape_filename;
use uucore::checksum::perform_checksum_validation;
use uucore::error::{UResult, strip_errno};
use uucore::format_usage;
use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256};
use uucore::translate;
use clap::builder::ValueParser;
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
use uucore::checksum::compute::{
ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation,
};
use uucore::checksum::validate::{
ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation,
};
use uucore::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length};
use uucore::error::UResult;
use uucore::line_ending::LineEnding;
use uucore::{format_usage, translate};
const NAME: &str = "hashsum";
// Using the same read buffer size as GNU
const READ_BUFFER_SIZE: usize = 32 * 1024;
struct Options<'a> {
algoname: &'static str,
digest: Box<dyn Digest + 'static>,
binary: bool,
binary_name: &'a str,
//check: bool,
tag: bool,
nonames: bool,
//status: bool,
//quiet: bool,
//strict: bool,
//warn: bool,
output_bits: usize,
zero: bool,
//ignore_missing: bool,
}
/// Creates a hasher instance based on the command-line flags.
///
@@ -63,10 +38,10 @@ struct Options<'a> {
/// the output length in bits or an Err if multiple hash algorithms are specified or if a
/// required flag is missing.
#[allow(clippy::cognitive_complexity)]
fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<HashAlgorithm> {
let mut alg: Option<HashAlgorithm> = None;
fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<(AlgoKind, Option<usize>)> {
let mut alg: Option<(AlgoKind, Option<usize>)> = None;
let mut set_or_err = |new_alg: HashAlgorithm| -> UResult<()> {
let mut set_or_err = |new_alg: (AlgoKind, Option<usize>)| -> UResult<()> {
if alg.is_some() {
return Err(ChecksumError::CombineMultipleAlgorithms.into());
}
@@ -75,80 +50,57 @@ fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<HashAlgorithm> {
};
if matches.get_flag("md5") {
set_or_err(detect_algo("md5sum", None)?)?;
set_or_err((AlgoKind::Md5, None))?;
}
if matches.get_flag("sha1") {
set_or_err(detect_algo("sha1sum", None)?)?;
set_or_err((AlgoKind::Sha1, None))?;
}
if matches.get_flag("sha224") {
set_or_err(detect_algo("sha224sum", None)?)?;
set_or_err((AlgoKind::Sha224, None))?;
}
if matches.get_flag("sha256") {
set_or_err(detect_algo("sha256sum", None)?)?;
set_or_err((AlgoKind::Sha256, None))?;
}
if matches.get_flag("sha384") {
set_or_err(detect_algo("sha384sum", None)?)?;
set_or_err((AlgoKind::Sha384, None))?;
}
if matches.get_flag("sha512") {
set_or_err(detect_algo("sha512sum", None)?)?;
set_or_err((AlgoKind::Sha512, None))?;
}
if matches.get_flag("b2sum") {
set_or_err(detect_algo("b2sum", None)?)?;
set_or_err((AlgoKind::Blake2b, None))?;
}
if matches.get_flag("b3sum") {
set_or_err(detect_algo("b3sum", None)?)?;
set_or_err((AlgoKind::Blake3, None))?;
}
if matches.get_flag("sha3") {
match matches.get_one::<usize>("bits") {
Some(bits) => set_or_err(create_sha3(*bits)?)?,
Some(bits @ (224 | 256 | 384 | 512)) => set_or_err((AlgoKind::Sha3, Some(*bits)))?,
Some(bits) => return Err(ChecksumError::InvalidLengthForSha(bits.to_string()).into()),
None => return Err(ChecksumError::LengthRequired("SHA3".into()).into()),
}
}
if matches.get_flag("sha3-224") {
set_or_err(HashAlgorithm {
name: "SHA3-224",
create_fn: Box::new(|| Box::new(Sha3_224::new())),
bits: 224,
})?;
set_or_err((AlgoKind::Sha3, Some(224)))?;
}
if matches.get_flag("sha3-256") {
set_or_err(HashAlgorithm {
name: "SHA3-256",
create_fn: Box::new(|| Box::new(Sha3_256::new())),
bits: 256,
})?;
set_or_err((AlgoKind::Sha3, Some(256)))?;
}
if matches.get_flag("sha3-384") {
set_or_err(HashAlgorithm {
name: "SHA3-384",
create_fn: Box::new(|| Box::new(Sha3_384::new())),
bits: 384,
})?;
set_or_err((AlgoKind::Sha3, Some(384)))?;
}
if matches.get_flag("sha3-512") {
set_or_err(HashAlgorithm {
name: "SHA3-512",
create_fn: Box::new(|| Box::new(Sha3_512::new())),
bits: 512,
})?;
set_or_err((AlgoKind::Sha3, Some(512)))?;
}
if matches.get_flag("shake128") {
match matches.get_one::<usize>("bits") {
Some(bits) => set_or_err(HashAlgorithm {
name: "SHAKE128",
create_fn: Box::new(|| Box::new(Shake128::new())),
bits: *bits,
})?,
Some(bits) => set_or_err((AlgoKind::Shake128, Some(*bits)))?,
None => return Err(ChecksumError::LengthRequired("SHAKE128".into()).into()),
}
}
if matches.get_flag("shake256") {
match matches.get_one::<usize>("bits") {
Some(bits) => set_or_err(HashAlgorithm {
name: "SHAKE256",
create_fn: Box::new(|| Box::new(Shake256::new())),
bits: *bits,
})?,
Some(bits) => set_or_err((AlgoKind::Shake256, Some(*bits)))?,
None => return Err(ChecksumError::LengthRequired("SHAKE256".into()).into()),
}
}
@@ -198,10 +150,10 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
None => None,
};
let algo = if is_hashsum_bin {
let (algo_kind, length) = if is_hashsum_bin {
create_algorithm_from_flags(&matches)?
} else {
detect_algo(&binary_name, length)?
(AlgoKind::from_bin_name(&binary_name)?, length)
};
let binary = if matches.get_flag("binary") {
@@ -213,9 +165,9 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
};
let check = matches.get_flag("check");
let status = matches.get_flag("status");
let quiet = matches.get_flag("quiet") || status;
let quiet = matches.get_flag("quiet");
let strict = matches.get_flag("strict");
let warn = matches.get_flag("warn") && !status;
let warn = matches.get_flag("warn");
let ignore_missing = matches.get_flag("ignore-missing");
if ignore_missing && !check {
@@ -227,16 +179,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
// on Windows, allow --binary/--text to be used with --check
// and keep the behavior of defaulting to binary
#[cfg(not(windows))]
let binary = {
{
let text_flag = matches.get_flag("text");
let binary_flag = matches.get_flag("binary");
if binary_flag || text_flag {
return Err(ChecksumError::BinaryTextConflict.into());
}
false
};
}
// Execute the checksum validation based on the presence of files or the use of stdin
// Determine the source of input: a list of files or stdin.
@@ -247,52 +197,51 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
let verbose = ChecksumVerbose::new(status, quiet, warn);
let opts = ChecksumOptions {
binary,
let opts = ChecksumValidateOptions {
ignore_missing,
strict,
verbose,
};
// Execute the checksum validation
return perform_checksum_validation(
input.iter().copied(),
Some(algo.name),
Some(algo.bits),
opts,
);
return perform_checksum_validation(input.iter().copied(), Some(algo_kind), length, opts);
} else if quiet {
return Err(ChecksumError::QuietNotCheck.into());
} else if strict {
return Err(ChecksumError::StrictNotCheck.into());
}
let nonames = *matches
let no_names = *matches
.try_get_one("no-names")
.unwrap_or(None)
.unwrap_or(&false);
let zero = matches.get_flag("zero");
let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero"));
let opts = Options {
algoname: algo.name,
digest: (algo.create_fn)(),
output_bits: algo.bits,
let algo = SizedAlgoKind::from_unsized(algo_kind, length)?;
let opts = ChecksumComputeOptions {
algo_kind: algo,
output_format: figure_out_output_format(
algo,
matches.get_flag(options::TAG),
binary,
/* raw */ false,
/* base64: */ false,
),
line_ending,
binary,
binary_name: &binary_name,
tag: matches.get_flag("tag"),
nonames,
//status,
//quiet,
//warn,
zero,
//ignore_missing,
no_names,
};
let files = matches.get_many::<OsString>(options::FILE).map_or_else(
// No files given, read from stdin.
|| Box::new(iter::once(OsStr::new("-"))) as Box<dyn Iterator<Item = &OsStr>>,
// At least one file given, read from them.
|files| Box::new(files.map(OsStr::new)) as Box<dyn Iterator<Item = &OsStr>>,
);
// Show the hashsum of the input
match matches.get_many::<OsString>(options::FILE) {
Some(files) => hashsum(opts, files.map(|f| f.as_os_str())),
None => hashsum(opts, iter::once(OsStr::new("-"))),
}
perform_checksum_computation(opts, files)
}
mod options {
@@ -523,87 +472,3 @@ fn uu_app(binary_name: &str) -> (Command, bool) {
(command, is_hashsum_bin)
}
#[allow(clippy::cognitive_complexity)]
fn hashsum<'a, I>(mut options: Options, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
let binary_marker = if options.binary { "*" } else { " " };
let mut err_found = None;
for filename in files {
let filename = Path::new(filename);
let mut file = BufReader::with_capacity(
READ_BUFFER_SIZE,
if filename == OsStr::new("-") {
Box::new(stdin()) as Box<dyn Read>
} else {
let file_buf = match File::open(filename) {
Ok(f) => f,
Err(e) => {
eprintln!(
"{}: {}: {}",
options.binary_name,
filename.to_string_lossy(),
strip_errno(&e)
);
err_found = Some(ChecksumError::Io(e));
continue;
}
};
Box::new(file_buf) as Box<dyn Read>
},
);
let sum = match digest_reader(
&mut options.digest,
&mut file,
options.binary,
options.output_bits,
) {
Ok((sum, _)) => sum,
Err(e) => {
eprintln!(
"{}: {}: {}",
options.binary_name,
filename.to_string_lossy(),
strip_errno(&e)
);
err_found = Some(ChecksumError::Io(e));
continue;
}
};
let (escaped_filename, prefix) = escape_filename(filename);
if options.tag {
if options.algoname == "blake2b" {
if options.digest.output_bits() == 512 {
println!("BLAKE2b ({escaped_filename}) = {sum}");
} else {
// special case for BLAKE2b with non-default output length
println!(
"BLAKE2b-{} ({escaped_filename}) = {sum}",
options.digest.output_bits()
);
}
} else {
println!(
"{prefix}{} ({escaped_filename}) = {sum}",
options.algoname.to_ascii_uppercase()
);
}
} else if options.nonames {
println!("{sum}");
} else if options.zero {
// with zero, we don't escape the filename
print!("{sum} {binary_marker}{}\0", filename.display());
} else {
println!("{prefix}{sum} {binary_marker}{escaped_filename}");
}
}
match err_found {
None => Ok(()),
Some(e) => Err(Box::new(e)),
}
}
@@ -0,0 +1,338 @@
// 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 bitlen
use std::ffi::OsStr;
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::error::{FromIo, UResult, USimpleError};
use crate::line_ending::LineEnding;
use crate::{encoding, show, translate};
/// Use the same buffer size as GNU when reading a file to create a checksum
/// from it: 32 KiB.
const READ_BUFFER_SIZE: usize = 32 * 1024;
pub struct ChecksumComputeOptions {
/// Which algorithm to use to compute the digest.
pub algo_kind: SizedAlgoKind,
/// Printing format to use for each checksum.
pub output_format: OutputFormat,
/// Whether to finish lines with '\n' or '\0'.
pub line_ending: LineEnding,
/// On windows, open files as binary instead of text
pub binary: bool,
/// (non-GNU option) Do not print file names
pub no_names: bool,
}
/// Reading mode used to compute digest.
///
/// On most linux systems, this is irrelevant, as there is no distinction
/// between text and binary files. Refer to GNU's cksum documentation for more
/// information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadingMode {
Binary,
Text,
}
impl ReadingMode {
#[inline]
fn as_char(&self) -> char {
match self {
Self::Binary => '*',
Self::Text => ' ',
}
}
}
/// Whether to write the digest as hexadecimal or encoded in base64.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestFormat {
Hexadecimal,
Base64,
}
impl DigestFormat {
#[inline]
fn is_base64(&self) -> bool {
*self == Self::Base64
}
}
/// Holds the representation that shall be used for printing a checksum line
#[derive(Debug, PartialEq, Eq)]
pub enum OutputFormat {
/// Raw digest
Raw,
/// Selected for older algorithms which had their custom formatting
///
/// Default for crc, sysv, bsd
Legacy,
/// `$ALGO_NAME ($FILENAME) = $DIGEST`
Tagged(DigestFormat),
/// '$DIGEST $FLAG$FILENAME'
/// where 'flag' depends on the reading mode
///
/// Default for standalone checksum utilities
Untagged(DigestFormat, ReadingMode),
}
impl OutputFormat {
#[inline]
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;
}
// 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)
}
}
fn print_legacy_checksum(
options: &ChecksumComputeOptions,
filename: &OsStr,
sum: &str,
size: usize,
) -> UResult<()> {
debug_assert!(options.algo_kind.is_legacy());
let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul {
(filename.to_string_lossy().to_string(), "")
} else {
escape_filename(filename)
};
print!("{prefix}");
// Print the sum
match options.algo_kind {
SizedAlgoKind::Sysv => print!(
"{} {}",
sum.parse::<u16>().unwrap(),
size.div_ceil(options.algo_kind.bitlen()),
),
SizedAlgoKind::Bsd => {
// The BSD checksum output is 5 digit integer
let bsd_width = 5;
print!(
"{:0bsd_width$} {:bsd_width$}",
sum.parse::<u16>().unwrap(),
size.div_ceil(options.algo_kind.bitlen()),
);
}
SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => {
print!("{sum} {size}");
}
_ => unreachable!("Not a legacy algorithm"),
}
// Print the filename after a space if not stdin
if escaped_filename != "-" {
print!(" ");
let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes());
}
Ok(())
}
fn print_tagged_checksum(
options: &ChecksumComputeOptions,
filename: &OsStr,
sum: &String,
) -> UResult<()> {
let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul {
(filename.to_string_lossy().to_string(), "")
} else {
escape_filename(filename)
};
// Print algo name and opening parenthesis.
print!("{prefix}{} (", options.algo_kind.to_tag());
// Print filename
let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes());
// Print closing parenthesis and sum
print!(") = {sum}");
Ok(())
}
fn print_untagged_checksum(
options: &ChecksumComputeOptions,
filename: &OsStr,
sum: &String,
reading_mode: ReadingMode,
) -> UResult<()> {
// early check for the "no-names" option
if options.no_names {
print!("{sum}");
return Ok(());
}
let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul {
(filename.to_string_lossy().to_string(), "")
} else {
escape_filename(filename)
};
// Print checksum and reading mode flag
print!("{prefix}{sum} {}", reading_mode.as_char());
// Print filename
let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes());
Ok(())
}
/// Calculate checksum
///
/// # Arguments
///
/// * `options` - CLI options for the assigning checksum algorithm
/// * `files` - A iterator of [`OsStr`] which is a bunch of files that are using for calculating checksum
pub fn perform_checksum_computation<'a, I>(options: ChecksumComputeOptions, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
let mut files = files.peekable();
while let Some(filename) = files.next() {
// Check that in raw mode, we are not provided with several files.
if options.output_format.is_raw() && files.peek().is_some() {
return Err(Box::new(ChecksumError::RawMultipleFiles));
}
let filepath = Path::new(filename);
let stdin_buf;
let file_buf;
if filepath.is_dir() {
show!(USimpleError::new(
1,
// TODO: Rework translation, which is broken since this code moved to uucore
// translate!("cksum-error-is-directory", "file" => filepath.display())
format!("{}: Is a directory", filepath.display())
));
continue;
}
// Handle the file input
let mut file = BufReader::with_capacity(
READ_BUFFER_SIZE,
if filename == "-" {
stdin_buf = io::stdin();
Box::new(stdin_buf) as Box<dyn Read>
} else {
file_buf = match File::open(filepath) {
Ok(file) => file,
Err(err) => {
show!(err.map_err_context(|| filepath.to_string_lossy().into()));
continue;
}
};
Box::new(file_buf) as Box<dyn Read>
},
);
let mut digest = options.algo_kind.create_digest();
let (sum_hex, sz) = digest_reader(
&mut digest,
&mut file,
options.binary,
options.algo_kind.bitlen(),
)
.map_err_context(|| translate!("cksum-error-failed-to-read-input"))?;
// Encodes the sum if df is Base64, leaves as-is otherwise.
let encode_sum = |sum: String, df: DigestFormat| {
if df.is_base64() {
encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap())
} else {
sum
}
};
match options.output_format {
OutputFormat::Raw => {
let bytes = match options.algo_kind {
SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => {
sum_hex.parse::<u32>().unwrap().to_be_bytes().to_vec()
}
SizedAlgoKind::Sysv | SizedAlgoKind::Bsd => {
sum_hex.parse::<u16>().unwrap().to_be_bytes().to_vec()
}
_ => hex::decode(sum_hex).unwrap(),
};
// Cannot handle multiple files anyway, output immediately.
io::stdout().write_all(&bytes)?;
return Ok(());
}
OutputFormat::Legacy => {
print_legacy_checksum(&options, filename, &sum_hex, sz)?;
}
OutputFormat::Tagged(digest_format) => {
print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?;
}
OutputFormat::Untagged(digest_format, reading_mode) => {
print_untagged_checksum(
&options,
filename,
&encode_sum(sum_hex, digest_format),
reading_mode,
)?;
}
}
print!("{}", options.line_ending);
}
Ok(())
}
File diff suppressed because it is too large Load Diff
+25 -8
View File
@@ -774,14 +774,31 @@ fn test_blake2b_length() {
#[test]
fn test_blake2b_length_greater_than_512() {
new_ucmd!()
.arg("--length=1024")
.arg("--algorithm=blake2b")
.arg("lorem_ipsum.txt")
.arg("alice_in_wonderland.txt")
.fails_with_code(1)
.no_stdout()
.stderr_is_fixture("length_larger_than_512.expected");
for l in ["513", "1024", "73786976294838206464"] {
new_ucmd!()
.arg("--algorithm=blake2b")
.arg("--length")
.arg(l)
.arg("lorem_ipsum.txt")
.fails_with_code(1)
.no_stdout()
.stderr_contains(format!("invalid length: '{l}'"))
.stderr_contains("maximum digest length for 'BLAKE2b' is 512 bits");
}
}
#[test]
fn test_blake2b_length_nan() {
for l in ["foo", "512x", "x512", "0xff"] {
new_ucmd!()
.arg("--algorithm=blake2b")
.arg("--length")
.arg(l)
.arg("lorem_ipsum.txt")
.fails_with_code(1)
.no_stdout()
.stderr_contains(format!("invalid length: '{l}'"));
}
}
#[test]
+4 -9
View File
@@ -107,17 +107,12 @@ macro_rules! test_digest {
at.write("a", "file1\n");
at.write("c", "file3\n");
#[cfg(unix)]
let file_not_found_str = "No such file or directory";
#[cfg(not(unix))]
let file_not_found_str = "The system cannot find the file specified";
ts.ucmd()
.args(&[DIGEST_ARG, BITS_ARG, "a", "b", "c"])
.fails()
.stdout_contains("a\n")
.stdout_contains("c\n")
.stderr_contains(format!("b: {file_not_found_str}"));
.stderr_contains("b: No such file or directory");
}
}
)*)
@@ -1097,11 +1092,11 @@ fn test_sha256_stdin_binary() {
);
}
// This test is currently disabled on windows
#[test]
#[cfg_attr(windows, ignore = "Discussion is in #9168")]
fn test_check_sha256_binary() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
new_ucmd!()
.args(&[
"--sha256",
"--bits=256",
-2
View File
@@ -1,2 +0,0 @@
cksum: invalid length: '1024'
cksum: maximum digest length for 'BLAKE2b' is 512 bits