mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
Merge pull request #6822 from RenjiSann/cksum-big-rework
checksum: prepare further behavior fix with a rework
This commit is contained in:
commit
83c7cbf0d0
4 changed files with 621 additions and 222 deletions
|
@ -13,8 +13,8 @@ use std::iter;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use uucore::checksum::{
|
use uucore::checksum::{
|
||||||
calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation,
|
calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation,
|
||||||
ChecksumError, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC,
|
ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD,
|
||||||
ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS,
|
ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS,
|
||||||
};
|
};
|
||||||
use uucore::{
|
use uucore::{
|
||||||
encoding,
|
encoding,
|
||||||
|
@ -318,17 +318,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|
||||||
|files| files.map(OsStr::new).collect::<Vec<_>>(),
|
|files| files.map(OsStr::new).collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
return perform_checksum_validation(
|
let opts = ChecksumOptions {
|
||||||
files.iter().copied(),
|
binary: binary_flag,
|
||||||
strict,
|
|
||||||
status,
|
|
||||||
warn,
|
|
||||||
binary_flag,
|
|
||||||
ignore_missing,
|
ignore_missing,
|
||||||
quiet,
|
quiet,
|
||||||
algo_option,
|
status,
|
||||||
length,
|
strict,
|
||||||
);
|
warn,
|
||||||
|
};
|
||||||
|
|
||||||
|
return perform_checksum_validation(files.iter().copied(), algo_option, length, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (tag, asterisk) = handle_tag_text_binary_flags(&matches)?;
|
let (tag, asterisk) = handle_tag_text_binary_flags(&matches)?;
|
||||||
|
|
|
@ -23,6 +23,7 @@ use uucore::checksum::digest_reader;
|
||||||
use uucore::checksum::escape_filename;
|
use uucore::checksum::escape_filename;
|
||||||
use uucore::checksum::perform_checksum_validation;
|
use uucore::checksum::perform_checksum_validation;
|
||||||
use uucore::checksum::ChecksumError;
|
use uucore::checksum::ChecksumError;
|
||||||
|
use uucore::checksum::ChecksumOptions;
|
||||||
use uucore::checksum::HashAlgorithm;
|
use uucore::checksum::HashAlgorithm;
|
||||||
use uucore::error::{FromIo, UResult};
|
use uucore::error::{FromIo, UResult};
|
||||||
use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256};
|
use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256};
|
||||||
|
@ -239,18 +240,21 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
|
||||||
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|
||||||
|files| files.map(OsStr::new).collect::<Vec<_>>(),
|
|files| files.map(OsStr::new).collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
let opts = ChecksumOptions {
|
||||||
|
binary,
|
||||||
|
ignore_missing,
|
||||||
|
quiet,
|
||||||
|
status,
|
||||||
|
strict,
|
||||||
|
warn,
|
||||||
|
};
|
||||||
|
|
||||||
// Execute the checksum validation
|
// Execute the checksum validation
|
||||||
return perform_checksum_validation(
|
return perform_checksum_validation(
|
||||||
input.iter().copied(),
|
input.iter().copied(),
|
||||||
strict,
|
|
||||||
status,
|
|
||||||
warn,
|
|
||||||
binary,
|
|
||||||
ignore_missing,
|
|
||||||
quiet,
|
|
||||||
Some(algo.name),
|
Some(algo.name),
|
||||||
Some(algo.bits),
|
Some(algo.bits),
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
} else if quiet {
|
} else if quiet {
|
||||||
return Err(ChecksumError::QuietNotCheck.into());
|
return Err(ChecksumError::QuietNotCheck.into());
|
||||||
|
|
|
@ -75,6 +75,71 @@ struct ChecksumResult {
|
||||||
pub failed_open_file: i32,
|
pub failed_open_file: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a reason for which the processing of a checksum line
|
||||||
|
/// could not proceed to digest comparison.
|
||||||
|
enum LineCheckError {
|
||||||
|
/// a generic UError was encountered in sub-functions
|
||||||
|
UError(Box<dyn UError>),
|
||||||
|
/// the computed checksum digest differs from the expected one
|
||||||
|
DigestMismatch,
|
||||||
|
/// the line is empty or is a comment
|
||||||
|
Skipped,
|
||||||
|
/// the line has a formatting error
|
||||||
|
ImproperlyFormatted,
|
||||||
|
/// file exists but is impossible to read
|
||||||
|
CantOpenFile,
|
||||||
|
/// there is nothing at the given path
|
||||||
|
FileNotFound,
|
||||||
|
/// the given path leads to a directory
|
||||||
|
FileIsDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Box<dyn UError>> for LineCheckError {
|
||||||
|
fn from(value: Box<dyn UError>) -> Self {
|
||||||
|
Self::UError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChecksumError> for LineCheckError {
|
||||||
|
fn from(value: ChecksumError) -> Self {
|
||||||
|
Self::UError(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an error that was encountered when processing a checksum file.
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
enum FileCheckError {
|
||||||
|
/// a generic UError was encountered in sub-functions
|
||||||
|
UError(Box<dyn UError>),
|
||||||
|
/// the error does not stop the processing of next files
|
||||||
|
NonCriticalError,
|
||||||
|
/// the error must stop the run of the program
|
||||||
|
CriticalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Box<dyn UError>> for FileCheckError {
|
||||||
|
fn from(value: Box<dyn UError>) -> Self {
|
||||||
|
Self::UError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChecksumError> for FileCheckError {
|
||||||
|
fn from(value: ChecksumError) -> Self {
|
||||||
|
Self::UError(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct regroups CLI flags.
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub struct ChecksumOptions {
|
||||||
|
pub binary: bool,
|
||||||
|
pub ignore_missing: bool,
|
||||||
|
pub quiet: bool,
|
||||||
|
pub status: bool,
|
||||||
|
pub strict: bool,
|
||||||
|
pub warn: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ChecksumError {
|
pub enum ChecksumError {
|
||||||
#[error("the --raw option is not supported with multiple files")]
|
#[error("the --raw option is not supported with multiple files")]
|
||||||
|
@ -174,6 +239,8 @@ fn cksum_output(res: &ChecksumResult, status: bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the different outcomes that can happen to a file
|
||||||
|
/// that is being checked.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum FileChecksumResult {
|
enum FileChecksumResult {
|
||||||
Ok,
|
Ok,
|
||||||
|
@ -181,6 +248,28 @@ enum FileChecksumResult {
|
||||||
CantOpen,
|
CantOpen,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FileChecksumResult {
|
||||||
|
/// Creates a `FileChecksumResult` from a digest comparison that
|
||||||
|
/// either succeeded or failed.
|
||||||
|
fn from_bool(checksum_correct: bool) -> Self {
|
||||||
|
if checksum_correct {
|
||||||
|
FileChecksumResult::Ok
|
||||||
|
} else {
|
||||||
|
FileChecksumResult::Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cli options might prevent to display on the outcome of the
|
||||||
|
/// comparison on STDOUT.
|
||||||
|
fn can_display(&self, opts: ChecksumOptions) -> bool {
|
||||||
|
match self {
|
||||||
|
FileChecksumResult::Ok => !opts.status && !opts.quiet,
|
||||||
|
FileChecksumResult::Failed => !opts.status,
|
||||||
|
FileChecksumResult::CantOpen => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for FileChecksumResult {
|
impl Display for FileChecksumResult {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -198,11 +287,14 @@ fn print_file_report<W: Write>(
|
||||||
filename: &[u8],
|
filename: &[u8],
|
||||||
result: FileChecksumResult,
|
result: FileChecksumResult,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
|
opts: ChecksumOptions,
|
||||||
) {
|
) {
|
||||||
|
if result.can_display(opts) {
|
||||||
let _ = write!(w, "{prefix}");
|
let _ = write!(w, "{prefix}");
|
||||||
let _ = w.write_all(filename);
|
let _ = w.write_all(filename);
|
||||||
let _ = writeln!(w, ": {result}");
|
let _ = writeln!(w, ": {result}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn detect_algo(algo: &str, length: Option<usize>) -> UResult<HashAlgorithm> {
|
pub fn detect_algo(algo: &str, length: Option<usize>) -> UResult<HashAlgorithm> {
|
||||||
match algo {
|
match algo {
|
||||||
|
@ -390,45 +482,47 @@ fn get_expected_checksum(
|
||||||
/// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-".
|
/// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-".
|
||||||
fn get_file_to_check(
|
fn get_file_to_check(
|
||||||
filename: &OsStr,
|
filename: &OsStr,
|
||||||
ignore_missing: bool,
|
opts: ChecksumOptions,
|
||||||
res: &mut ChecksumResult,
|
) -> Result<Box<dyn Read>, LineCheckError> {
|
||||||
) -> Option<Box<dyn Read>> {
|
|
||||||
let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error");
|
let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error");
|
||||||
let filename_lossy = String::from_utf8_lossy(filename_bytes);
|
let filename_lossy = String::from_utf8_lossy(filename_bytes);
|
||||||
if filename == "-" {
|
if filename == "-" {
|
||||||
Some(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file
|
Ok(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file
|
||||||
} else {
|
} else {
|
||||||
let mut failed_open = || {
|
let failed_open = || {
|
||||||
print_file_report(
|
print_file_report(
|
||||||
std::io::stdout(),
|
std::io::stdout(),
|
||||||
filename_bytes,
|
filename_bytes,
|
||||||
FileChecksumResult::CantOpen,
|
FileChecksumResult::CantOpen,
|
||||||
"",
|
"",
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
res.failed_open_file += 1;
|
|
||||||
};
|
};
|
||||||
match File::open(filename) {
|
match File::open(filename) {
|
||||||
Ok(f) => {
|
Ok(f) => {
|
||||||
if f.metadata().ok()?.is_dir() {
|
if f.metadata()
|
||||||
|
.map_err(|_| LineCheckError::CantOpenFile)?
|
||||||
|
.is_dir()
|
||||||
|
{
|
||||||
show!(USimpleError::new(
|
show!(USimpleError::new(
|
||||||
1,
|
1,
|
||||||
format!("{filename_lossy}: Is a directory")
|
format!("{filename_lossy}: Is a directory")
|
||||||
));
|
));
|
||||||
// also regarded as a failed open
|
// also regarded as a failed open
|
||||||
failed_open();
|
failed_open();
|
||||||
None
|
Err(LineCheckError::FileIsDirectory)
|
||||||
} else {
|
} else {
|
||||||
Some(Box::new(f))
|
Ok(Box::new(f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if !ignore_missing {
|
if !opts.ignore_missing {
|
||||||
// yes, we have both stderr and stdout here
|
// yes, we have both stderr and stdout here
|
||||||
show!(err.map_err_context(|| filename_lossy.to_string()));
|
show!(err.map_err_context(|| filename_lossy.to_string()));
|
||||||
failed_open();
|
failed_open();
|
||||||
}
|
}
|
||||||
// we could not open the file but we want to continue
|
// we could not open the file but we want to continue
|
||||||
None
|
Err(LineCheckError::FileNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -502,26 +596,130 @@ fn identify_algo_name_and_length(
|
||||||
Some((algorithm, bits))
|
Some((algorithm, bits))
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/// Parses a checksum line, detect the algorithm to use, read the file and produce
|
||||||
* Do the checksum validation (can be strict or not)
|
/// its digest, and compare it to the expected value.
|
||||||
*/
|
///
|
||||||
|
/// Returns `Ok(bool)` if the comparison happened, bool indicates if the digest
|
||||||
|
/// matched the expected.
|
||||||
|
/// If the comparison didn't happen, return a `LineChecksumError`.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn perform_checksum_validation<'a, I>(
|
fn process_checksum_line(
|
||||||
files: I,
|
filename_input: &OsStr,
|
||||||
strict: bool,
|
line: &OsStr,
|
||||||
status: bool,
|
i: usize,
|
||||||
warn: bool,
|
chosen_regex: &Regex,
|
||||||
binary: bool,
|
is_algo_based_format: bool,
|
||||||
ignore_missing: bool,
|
res: &mut ChecksumResult,
|
||||||
quiet: bool,
|
cli_algo_name: Option<&str>,
|
||||||
algo_name_input: Option<&str>,
|
cli_algo_length: Option<usize>,
|
||||||
length_input: Option<usize>,
|
properly_formatted: &mut bool,
|
||||||
) -> UResult<()>
|
opts: ChecksumOptions,
|
||||||
where
|
) -> Result<(), LineCheckError> {
|
||||||
I: Iterator<Item = &'a OsStr>,
|
let line_bytes = os_str_as_bytes(line)?;
|
||||||
|
if let Some(caps) = chosen_regex.captures(line_bytes) {
|
||||||
|
*properly_formatted = true;
|
||||||
|
|
||||||
|
let mut filename_to_check = caps.name("filename").unwrap().as_bytes();
|
||||||
|
|
||||||
|
if filename_to_check.starts_with(b"*")
|
||||||
|
&& i == 0
|
||||||
|
&& chosen_regex.as_str() == SINGLE_SPACE_REGEX
|
||||||
{
|
{
|
||||||
// if cksum has several input files, it will print the result for each file
|
// Remove the leading asterisk if present - only for the first line
|
||||||
for filename_input in files {
|
filename_to_check = &filename_to_check[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_checksum = get_expected_checksum(filename_to_check, &caps, chosen_regex)?;
|
||||||
|
|
||||||
|
// If the algo_name is provided, we use it, otherwise we try to detect it
|
||||||
|
let (algo_name, length) = if is_algo_based_format {
|
||||||
|
identify_algo_name_and_length(&caps, cli_algo_name, res, properly_formatted)
|
||||||
|
.unwrap_or((String::new(), None))
|
||||||
|
} else if let Some(a) = cli_algo_name {
|
||||||
|
// When a specific algorithm name is input, use it and use the provided bits
|
||||||
|
// except when dealing with blake2b, where we will detect the length
|
||||||
|
if cli_algo_name == Some(ALGORITHM_OPTIONS_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.
|
||||||
|
let length = Some(expected_checksum.len() / 2);
|
||||||
|
(ALGORITHM_OPTIONS_BLAKE2B.to_string(), length)
|
||||||
|
} else {
|
||||||
|
(a.to_lowercase(), cli_algo_length)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default case if no algorithm is specified and non-algo based format is matched
|
||||||
|
(String::new(), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
if algo_name.is_empty() {
|
||||||
|
// we haven't been able to detect the algo name. No point to continue
|
||||||
|
*properly_formatted = false;
|
||||||
|
|
||||||
|
// TODO: return error?
|
||||||
|
return Err(LineCheckError::ImproperlyFormatted);
|
||||||
|
}
|
||||||
|
let mut algo = detect_algo(&algo_name, length)?;
|
||||||
|
|
||||||
|
let (filename_to_check_unescaped, prefix) = unescape_filename(filename_to_check);
|
||||||
|
|
||||||
|
let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?;
|
||||||
|
|
||||||
|
// manage the input file
|
||||||
|
let file_to_check = get_file_to_check(&real_filename_to_check, opts)?;
|
||||||
|
let mut file_reader = BufReader::new(file_to_check);
|
||||||
|
|
||||||
|
// Read the file and calculate the checksum
|
||||||
|
let create_fn = &mut algo.create_fn;
|
||||||
|
let mut digest = create_fn();
|
||||||
|
let (calculated_checksum, _) =
|
||||||
|
digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bits).unwrap();
|
||||||
|
|
||||||
|
// Do the checksum validation
|
||||||
|
let checksum_correct = expected_checksum == calculated_checksum;
|
||||||
|
print_file_report(
|
||||||
|
std::io::stdout(),
|
||||||
|
filename_to_check,
|
||||||
|
FileChecksumResult::from_bool(checksum_correct),
|
||||||
|
prefix,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
|
||||||
|
if checksum_correct {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LineCheckError::DigestMismatch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if line.is_empty() || line_bytes.starts_with(b"#") {
|
||||||
|
// Don't show any warning for empty or commented lines.
|
||||||
|
return Err(LineCheckError::Skipped);
|
||||||
|
}
|
||||||
|
if opts.warn {
|
||||||
|
let algo = if let Some(algo_name_input) = cli_algo_name {
|
||||||
|
algo_name_input.to_uppercase()
|
||||||
|
} else {
|
||||||
|
"Unknown algorithm".to_string()
|
||||||
|
};
|
||||||
|
eprintln!(
|
||||||
|
"{}: {}: {}: improperly formatted {} checksum line",
|
||||||
|
util_name(),
|
||||||
|
&filename_input.maybe_quote(),
|
||||||
|
i + 1,
|
||||||
|
algo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.bad_format += 1;
|
||||||
|
Err(LineCheckError::ImproperlyFormatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_checksum_file(
|
||||||
|
filename_input: &OsStr,
|
||||||
|
cli_algo_name: Option<&str>,
|
||||||
|
cli_algo_length: Option<usize>,
|
||||||
|
opts: ChecksumOptions,
|
||||||
|
) -> Result<(), FileCheckError> {
|
||||||
let mut correct_format = 0;
|
let mut correct_format = 0;
|
||||||
let mut properly_formatted = false;
|
let mut properly_formatted = false;
|
||||||
let mut res = ChecksumResult::default();
|
let mut res = ChecksumResult::default();
|
||||||
|
@ -537,7 +735,7 @@ where
|
||||||
// Could not read the file, show the error and continue to the next file
|
// Could not read the file, show the error and continue to the next file
|
||||||
show_error!("{e}");
|
show_error!("{e}");
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
continue;
|
return Err(FileCheckError::NonCriticalError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -551,140 +749,55 @@ where
|
||||||
};
|
};
|
||||||
show_error!("{e}");
|
show_error!("{e}");
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
continue;
|
return Err(FileCheckError::NonCriticalError);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (i, line) in lines.iter().enumerate() {
|
for (i, line) in lines.iter().enumerate() {
|
||||||
let line_bytes = os_str_as_bytes(line)?;
|
match process_checksum_line(
|
||||||
if let Some(caps) = chosen_regex.captures(line_bytes) {
|
filename_input,
|
||||||
properly_formatted = true;
|
line,
|
||||||
|
i,
|
||||||
let mut filename_to_check = caps.name("filename").unwrap().as_bytes();
|
&chosen_regex,
|
||||||
|
is_algo_based_format,
|
||||||
if filename_to_check.starts_with(b"*")
|
|
||||||
&& i == 0
|
|
||||||
&& chosen_regex.as_str() == SINGLE_SPACE_REGEX
|
|
||||||
{
|
|
||||||
// Remove the leading asterisk if present - only for the first line
|
|
||||||
filename_to_check = &filename_to_check[1..];
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected_checksum =
|
|
||||||
get_expected_checksum(filename_to_check, &caps, &chosen_regex)?;
|
|
||||||
|
|
||||||
// If the algo_name is provided, we use it, otherwise we try to detect it
|
|
||||||
let (algo_name, length) = if is_algo_based_format {
|
|
||||||
identify_algo_name_and_length(
|
|
||||||
&caps,
|
|
||||||
algo_name_input,
|
|
||||||
&mut res,
|
&mut res,
|
||||||
|
cli_algo_name,
|
||||||
|
cli_algo_length,
|
||||||
&mut properly_formatted,
|
&mut properly_formatted,
|
||||||
)
|
opts,
|
||||||
.unwrap_or((String::new(), None))
|
) {
|
||||||
} else if let Some(a) = algo_name_input {
|
Ok(()) => correct_format += 1,
|
||||||
// When a specific algorithm name is input, use it and use the provided bits
|
Err(LineCheckError::DigestMismatch) => res.failed_cksum += 1,
|
||||||
// except when dealing with blake2b, where we will detect the length
|
Err(LineCheckError::UError(e)) => return Err(e.into()),
|
||||||
if algo_name_input == Some(ALGORITHM_OPTIONS_BLAKE2B) {
|
Err(LineCheckError::Skipped) => continue,
|
||||||
// division by 2 converts the length of the Blake2b checksum from hexadecimal
|
Err(LineCheckError::ImproperlyFormatted) => (),
|
||||||
// characters to bytes, as each byte is represented by two hexadecimal characters.
|
Err(LineCheckError::CantOpenFile | LineCheckError::FileIsDirectory) => {
|
||||||
let length = Some(expected_checksum.len() / 2);
|
res.failed_open_file += 1
|
||||||
(ALGORITHM_OPTIONS_BLAKE2B.to_string(), length)
|
}
|
||||||
} else {
|
Err(LineCheckError::FileNotFound) => {
|
||||||
(a.to_lowercase(), length_input)
|
if !opts.ignore_missing {
|
||||||
|
res.failed_open_file += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Default case if no algorithm is specified and non-algo based format is matched
|
|
||||||
(String::new(), None)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if algo_name.is_empty() {
|
|
||||||
// we haven't been able to detect the algo name. No point to continue
|
|
||||||
properly_formatted = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut algo = detect_algo(&algo_name, length)?;
|
|
||||||
|
|
||||||
let (filename_to_check_unescaped, prefix) = unescape_filename(filename_to_check);
|
|
||||||
|
|
||||||
let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?;
|
|
||||||
|
|
||||||
// manage the input file
|
|
||||||
let file_to_check =
|
|
||||||
match get_file_to_check(&real_filename_to_check, ignore_missing, &mut res) {
|
|
||||||
Some(file) => file,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
let mut file_reader = BufReader::new(file_to_check);
|
|
||||||
// Read the file and calculate the checksum
|
|
||||||
let create_fn = &mut algo.create_fn;
|
|
||||||
let mut digest = create_fn();
|
|
||||||
let (calculated_checksum, _) =
|
|
||||||
digest_reader(&mut digest, &mut file_reader, binary, algo.bits).unwrap();
|
|
||||||
|
|
||||||
// Do the checksum validation
|
|
||||||
if expected_checksum == calculated_checksum {
|
|
||||||
if !quiet && !status {
|
|
||||||
print_file_report(
|
|
||||||
std::io::stdout(),
|
|
||||||
filename_to_check,
|
|
||||||
FileChecksumResult::Ok,
|
|
||||||
prefix,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
correct_format += 1;
|
|
||||||
} else {
|
|
||||||
if !status {
|
|
||||||
print_file_report(
|
|
||||||
std::io::stdout(),
|
|
||||||
filename_to_check,
|
|
||||||
FileChecksumResult::Failed,
|
|
||||||
prefix,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.failed_cksum += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if line.is_empty() || line_bytes.starts_with(b"#") {
|
|
||||||
// Don't show any warning for empty or commented lines.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if warn {
|
|
||||||
let algo = if let Some(algo_name_input) = algo_name_input {
|
|
||||||
algo_name_input.to_uppercase()
|
|
||||||
} else {
|
|
||||||
"Unknown algorithm".to_string()
|
|
||||||
};
|
|
||||||
eprintln!(
|
|
||||||
"{}: {}: {}: improperly formatted {} checksum line",
|
|
||||||
util_name(),
|
|
||||||
&filename_input.maybe_quote(),
|
|
||||||
i + 1,
|
|
||||||
algo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.bad_format += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// not a single line correctly formatted found
|
// not a single line correctly formatted found
|
||||||
// return an error
|
// return an error
|
||||||
if !properly_formatted {
|
if !properly_formatted {
|
||||||
if !status {
|
if !opts.status {
|
||||||
return Err(ChecksumError::NoProperlyFormattedChecksumLinesFound {
|
return Err(ChecksumError::NoProperlyFormattedChecksumLinesFound {
|
||||||
filename: get_filename_for_output(filename_input, input_is_stdin),
|
filename: get_filename_for_output(filename_input, input_is_stdin),
|
||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
|
return Err(FileCheckError::CriticalError);
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if any incorrectly formatted line, show it
|
// if any incorrectly formatted line, show it
|
||||||
cksum_output(&res, status);
|
cksum_output(&res, opts.status);
|
||||||
|
|
||||||
if ignore_missing && correct_format == 0 {
|
if opts.ignore_missing && correct_format == 0 {
|
||||||
// we have only bad format
|
// we have only bad format
|
||||||
// and we had ignore-missing
|
// and we had ignore-missing
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
@ -696,15 +809,39 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// strict means that we should have an exit code.
|
// strict means that we should have an exit code.
|
||||||
if strict && res.bad_format > 0 {
|
if opts.strict && res.bad_format > 0 {
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have any failed checksum verification, we set an exit code
|
// if we have any failed checksum verification, we set an exit code
|
||||||
// except if we have ignore_missing
|
// except if we have ignore_missing
|
||||||
if (res.failed_cksum > 0 || res.failed_open_file > 0) && !ignore_missing {
|
if (res.failed_cksum > 0 || res.failed_open_file > 0) && !opts.ignore_missing {
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Do the checksum validation (can be strict or not)
|
||||||
|
*/
|
||||||
|
pub fn perform_checksum_validation<'a, I>(
|
||||||
|
files: I,
|
||||||
|
algo_name_input: Option<&str>,
|
||||||
|
length_input: Option<usize>,
|
||||||
|
opts: ChecksumOptions,
|
||||||
|
) -> UResult<()>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = &'a OsStr>,
|
||||||
|
{
|
||||||
|
// if cksum has several input files, it will print the result for each file
|
||||||
|
for filename_input in files {
|
||||||
|
use FileCheckError::*;
|
||||||
|
match process_checksum_file(filename_input, algo_name_input, length_input, opts) {
|
||||||
|
Err(UError(e)) => return Err(e),
|
||||||
|
Err(CriticalError) => break,
|
||||||
|
Err(NonCriticalError) | Ok(_) => continue,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1139,6 +1276,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_print_file_report() {
|
fn test_print_file_report() {
|
||||||
|
let opts = ChecksumOptions::default();
|
||||||
|
|
||||||
let cases: &[(&[u8], FileChecksumResult, &str, &[u8])] = &[
|
let cases: &[(&[u8], FileChecksumResult, &str, &[u8])] = &[
|
||||||
(b"filename", FileChecksumResult::Ok, "", b"filename: OK\n"),
|
(b"filename", FileChecksumResult::Ok, "", b"filename: OK\n"),
|
||||||
(
|
(
|
||||||
|
@ -1169,7 +1308,7 @@ mod tests {
|
||||||
|
|
||||||
for (filename, result, prefix, expected) in cases {
|
for (filename, result, prefix, expected) in cases {
|
||||||
let mut buffer: Vec<u8> = vec![];
|
let mut buffer: Vec<u8> = vec![];
|
||||||
print_file_report(&mut buffer, filename, *result, prefix);
|
print_file_report(&mut buffer, filename, *result, prefix, opts);
|
||||||
assert_eq!(&buffer, expected)
|
assert_eq!(&buffer, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
// spell-checker:ignore (words) asdf algo algos asha mgmt xffname
|
// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb
|
||||||
|
|
||||||
use crate::common::util::TestScenario;
|
use crate::common::util::TestScenario;
|
||||||
|
|
||||||
|
@ -1502,3 +1502,260 @@ mod check_utf8 {
|
||||||
.stderr_contains("1 listed file could not be read");
|
.stderr_contains("1 listed file could not be read");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ignore = "not yet implemented"]
|
||||||
|
#[test]
|
||||||
|
fn test_check_blake_length_guess() {
|
||||||
|
let correct_lines = [
|
||||||
|
// Correct: The length is not explicit, but the checksum's size
|
||||||
|
// matches the default parameter.
|
||||||
|
"BLAKE2b (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d",
|
||||||
|
// Correct: The length is explicitly given, and the checksum's size
|
||||||
|
// matches the length.
|
||||||
|
"BLAKE2b-512 (foo.dat) = ca002330e69d3e6b84a46a56a6533fd79d51d97a3bb7cad6c2ff43b354185d6dc1e723fb3db4ae0737e120378424c714bb982d9dc5bbd7a0ab318240ddd18f8d",
|
||||||
|
// Correct: the checksum size is not default but
|
||||||
|
// the length is explicitly given.
|
||||||
|
"BLAKE2b-48 (foo.dat) = 171cdfdf84ed",
|
||||||
|
];
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
at.write("foo.dat", "foo");
|
||||||
|
|
||||||
|
for line in correct_lines {
|
||||||
|
at.write("foo.sums", line);
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg(at.subdir.join("foo.sums"))
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("foo.dat: OK\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incorrect lines
|
||||||
|
|
||||||
|
// This is incorrect because the algorithm provides no length,
|
||||||
|
// and the checksum length is not default.
|
||||||
|
let incorrect = "BLAKE2b (foo.dat) = 171cdfdf84ed";
|
||||||
|
at.write("foo.sums", incorrect);
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg(at.subdir.join("foo.sums"))
|
||||||
|
.fails()
|
||||||
|
.stderr_contains("foo.sums: no properly formatted checksum lines found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore = "not yet implemented"]
|
||||||
|
#[test]
|
||||||
|
fn test_check_confusing_base64() {
|
||||||
|
let cksum = "BLAKE2b-48 (foo.dat) = fc1f97C4";
|
||||||
|
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
at.write("foo.dat", "esq");
|
||||||
|
at.write("foo.sums", cksum);
|
||||||
|
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg(at.subdir.join("foo.sums"))
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("foo.dat: OK\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This test checks that when a file contains several checksum lines
|
||||||
|
/// with different encoding, the decoding still works.
|
||||||
|
#[ignore = "not yet implemented"]
|
||||||
|
#[test]
|
||||||
|
fn test_check_mix_hex_base64() {
|
||||||
|
let b64 = "BLAKE2b-128 (foo1.dat) = BBNuJPhdRwRlw9tm5Y7VbA==";
|
||||||
|
let hex = "BLAKE2b-128 (foo2.dat) = 04136e24f85d470465c3db66e58ed56c";
|
||||||
|
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
at.write("foo1.dat", "foo");
|
||||||
|
at.write("foo2.dat", "foo");
|
||||||
|
|
||||||
|
at.write("hex_b64", &format!("{hex}\n{b64}"));
|
||||||
|
at.write("b64_hex", &format!("{b64}\n{hex}"));
|
||||||
|
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg(at.subdir.join("hex_b64"))
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("foo2.dat: OK\nfoo1.dat: OK\n");
|
||||||
|
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg(at.subdir.join("b64_hex"))
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("foo1.dat: OK\nfoo2.dat: OK\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore = "not yet implemented"]
|
||||||
|
#[test]
|
||||||
|
fn test_check_incorrectly_formatted_checksum_does_not_stop_processing() {
|
||||||
|
// The first line contains an incorrectly formatted checksum that can't be
|
||||||
|
// correctly decoded. This must not prevent the program from looking at the
|
||||||
|
// rest of the file.
|
||||||
|
let lines = [
|
||||||
|
"BLAKE2b-56 (foo1) = GFYEQ7HhAw=", // Should be 2 '=' at the end
|
||||||
|
"BLAKE2b-56 (foo2) = 18560443b1e103", // OK
|
||||||
|
];
|
||||||
|
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
at.write("foo1", "foo");
|
||||||
|
at.write("foo2", "foo");
|
||||||
|
at.write("sum", &lines.join("\n"));
|
||||||
|
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg(at.subdir.join("sum"))
|
||||||
|
.succeeds()
|
||||||
|
.stderr_contains("1 line is improperly formatted")
|
||||||
|
.stdout_contains("foo2: OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This module reimplements the cksum-base64.pl GNU test.
|
||||||
|
mod cksum_base64 {
|
||||||
|
use super::*;
|
||||||
|
use crate::common::util::log_info;
|
||||||
|
|
||||||
|
const PAIRS: [(&str, &str); 11] = [
|
||||||
|
("sysv", "0 0 f"),
|
||||||
|
("bsd", "00000 0 f"),
|
||||||
|
("crc", "4294967295 0 f"),
|
||||||
|
("md5", "1B2M2Y8AsgTpgAmY7PhCfg=="),
|
||||||
|
("sha1", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="),
|
||||||
|
("sha224", "0UoCjCo6K8lHYQK7KII0xBWisB+CjqYqxbPkLw=="),
|
||||||
|
("sha256", "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="),
|
||||||
|
(
|
||||||
|
"sha384",
|
||||||
|
"OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sha512",
|
||||||
|
"z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"blake2b",
|
||||||
|
"eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg=="
|
||||||
|
),
|
||||||
|
("sm3", "GrIdg1XPoX+OYRlIMegajyK+yMco/vt0ftA161CCqis="),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn make_scene() -> TestScenario {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
at.touch("f");
|
||||||
|
|
||||||
|
scene
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_format(algo: &str, digest: &str) -> String {
|
||||||
|
if ["sysv", "bsd", "crc"].contains(&algo) {
|
||||||
|
digest.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} (f) = {}", algo.to_uppercase(), digest).replace("BLAKE2B", "BLAKE2b")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generating() {
|
||||||
|
// Ensure that each algorithm works with `--base64`.
|
||||||
|
let scene = make_scene();
|
||||||
|
|
||||||
|
for (algo, digest) in PAIRS {
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--base64")
|
||||||
|
.arg("-a")
|
||||||
|
.arg(algo)
|
||||||
|
.arg("f")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("{}\n", output_format(algo, digest)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chk() {
|
||||||
|
// For each algorithm that accepts `--check`,
|
||||||
|
// ensure that it works with base64 digests.
|
||||||
|
let scene = make_scene();
|
||||||
|
|
||||||
|
for (algo, digest) in PAIRS {
|
||||||
|
if ["sysv", "bsd", "crc"].contains(&algo) {
|
||||||
|
// These algorithms do not accept `--check`
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = output_format(algo, digest);
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.arg("--strict")
|
||||||
|
.pipe_in(line)
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("f: OK\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chk_eq1() {
|
||||||
|
// For digests ending with '=', ensure `--check` fails if '=' is removed.
|
||||||
|
let scene = make_scene();
|
||||||
|
|
||||||
|
for (algo, digest) in PAIRS {
|
||||||
|
if !digest.ends_with('=') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line = output_format(algo, digest);
|
||||||
|
if line.ends_with('=') {
|
||||||
|
line.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), "");
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.pipe_in(line)
|
||||||
|
.fails()
|
||||||
|
.no_stdout()
|
||||||
|
.stderr_contains("no properly formatted checksum lines found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chk_eq2() {
|
||||||
|
// For digests ending with '==',
|
||||||
|
// ensure `--check` fails if '==' is removed.
|
||||||
|
let scene = make_scene();
|
||||||
|
|
||||||
|
for (algo, digest) in PAIRS {
|
||||||
|
if !digest.ends_with("==") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = output_format(algo, digest);
|
||||||
|
let line = line.trim_end_matches("==");
|
||||||
|
|
||||||
|
log_info(format!("ALGORITHM: {algo}, STDIN: '{line}'"), "");
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("--check")
|
||||||
|
.pipe_in(line)
|
||||||
|
.fails()
|
||||||
|
.no_stdout()
|
||||||
|
.stderr_contains("no properly formatted checksum lines found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue