Merge pull request #9532 from RenjiSann/cksum-improvements
Some checks are pending
CICD / Style/cargo-deny (push) Waiting to run
CICD / Dependencies (push) Waiting to run
CICD / Style/deps (push) Waiting to run
CICD / Documentation/warnings (push) Waiting to run
CICD / MinRustV (push) Waiting to run
CICD / Build/Makefile (push) Blocked by required conditions
CICD / Build/stable (push) Blocked by required conditions
CICD / Build/nightly (push) Blocked by required conditions
CICD / Binary sizes (push) Blocked by required conditions
CICD / Build (push) Blocked by required conditions
CICD / Tests/BusyBox test suite (push) Blocked by required conditions
CICD / Tests/Toybox test suite (push) Blocked by required conditions
CICD / Code Coverage (push) Waiting to run
CICD / Separate Builds (push) Waiting to run
CICD / Test all features separately (push) Blocked by required conditions
CICD / Build/SELinux (push) Blocked by required conditions
CICD / Build/SELinux-Stubs (Non-Linux) (push) Blocked by required conditions
CICD / Safe Traversal Security Check (push) Blocked by required conditions
GnuTests / Run GNU tests (native) (push) Waiting to run
GnuTests / Run GNU tests (SELinux) (push) Waiting to run
GnuTests / Aggregate GNU test results (push) Blocked by required conditions
Android / Test builds (push) Waiting to run
Benchmarks / Run benchmarks (CodSpeed) (push) Waiting to run
Code Quality / Style/lint (push) Waiting to run
Devcontainer / Verify devcontainer (push) Waiting to run
Check uudoc Documentation Generation / Verify uudoc generates correct documentation (push) Waiting to run
FreeBSD / Style and Lint (push) Waiting to run
FreeBSD / Tests (push) Waiting to run
WSL2 / Test (push) Waiting to run
Code Quality / Style/Python (push) Waiting to run
Code Quality / Style/format (push) Waiting to run
Code Quality / Style/spelling (push) Waiting to run
Code Quality / Style/toml (push) Waiting to run
Code Quality / Pre-commit hooks (push) Waiting to run
OpenBSD / Style and Lint (push) Waiting to run
OpenBSD / Tests (push) Waiting to run

cksum: small improvements, l10n
This commit is contained in:
Daniel Hofstetter 2025-12-05 09:18:11 +01:00 committed by GitHub
commit 4c4c59fdd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 268 additions and 188 deletions

View file

@ -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

View file

@ -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

View file

@ -123,7 +123,7 @@ default = []
# * non-default features
backup-control = []
colors = []
checksum = ["data-encoding", "quoting-style", "sum"]
checksum = ["quoting-style", "sum", "base64-simd"]
encoding = ["data-encoding", "data-encoding-macro", "z85", "base64-simd"]
entries = ["libc"]
extendedbigdecimal = ["bigdecimal", "num-traits"]
@ -175,6 +175,7 @@ sum = [
"blake3",
"sm3",
"crc-fast",
"data-encoding",
]
update-control = ["parser"]
utf8 = []

View file

@ -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 = <current directory>
safe-traversal-directory = <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

View file

@ -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 = <répertoire courant>
safe-traversal-directory = <répertoire>
# 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 }

View file

@ -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(), "")
@ -150,28 +152,24 @@ fn print_legacy_checksum(
escape_filename(filename)
};
print!("{prefix}");
// Print the sum
match options.algo_kind {
SizedAlgoKind::Sysv => print!(
"{} {}",
sum.parse::<u16>().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::<u16>().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
@ -257,9 +255,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;
}
@ -284,49 +280,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!("checksum-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::<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)?;
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,
)?;
}

View file

@ -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<T: Read>(
digest: &mut Box<dyn Digest>,
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<T: Read>(
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.

View file

@ -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,9 +15,10 @@ 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,
translate,
};
/// To what level should checksum validation print logging info.
@ -147,31 +147,45 @@ impl From<ChecksumError> 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: String) {
show_error!("{filename}: no properly formatted checksum lines found");
fn log_no_properly_formatted(filename: impl Display) {
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!(
"{}",
translate!("checksum-no-file-verified", "checksum_file" => filename)
);
}
/// Represents the different outcomes that can happen to a file
@ -371,19 +385,32 @@ 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 '='
let mut is_base64 = false;
if main.is_empty()
|| !main
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
{
return None;
for index in 0..checksum.len() {
match checksum[index..] {
// ASCII alphanumeric
[b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9', ..] => (),
// Base64 special character
[b'+' | b'/', ..] => is_base64 = true,
// Base64 end of string padding
[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;
}
@ -454,57 +481,45 @@ 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
fn get_expected_digest_as_hex_string(
checksum: &String,
byte_len_hint: Option<usize>,
) -> Option<Cow<'_, str>> {
/// Extract the expected digest from the checksum string and decode it
fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option<usize>) -> Option<Vec<u8>> {
// 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 "-".
@ -569,17 +584,18 @@ fn get_input_file(filename: &OsStr) -> UResult<Box<dyn Read>> {
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()),
}
@ -644,7 +660,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> {
@ -660,16 +676,15 @@ 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 = 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,
@ -704,9 +719,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)?;
@ -729,22 +743,17 @@ 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
// bits except when dealing with blake2b, sha2 and sha3, where we will
// detect the length.
let (algo_kind, algo_byte_len) = match cli_algo_kind {
AlgoKind::Blake2b => {
// 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 => (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))
// multiplication by 8 to get the number of bits
(algo, Some(expected_checksum.len() * 8))
}
_ => (cli_algo_kind, cli_algo_length),
};
@ -864,11 +873,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)
);
}
}
@ -878,11 +885,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);
}
@ -896,11 +911,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);
}
@ -1179,26 +1190,23 @@ 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_raw_expected_digest(&ck, None);
assert_eq!(
result.unwrap(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
hex::decode(b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
.unwrap()
);
}
#[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_raw_expected_digest(&ck, None);
assert!(result.is_none());
}

View file

@ -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<u8>),
/// 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<String> {
match self {
Self::Vec(buf) => Ok(hex::encode(buf)),
_ => Err(USimpleError::new(1, "Legacy output cannot be encoded")),
}
}
pub fn to_base64(&self) -> UResult<String> {
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<u8> = 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<u8> = 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<u8> = 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) {
@ -292,7 +335,7 @@ impl Digest for SysV {
// Implements the Digest trait for sha2 / sha3 algorithms with fixed output
macro_rules! impl_digest_common {
($algo_type: ty, $size: expr) => {
($algo_type: ty, $size: literal) => {
impl Digest for $algo_type {
fn new() -> Self {
Self(Default::default())
@ -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<dyn Digest>;
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);
}

View file

@ -2755,6 +2755,20 @@ 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()
.no_stdout()
.stderr_contains("'standard input': no file was verified");
}
#[test]
fn test_status_and_warn() {
let scene = make_scene_with_checksum_missing();