From 3573ee9dffa6408adb620d49d118c8707eb436e2 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Tue, 25 Nov 2025 01:07:38 +0100 Subject: [PATCH 1/6] checksum: Introduce a DigestOutput type... ... to prevent a preemptive computation of the hex encoding. --- .../src/lib/features/checksum/compute.rs | 60 ++++++------- src/uucore/src/lib/features/checksum/mod.rs | 16 +--- .../src/lib/features/checksum/validate.rs | 11 +-- src/uucore/src/lib/features/sum.rs | 89 ++++++++++++++----- 4 files changed, 101 insertions(+), 75 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index e91c54166d7..077905c38c2 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -13,7 +13,8 @@ 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 crate::sum::DigestOutput; +use crate::{show, translate}; /// Use the same buffer size as GNU when reading a file to create a checksum /// from it: 32 KiB. @@ -139,10 +140,11 @@ pub fn figure_out_output_format( fn print_legacy_checksum( options: &ChecksumComputeOptions, filename: &OsStr, - sum: &str, + sum: &DigestOutput, size: usize, ) -> UResult<()> { debug_assert!(options.algo_kind.is_legacy()); + debug_assert!(matches!(sum, DigestOutput::U16(_) | DigestOutput::Crc(_))); let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { (filename.to_string_lossy().to_string(), "") @@ -153,25 +155,23 @@ fn print_legacy_checksum( print!("{prefix}"); // Print the sum - match options.algo_kind { - SizedAlgoKind::Sysv => print!( - "{} {}", - sum.parse::().unwrap(), + match (options.algo_kind, sum) { + (SizedAlgoKind::Sysv, DigestOutput::U16(sum)) => print!( + "{prefix}{sum} {}", size.div_ceil(options.algo_kind.bitlen()), ), - SizedAlgoKind::Bsd => { + (SizedAlgoKind::Bsd, DigestOutput::U16(sum)) => { // The BSD checksum output is 5 digit integer let bsd_width = 5; print!( - "{:0bsd_width$} {:bsd_width$}", - sum.parse::().unwrap(), + "{prefix}{sum:0bsd_width$} {:bsd_width$}", size.div_ceil(options.algo_kind.bitlen()), ); } - SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { - print!("{sum} {size}"); + (SizedAlgoKind::Crc | SizedAlgoKind::Crc32b, DigestOutput::Crc(sum)) => { + print!("{prefix}{sum} {size}"); } - _ => unreachable!("Not a legacy algorithm"), + (algo, output) => unreachable!("Bug: Invalid legacy checksum ({algo:?}, {output:?})"), } // Print the filename after a space if not stdin @@ -284,49 +284,39 @@ where 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"))?; + let (digest_output, sz) = digest_reader(&mut digest, &mut file, options.binary) + .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| { + let encode_sum = |sum: DigestOutput, df: DigestFormat| { if df.is_base64() { - encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap()) + sum.to_base64() } else { - sum + sum.to_hex() } }; match options.output_format { OutputFormat::Raw => { - let bytes = match options.algo_kind { - SizedAlgoKind::Crc | SizedAlgoKind::Crc32b => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - SizedAlgoKind::Sysv | SizedAlgoKind::Bsd => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - _ => hex::decode(sum_hex).unwrap(), - }; // Cannot handle multiple files anyway, output immediately. - io::stdout().write_all(&bytes)?; + digest_output.write_raw(io::stdout())?; return Ok(()); } OutputFormat::Legacy => { - print_legacy_checksum(&options, filename, &sum_hex, sz)?; + print_legacy_checksum(&options, filename, &digest_output, sz)?; } OutputFormat::Tagged(digest_format) => { - print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?; + print_tagged_checksum( + &options, + filename, + &encode_sum(digest_output, digest_format)?, + )?; } OutputFormat::Untagged(digest_format, reading_mode) => { print_untagged_checksum( &options, filename, - &encode_sum(sum_hex, digest_format), + &encode_sum(digest_output, digest_format)?, reading_mode, )?; } diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 87c8836fd7f..5339f833fbe 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -15,8 +15,8 @@ use thiserror::Error; use crate::error::{UError, UResult}; use crate::show_error; use crate::sum::{ - Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, - Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, + Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestOutput, DigestWriter, Md5, Sha1, Sha3_224, + Sha3_256, Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, }; pub mod compute; @@ -420,8 +420,7 @@ pub fn digest_reader( digest: &mut Box, reader: &mut T, binary: bool, - output_bits: usize, -) -> io::Result<(String, usize)> { +) -> io::Result<(DigestOutput, usize)> { digest.reset(); // Read bytes from `reader` and write those bytes to `digest`. @@ -440,14 +439,7 @@ pub fn digest_reader( let output_size = std::io::copy(reader, &mut digest_writer)? as usize; digest_writer.finalize(); - if digest.output_bits() > 0 { - Ok((digest.result_str(), output_size)) - } else { - // Assume it's SHAKE. result_str() doesn't work with shake (as of 8/30/2016) - let mut bytes = vec![0; output_bits.div_ceil(8)]; - digest.hash_finalize(&mut bytes); - Ok((hex::encode(bytes), output_size)) - } + Ok((digest.result(), output_size)) } /// Calculates the length of the digest. diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 1869d91bfe9..06bfd6634de 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -660,16 +660,11 @@ fn compute_and_check_digest_from_file( // TODO: improve function signature to use ReadingMode instead of binary bool // Set binary to false because --binary is not supported with --check - let (calculated_checksum, _) = digest_reader( - &mut digest, - &mut file_reader, - /* binary */ false, - algo.bitlen(), - ) - .unwrap(); + let (calculated_checksum, _) = + digest_reader(&mut digest, &mut file_reader, /* binary */ false).unwrap(); // Do the checksum validation - let checksum_correct = expected_checksum == calculated_checksum; + let checksum_correct = expected_checksum == calculated_checksum.to_hex()?; print_file_report( std::io::stdout(), filename, diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index e517a03fcc0..eb40a0e9c88 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -12,12 +12,52 @@ //! [`DigestWriter`] struct provides a wrapper around [`Digest`] that //! implements the [`Write`] trait, for use in situations where calling //! [`write`] would be useful. -use std::io::Write; -use hex::encode; +use std::io::{self, Write}; + +use data_encoding::BASE64; + #[cfg(windows)] use memchr::memmem; +use crate::error::{UResult, USimpleError}; + +/// Represents the output of a checksum computation. +#[derive(Debug)] +pub enum DigestOutput { + /// Varying-size output + Vec(Vec), + /// Legacy output for Crc and Crc32B modes + Crc(u32), + /// Legacy output for Sysv and BSD modes + U16(u16), +} + +impl DigestOutput { + pub fn write_raw(&self, mut w: impl std::io::Write) -> io::Result<()> { + match self { + Self::Vec(buf) => w.write_all(buf), + // For legacy outputs, print them in big endian + Self::Crc(n) => w.write_all(&n.to_be_bytes()), + Self::U16(n) => w.write_all(&n.to_be_bytes()), + } + } + + pub fn to_hex(&self) -> UResult { + match self { + Self::Vec(buf) => Ok(hex::encode(buf)), + _ => Err(USimpleError::new(1, "Legacy output cannot be encoded")), + } + } + + pub fn to_base64(&self) -> UResult { + match self { + Self::Vec(buf) => Ok(BASE64.encode(buf)), + _ => Err(USimpleError::new(1, "Legacy output cannot be encoded")), + } + } +} + pub trait Digest { fn new() -> Self where @@ -29,10 +69,11 @@ pub trait Digest { fn output_bytes(&self) -> usize { self.output_bits().div_ceil(8) } - fn result_str(&mut self) -> String { + + fn result(&mut self) -> DigestOutput { let mut buf: Vec = vec![0; self.output_bytes()]; self.hash_finalize(&mut buf); - encode(buf) + DigestOutput::Vec(buf) } } @@ -167,10 +208,12 @@ impl Digest for Crc { out.copy_from_slice(&self.digest.finalize().to_ne_bytes()); } - fn result_str(&mut self) -> String { + fn result(&mut self) -> DigestOutput { let mut out: [u8; 8] = [0; 8]; self.hash_finalize(&mut out); - u64::from_ne_bytes(out).to_string() + + let x = u64::from_ne_bytes(out); + DigestOutput::Crc((x & (u32::MAX as u64)) as u32) } fn reset(&mut self) { @@ -214,10 +257,10 @@ impl Digest for CRC32B { 32 } - fn result_str(&mut self) -> String { + fn result(&mut self) -> DigestOutput { let mut out = [0; 4]; self.hash_finalize(&mut out); - format!("{}", u32::from_be_bytes(out)) + DigestOutput::Crc(u32::from_be_bytes(out)) } } @@ -240,10 +283,10 @@ impl Digest for Bsd { out.copy_from_slice(&self.state.to_ne_bytes()); } - fn result_str(&mut self) -> String { - let mut _out: Vec = vec![0; 2]; + fn result(&mut self) -> DigestOutput { + let mut _out = [0; 2]; self.hash_finalize(&mut _out); - format!("{}", self.state) + DigestOutput::U16(self.state) } fn reset(&mut self) { @@ -275,10 +318,10 @@ impl Digest for SysV { out.copy_from_slice(&(self.state as u16).to_ne_bytes()); } - fn result_str(&mut self) -> String { - let mut _out: Vec = vec![0; 2]; + fn result(&mut self) -> DigestOutput { + let mut _out = [0; 2]; self.hash_finalize(&mut _out); - format!("{}", self.state) + DigestOutput::U16((self.state & (u16::MAX as u32)) as u16) } fn reset(&mut self) { @@ -319,7 +362,7 @@ macro_rules! impl_digest_common { // Implements the Digest trait for sha2 / sha3 algorithms with variable output macro_rules! impl_digest_shake { - ($algo_type: ty) => { + ($algo_type: ty, $output_bits: literal) => { impl Digest for $algo_type { fn new() -> Self { Self(Default::default()) @@ -338,7 +381,13 @@ macro_rules! impl_digest_shake { } fn output_bits(&self) -> usize { - 0 + $output_bits + } + + fn result(&mut self) -> DigestOutput { + let mut bytes = vec![0; self.output_bits().div_ceil(8)]; + self.hash_finalize(&mut bytes); + DigestOutput::Vec(bytes) } } }; @@ -368,8 +417,8 @@ impl_digest_common!(Sha3_512, 512); pub struct Shake128(sha3::Shake128); pub struct Shake256(sha3::Shake256); -impl_digest_shake!(Shake128); -impl_digest_shake!(Shake256); +impl_digest_shake!(Shake128, 256); +impl_digest_shake!(Shake256, 512); /// A struct that writes to a digest. /// @@ -501,14 +550,14 @@ mod tests { writer_crlf.write_all(b"\r").unwrap(); writer_crlf.write_all(b"\n").unwrap(); writer_crlf.finalize(); - let result_crlf = digest.result_str(); + let result_crlf = digest.result(); // We expect "\r\n" to be replaced with "\n" in text mode on Windows. let mut digest = Box::new(Md5::new()) as Box; let mut writer_lf = DigestWriter::new(&mut digest, false); writer_lf.write_all(b"\n").unwrap(); writer_lf.finalize(); - let result_lf = digest.result_str(); + let result_lf = digest.result(); assert_eq!(result_crlf, result_lf); } From bdc9ee797ebcbf0de285890f4ec05af1be71d74a Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Fri, 28 Nov 2025 17:08:07 +0100 Subject: [PATCH 2/6] checksum(validate): Simplify and optimize LineFormat::validate_checksum_format --- .../src/lib/features/checksum/validate.rs | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 06bfd6634de..d5f962413f0 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -371,19 +371,38 @@ impl LineFormat { return None; } - let mut parts = checksum.splitn(2, |&b| b == b'='); - let main = parts.next().unwrap(); // Always exists since checksum isn't empty - let padding = parts.next().unwrap_or_default(); // Empty if no '=' - - if main.is_empty() - || !main - .iter() - .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') - { - return None; + let mut is_base64 = false; + let mut index = 0; + + while index < checksum.len() { + match checksum[index..] { + // ASCII alphanumeric + [b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9', ..] => { + index += 1; + continue; + } + [b'+' | b'/', ..] => { + is_base64 = true; + index += 1; + continue; + } + [b'='] | [b'=', b'='] | [b'=', b'=', b'='] => { + is_base64 = true; + break; + } + // Any other character means the checksum is wrong + _ => return None, + } } - if padding.len() > 2 || padding.iter().any(|&b| b != b'=') { + // If base64 characters were encountered, make sure the checksum has a + // length multiple of 4. + // + // This check is not enough because it may allow base64-encoded + // checksums that are fully alphanumeric. Another check happens later + // when we are provided with a length hint to detect ambiguous + // base64-encoded checksums. + if is_base64 && checksum.len() % 4 != 0 { return None; } @@ -1174,11 +1193,9 @@ mod tests { #[test] fn test_get_expected_digest() { - let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="); - let mut cached_line_format = None; - let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); + let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=".to_owned(); - let result = get_expected_digest_as_hex_string(&line_info.checksum, None); + let result = get_expected_digest_as_hex_string(&ck, None); assert_eq!( result.unwrap(), @@ -1189,11 +1206,9 @@ mod tests { #[test] fn test_get_expected_checksum_invalid() { // The line misses a '=' at the end to be valid base64 - let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"); - let mut cached_line_format = None; - let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); + let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU".to_owned(); - let result = get_expected_digest_as_hex_string(&line_info.checksum, None); + let result = get_expected_digest_as_hex_string(&ck, None); assert!(result.is_none()); } From b0ac363c726d76b931338e2b396972de659dc4ac Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Sat, 29 Nov 2025 03:37:45 +0100 Subject: [PATCH 3/6] checksum(validate): Check calculated checksum against raw expected to avoid decoding base64 and directly re-encoding it in hexadecimal --- .../src/lib/features/checksum/validate.rs | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index d5f962413f0..63882f7cfe2 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -5,7 +5,6 @@ // spell-checker:ignore rsplit hexdigit bitlen invalidchecksum inva idchecksum xffname -use std::borrow::Cow; use std::ffi::OsStr; use std::fmt::Display; use std::fs::File; @@ -16,6 +15,7 @@ use os_display::Quotable; use crate::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, digest_reader, unescape_filename}; use crate::error::{FromIo, UError, UResult, USimpleError}; use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; +use crate::sum::DigestOutput; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, util_name, @@ -483,47 +483,45 @@ fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { .to_string() } -/// Extract the expected digest from the checksum string -fn get_expected_digest_as_hex_string( - checksum: &String, - byte_len_hint: Option, -) -> Option> { +/// Extract the expected digest from the checksum string and decode it +fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { + // If the length of the digest is not a multiple of 2, then it must be + // improperly formatted (1 byte is 2 hex digits, and base64 strings should + // always be a multiple of 4). if checksum.len() % 2 != 0 { - // If the length of the digest is not a multiple of 2, then it - // must be improperly formatted (1 hex digit is 2 characters) return None; } let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len); - // If the digest can be decoded as hexadecimal AND its byte length matches - // the one expected (in case it's given), just go with it. - if checksum.as_bytes().iter().all(u8::is_ascii_hexdigit) && checks_hint(checksum.len() / 2) { - return Some(checksum.as_str().into()); + // If the length of the string matches the one to be expected (in case it's + // given) AND the digest can be decoded as hexadecimal, just go with it. + if checks_hint(checksum.len() / 2) { + if let Ok(raw_ck) = hex::decode(checksum) { + return Some(raw_ck); + } } - // If hexadecimal digest fails for any reason, interpret the digest as base - // 64. + // If the checksum cannot be decoded as hexadecimal, interpret it as Base64 + // instead. // But first, verify the encoded checksum length, which should be a // multiple of 4. + // + // It is important to check it before trying to decode, because the + // forgiving mode of decoding will ignore if padding characters '=' are + // MISSING, but to match GNU's behavior, we must reject it. if checksum.len() % 4 != 0 { return None; } // Perform the decoding and be FORGIVING about it, to allow for checksums - // with invalid padding to still be decoded. This is enforced by + // with INVALID padding to still be decoded. This is enforced by // `test_untagged_base64_matching_tag` in `test_cksum.rs` - // - // TODO: Ideally, we should not re-encode the result in hexadecimal, to avoid - // un-necessary computation. - - match base64_simd::forgiving_decode_to_vec(checksum.as_bytes()) { - Ok(buffer) if checks_hint(buffer.len()) => Some(hex::encode(buffer).into()), - // The resulting length is not as expected - Ok(_) => None, - Err(_) => None, - } + + base64_simd::forgiving_decode_to_vec(checksum.as_bytes()) + .ok() + .filter(|raw| checks_hint(raw.len())) } /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". @@ -663,7 +661,7 @@ fn identify_algo_name_and_length( /// the expected one. fn compute_and_check_digest_from_file( filename: &[u8], - expected_checksum: &str, + expected_checksum: &[u8], algo: SizedAlgoKind, opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { @@ -683,7 +681,11 @@ fn compute_and_check_digest_from_file( digest_reader(&mut digest, &mut file_reader, /* binary */ false).unwrap(); // Do the checksum validation - let checksum_correct = expected_checksum == calculated_checksum.to_hex()?; + let checksum_correct = match calculated_checksum { + DigestOutput::Vec(data) => data == expected_checksum, + DigestOutput::Crc(n) => n.to_be_bytes() == expected_checksum, + DigestOutput::U16(n) => n.to_be_bytes() == expected_checksum, + }; print_file_report( std::io::stdout(), filename, @@ -718,9 +720,8 @@ fn process_algo_based_line( _ => None, }; - let expected_checksum = - get_expected_digest_as_hex_string(&line_info.checksum, digest_char_length_hint) - .ok_or(LineCheckError::ImproperlyFormatted)?; + let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_char_length_hint) + .ok_or(LineCheckError::ImproperlyFormatted)?; let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; @@ -743,7 +744,7 @@ fn process_non_algo_based_line( // Remove the leading asterisk if present - only for the first line filename_to_check = &filename_to_check[1..]; } - let expected_checksum = get_expected_digest_as_hex_string(&line_info.checksum, None) + let expected_checksum = get_raw_expected_digest(&line_info.checksum, None) .ok_or(LineCheckError::ImproperlyFormatted)?; // When a specific algorithm name is input, use it and use the provided @@ -754,11 +755,11 @@ fn process_non_algo_based_line( // division by 2 converts the length of the Blake2b checksum from // hexadecimal characters to bytes, as each byte is represented by // two hexadecimal characters. - (AlgoKind::Blake2b, Some(expected_checksum.len() / 2)) + (AlgoKind::Blake2b, Some(expected_checksum.len())) } algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { // multiplication by 4 to get the number of bits - (algo, Some(expected_checksum.len() * 4)) + (algo, Some(expected_checksum.len() * 8)) } _ => (cli_algo_kind, cli_algo_length), }; @@ -1195,11 +1196,12 @@ mod tests { fn test_get_expected_digest() { let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=".to_owned(); - let result = get_expected_digest_as_hex_string(&ck, None); + let result = get_raw_expected_digest(&ck, None); assert_eq!( result.unwrap(), - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + hex::decode(b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + .unwrap() ); } @@ -1208,7 +1210,7 @@ mod tests { // The line misses a '=' at the end to be valid base64 let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU".to_owned(); - let result = get_expected_digest_as_hex_string(&ck, None); + let result = get_raw_expected_digest(&ck, None); assert!(result.is_none()); } From 862d59a2e62956ac5c80c7aa3cbc5c2f1a3a4419 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 1 Dec 2025 02:13:08 +0100 Subject: [PATCH 4/6] test(cksum): Add test for ignore-missing standard input --- tests/by-util/test_cksum.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 4b39627b7de..9971ee469e5 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2755,6 +2755,21 @@ mod gnu_cksum_c { .stderr_contains("CHECKSUMS-missing: no file was verified"); } + #[test] + fn test_ignore_missing_stdin() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--ignore-missing") + .arg("--check") + .pipe_in_fixture("CHECKSUMS-missing") + .fails() + .stdout_does_not_contain("nonexistent: No such file or directory") + .stdout_does_not_contain("nonexistent: FAILED open or read") + .stderr_contains("'standard input': no file was verified"); + } + #[test] fn test_status_and_warn() { let scene = make_scene_with_checksum_missing(); From a24dc4786f7403ceed61ba5dc211c4b520af6cf1 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 1 Dec 2025 01:58:19 +0100 Subject: [PATCH 5/6] checksum(validate): Remove called-once simple functions, fix standard-input filename print --- .../src/lib/features/checksum/validate.rs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 63882f7cfe2..43a42bf10aa 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -170,10 +170,16 @@ fn print_cksum_report(res: &ChecksumResult) { /// Print a "no properly formatted lines" message in stderr #[inline] -fn log_no_properly_formatted(filename: String) { +fn log_no_properly_formatted(filename: impl Display) { show_error!("{filename}: no properly formatted checksum lines found"); } +/// Print a "no file was verified" message in stderr +#[inline] +fn log_no_file_verified(filename: impl Display) { + show_error!("{filename}: no file was verified"); +} + /// Represents the different outcomes that can happen to a file /// that is being checked. #[derive(Debug, Clone, Copy)] @@ -473,16 +479,6 @@ impl LineInfo { } } -fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { - if input_is_stdin { - "standard input" - } else { - filename.to_str().unwrap() - } - .maybe_quote() - .to_string() -} - /// Extract the expected digest from the checksum string and decode it fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { // If the length of the digest is not a multiple of 2, then it must be @@ -893,11 +889,19 @@ fn process_checksum_file( } } + let filename_display = || { + if input_is_stdin { + "standard input".maybe_quote() + } else { + filename_input.maybe_quote() + } + }; + // not a single line correctly formatted found // return an error if res.total_properly_formatted() == 0 { if opts.verbose.over_status() { - log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); + log_no_properly_formatted(filename_display()); } return Err(FileCheckError::Failed); } @@ -911,11 +915,7 @@ fn process_checksum_file( // we have only bad format // and we had ignore-missing if opts.verbose.over_status() { - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); + log_no_file_verified(filename_display()); } return Err(FileCheckError::Failed); } From dc8063d56cf21b715d0ff3cb28bcc3ad1940e314 Mon Sep 17 00:00:00 2001 From: Dorian Peron Date: Mon, 1 Dec 2025 02:59:12 +0100 Subject: [PATCH 6/6] l10n(uucore::checksum): Implement l10n for English + French --- src/uu/cksum/locales/en-US.ftl | 4 -- src/uu/cksum/locales/fr-FR.ftl | 4 -- src/uucore/locales/en-US.ftl | 19 ++++++ src/uucore/locales/fr-FR.ftl | 19 ++++++ .../src/lib/features/checksum/compute.rs | 6 +- .../src/lib/features/checksum/validate.rs | 58 +++++++++++-------- 6 files changed, 73 insertions(+), 37 deletions(-) diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl index 4a49caebd06..834cd77b0ef 100644 --- a/src/uu/cksum/locales/en-US.ftl +++ b/src/uu/cksum/locales/en-US.ftl @@ -28,7 +28,3 @@ 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 - -# Error messages -cksum-error-is-directory = { $file }: Is a directory -cksum-error-failed-to-read-input = failed to read input diff --git a/src/uu/cksum/locales/fr-FR.ftl b/src/uu/cksum/locales/fr-FR.ftl index 686584696e0..01136f606f9 100644 --- a/src/uu/cksum/locales/fr-FR.ftl +++ b/src/uu/cksum/locales/fr-FR.ftl @@ -28,7 +28,3 @@ 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 - -# Messages d'erreur -cksum-error-is-directory = { $file } : Est un répertoire -cksum-error-failed-to-read-input = échec de la lecture de l'entrée diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 09fb457832c..384e4a83de9 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -29,6 +29,7 @@ error-io = I/O error error-permission-denied = Permission denied error-file-not-found = No such file or directory error-invalid-argument = Invalid argument +error-is-a-directory = { $file }: Is a directory # Common actions action-copying = copying @@ -54,3 +55,21 @@ safe-traversal-error-unlink-failed = failed to unlink '{ $path }': { $source } safe-traversal-error-invalid-fd = invalid file descriptor safe-traversal-current-directory = safe-traversal-directory = + +# checksum-related messages +checksum-no-properly-formatted = { $checksum_file }: no properly formatted checksum lines found +checksum-no-file-verified = { $checksum_file }: no file was verified +checksum-error-failed-to-read-input = failed to read input +checksum-bad-format = { $count -> + [1] { $count } line is improperly formatted + *[other] { $count } lines are improperly formatted +} +checksum-failed-cksum = { $count -> + [1] { $count } computed checksum did NOT match + *[other] { $count } computed checksums did NOT match +} +checksum-failed-open-file = { $count -> + [1] { $count } listed file could not be read + *[other] { $count } listed files could not be read +} +checksum-error-algo-bad-format = { $file }: { $line }: improperly formatted { $algo } checksum line diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index a8a34468865..4c844e9b122 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -29,6 +29,7 @@ error-io = Erreur E/S error-permission-denied = Permission refusée error-file-not-found = Aucun fichier ou répertoire de ce type error-invalid-argument = Argument invalide +error-is-a-directory = { $file }: Est un répertoire # Actions communes action-copying = copie @@ -54,3 +55,21 @@ safe-traversal-error-unlink-failed = échec de la suppression de '{ $path }' : { safe-traversal-error-invalid-fd = descripteur de fichier invalide safe-traversal-current-directory = safe-traversal-directory = + +# Messages relatifs au module checksum +checksum-no-properly-formatted = { $checksum_file }: aucune ligne correctement formattée n'a été trouvée +checksum-no-file-verified = { $checksum_file }: aucun fichier n'a été vérifié +checksum-error-failed-to-read-input = échec de la lecture de l'entrée +checksum-bad-format = { $count -> + [1] { $count } ligne invalide + *[other] { $count } lignes invalides +} +checksum-failed-cksum = { $count -> + [1] { $count } somme de hachage ne correspond PAS + *[other] { $count } sommes de hachage ne correspondent PAS +} +checksum-failed-open-file = { $count -> + [1] { $count } fichier passé n'a pas pu être lu + *[other] { $count } fichiers passés n'ont pas pu être lu +} +checksum-error-algo-bad-format = { $file }: { $line }: ligne invalide pour { $algo } diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs index 077905c38c2..5c57b036df9 100644 --- a/src/uucore/src/lib/features/checksum/compute.rs +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -257,9 +257,7 @@ where 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()) + translate!("error-is-a-directory", "file" => filepath.display()) )); continue; } @@ -285,7 +283,7 @@ where let mut digest = options.algo_kind.create_digest(); let (digest_output, sz) = digest_reader(&mut digest, &mut file, options.binary) - .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; + .map_err_context(|| translate!("checksum-error-failed-to-read-input"))?; // Encodes the sum if df is Base64, leaves as-is otherwise. let encode_sum = |sum: DigestOutput, df: DigestFormat| { diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 43a42bf10aa..48dabe507d3 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -18,7 +18,7 @@ use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; use crate::sum::DigestOutput; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, - util_name, + translate, }; /// To what level should checksum validation print logging info. @@ -149,35 +149,44 @@ impl From for FileCheckError { #[allow(clippy::comparison_chain)] fn print_cksum_report(res: &ChecksumResult) { - if res.bad_format == 1 { - show_warning_caps!("{} line is improperly formatted", res.bad_format); - } else if res.bad_format > 1 { - show_warning_caps!("{} lines are improperly formatted", res.bad_format); + if res.bad_format > 0 { + show_warning_caps!( + "{}", + translate!("checksum-bad-format", "count" => res.bad_format) + ); } - if res.failed_cksum == 1 { - show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); - } else if res.failed_cksum > 1 { - show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); + if res.failed_cksum > 0 { + show_warning_caps!( + "{}", + translate!("checksum-failed-cksum", "count" => res.failed_cksum) + ); } - if res.failed_open_file == 1 { - show_warning_caps!("{} listed file could not be read", res.failed_open_file); - } else if res.failed_open_file > 1 { - show_warning_caps!("{} listed files could not be read", res.failed_open_file); + if res.failed_open_file > 0 { + show_warning_caps!( + "{}", + translate!("checksum-failed-open-file", "count" => res.failed_open_file) + ); } } /// Print a "no properly formatted lines" message in stderr #[inline] fn log_no_properly_formatted(filename: impl Display) { - show_error!("{filename}: no properly formatted checksum lines found"); + show_error!( + "{}", + translate!("checksum-no-properly-formatted", "checksum_file" => filename) + ); } /// Print a "no file was verified" message in stderr #[inline] fn log_no_file_verified(filename: impl Display) { - show_error!("{filename}: no file was verified"); + show_error!( + "{}", + translate!("checksum-no-file-verified", "checksum_file" => filename) + ); } /// Represents the different outcomes that can happen to a file @@ -582,17 +591,18 @@ fn get_input_file(filename: &OsStr) -> UResult> { match File::open(filename) { Ok(f) => { if f.metadata()?.is_dir() { - Err( - io::Error::other(format!("{}: Is a directory", filename.to_string_lossy())) - .into(), + Err(io::Error::other( + translate!("error-is-a-directory", "file" => filename.to_string_lossy()), ) + .into()) } else { Ok(Box::new(f)) } } Err(_) => Err(io::Error::other(format!( - "{}: No such file or directory", - filename.to_string_lossy() + "{}: {}", + filename.to_string_lossy(), + translate!("error-file-not-found") )) .into()), } @@ -875,11 +885,9 @@ fn process_checksum_file( } else { "Unknown algorithm" }; - eprintln!( - "{}: {}: {}: improperly formatted {algo} checksum line", - util_name(), - filename_input.maybe_quote(), - i + 1, + show_error!( + "{}", + translate!("checksum-error-algo-bad-format", "file" => filename_input.maybe_quote(), "line" => i + 1, "algo" => algo) ); } }