chksum: add support of --check --algorithm=xxx

This commit is contained in:
Sylvestre Ledru 2024-05-11 08:25:45 +02:00
parent 0b04bcaf9a
commit 1aec8b407d
2 changed files with 254 additions and 56 deletions

View file

@ -75,10 +75,10 @@ impl Display for CkSumError {
}
fn detect_algo(
program: &str,
algo: &str,
length: Option<usize>,
) -> (&'static str, Box<dyn Digest + 'static>, usize) {
match program {
match algo {
ALGORITHM_OPTIONS_SYSV => (
ALGORITHM_OPTIONS_SYSV,
Box::new(SYSV::new()) as Box<dyn Digest>,
@ -411,48 +411,99 @@ fn handle_tag_text_binary_flags(matches: &clap::ArgMatches) -> UResult<(bool, bo
/***
* Do the checksum validation (can be strict or not)
*/
fn perform_checksum_validation<'a, I>(files: I, strict: bool) -> UResult<()>
fn perform_checksum_validation<'a, I>(
files: I,
strict: bool,
algo_name_input: Option<&str>,
) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
let re = Regex::new(r"(?P<algo>\w+)(-(?P<bits>\d+))? \((?P<filename>.*)\) = (?P<checksum>.*)")
.unwrap();
let mut properly_formatted = false;
let mut bad_format = 0;
let mut failed_cksum = 0;
let mut failed_open_file = 0;
// Regexp to handle the two input formats:
// 1. <algo>[-<bits>] (<filename>) = <checksum>
// 2. <checksum> [* ]<filename>
let regex_pattern = r"^(?P<algo>\w+)(-(?P<bits>\d+))?\s?\((?P<filename1>.*)\) = (?P<checksum1>[a-fA-F0-9]+)$|^(?P<checksum2>[a-fA-F0-9]+)\s[* ](?P<filename2>.*)";
let re = Regex::new(regex_pattern).unwrap();
// if cksum has several input files, it will print the result for each file
for filename_input in files {
let mut bad_format = 0;
let mut failed_cksum = 0;
let mut failed_open_file = 0;
let mut properly_formatted = false;
let input_is_stdin = filename_input == OsStr::new("-");
let file: Box<dyn Read> = if input_is_stdin {
Box::new(stdin()) // Use stdin if "-" is specified
} else {
match File::open(filename_input) {
Ok(f) => Box::new(f),
Err(err) => {
show!(err.map_err_context(|| format!(
"Failed to open file: {}",
filename_input.to_string_lossy()
)));
return Err(io::Error::new(io::ErrorKind::Other, "Failed to open file").into());
Err(_) => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"{}: No such file or directory",
filename_input.to_string_lossy()
),
)
.into());
}
}
};
let reader = BufReader::new(file);
// for each line in the input, check if it is a valid checksum line
for line in reader.lines() {
let line = line?;
let line = line.unwrap_or_else(|_| String::new());
if let Some(caps) = re.captures(&line) {
properly_formatted = true;
let algo_name = caps.name("algo").unwrap().as_str().to_lowercase();
let filename_to_check = caps.name("filename").unwrap().as_str();
let expected_checksum = caps.name("checksum").unwrap().as_str();
let length = caps
.name("bits")
.map(|m| m.as_str().parse::<usize>().unwrap() / 8);
let (_, mut algo, bits) = detect_algo(&algo_name, length);
// Determine what kind of file input we had
// we need it for case "--check -a sm3 <file>" when <file> is
// <algo>[-<bits>] (<filename>) = <checksum>
let algo_based_format =
caps.name("filename1").is_some() && caps.name("checksum1").is_some();
let filename_to_check = caps
.name("filename1")
.or(caps.name("filename2"))
.unwrap()
.as_str();
let expected_checksum = caps
.name("checksum1")
.or(caps.name("checksum2"))
.unwrap()
.as_str();
// If the algo_name is provided, we use it, otherwise we try to detect it
let algo_details = if algo_based_format {
// When the algo-based format is matched, extract details from regex captures
let algorithm = caps.name("algo").map_or("", |m| m.as_str()).to_lowercase();
let bits = caps
.name("bits")
.map(|m| m.as_str().parse::<usize>().unwrap() / 8);
(algorithm, bits)
} else if let Some(a) = algo_name_input {
// When a specific algorithm name is input, use it and default bits to None
(a.to_lowercase(), None)
} else {
// Default case if no algorithm is specified and non-algo based format is matched
(String::new(), None)
};
if algo_based_format
&& algo_name_input.map_or(false, |input| algo_details.0 != input)
{
bad_format += 1;
continue;
}
if algo_details.0.is_empty() {
// we haven't been able to detect the algo name. No point to continue
properly_formatted = false;
continue;
}
let (_, mut algo, bits) = detect_algo(&algo_details.0, algo_details.1);
// manage the input file
let file_to_check: Box<dyn Read> = if filename_to_check == "-" {
Box::new(stdin()) // Use stdin if "-" is specified in the checksum file
} else {
@ -470,9 +521,11 @@ where
}
};
let mut file_reader = BufReader::new(file_to_check);
// Read the file and calculate the checksum
let (calculated_checksum, _) =
digest_read(&mut algo, &mut file_reader, bits).unwrap();
let (calculated_checksum, _) = digest_read(&mut algo, &mut file_reader, bits)
.map_err_context(|| "failed to read input".to_string())?;
// Do the checksum validation
if expected_checksum == calculated_checksum {
println!("{}: OK", filename_to_check);
} else {
@ -483,31 +536,29 @@ where
bad_format += 1;
}
}
// not a single line correctly formatted found
// return an error
if !properly_formatted {
uucore::show_error!(
"{}: no properly formatted checksum lines found",
filename_input.to_string_lossy()
);
set_exit_code(1);
}
// strict means that we should have an exit code.
if strict && bad_format > 0 {
set_exit_code(1);
}
// if we have any failed checksum verification, we set an exit code
if failed_cksum > 0 || failed_open_file > 0 {
set_exit_code(1);
}
// if any incorrectly formatted line, show it
cksum_output(bad_format, failed_cksum, failed_open_file);
}
// not a single line correctly formatted found
// return an error
if !properly_formatted {
return Err(io::Error::new(
io::ErrorKind::Other,
"no properly formatted checksum lines found",
)
.into());
}
// strict means that we should have an exit code.
if strict && bad_format > 0 {
set_exit_code(1);
}
// if we have any failed checksum verification, we set an exit code
if failed_cksum > 0 || failed_open_file > 0 {
set_exit_code(1);
}
// if any incorrectly formatted line, show it
cksum_output(bad_format, failed_cksum, failed_open_file);
Ok(())
}
@ -549,10 +600,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
)
.into());
}
// Determine the appropriate algorithm option to pass
let algo_option = if algo_name.is_empty() {
None
} else {
Some(algo_name)
};
// Execute the checksum validation based on the presence of files or the use of stdin
return match matches.get_many::<String>(options::FILE) {
Some(files) => perform_checksum_validation(files.map(OsStr::new), strict),
None => perform_checksum_validation(iter::once(OsStr::new("-")), strict),
Some(files) => perform_checksum_validation(files.map(OsStr::new), strict, algo_option),
None => perform_checksum_validation(iter::once(OsStr::new("-")), strict, algo_option),
};
}

View file

@ -472,6 +472,21 @@ fn test_all_algorithms_fail_on_folder() {
}
}
#[cfg(unix)]
#[test]
fn test_check_error_incorrect_format() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.write("checksum", "e5773576fc75ff0f8eba14f61587ae28 README.md");
scene
.ucmd()
.arg("-c")
.arg("checksum")
.fails()
.stderr_contains("no properly formatted checksum lines found");
}
#[cfg(unix)]
#[test]
fn test_dev_null() {
@ -674,13 +689,13 @@ fn test_check_algo_err() {
scene
.ucmd()
.arg("--a")
.arg("-a")
.arg("sm3")
.arg("--check")
.arg("f")
.fails()
.no_stdout()
.stderr_contains("cksum: no properly formatted checksum lines found")
.stderr_contains("cksum: f: no properly formatted checksum lines found")
.code_is(1);
}
@ -705,14 +720,16 @@ fn test_cksum_check() {
.arg("--check")
.arg("CHECKSUM")
.succeeds()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n");
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
.stderr_does_not_contain("line is improperly formatted");
scene
.ucmd()
.arg("--check")
.arg("--strict")
.arg("CHECKSUM")
.succeeds()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n");
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
.stderr_does_not_contain("line is improperly formatted");
// inject invalid content
at.append("CHECKSUM", "incorrect data");
scene
@ -805,4 +822,127 @@ fn test_cksum_check_failed() {
.stderr_str()
.contains("1 listed file could not be read\n"));
assert!(result.stdout_str().contains("f: OK\n"));
// tests with two files
at.touch("CHECKSUM2");
at.write("f2", "42");
for command in &commands {
let result = scene.ucmd().args(command).arg("f2").succeeds();
at.append("CHECKSUM2", result.stdout_str());
}
// inject invalid content
at.append("CHECKSUM2", "again incorrect data\naze\nSM3 (input2) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n");
at.append("CHECKSUM2", "again incorrect data\naze\nSM3 (input2) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n");
let result = scene
.ucmd()
.arg("--check")
.arg("CHECKSUM")
.arg("CHECKSUM2")
.fails();
println!("result.stderr_str() {}", result.stderr_str());
println!("result.stdout_str() {}", result.stdout_str());
assert!(result.stderr_str().contains("Failed to open file: input2"));
assert!(result
.stderr_str()
.contains("4 lines are improperly formatted\n"));
assert!(result
.stderr_str()
.contains("2 listed files could not be read\n"));
assert!(result.stdout_str().contains("f: OK\n"));
assert!(result.stdout_str().contains("2: OK\n"));
}
#[test]
fn test_check_md5_format() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("empty");
at.write("f", "d41d8cd98f00b204e9800998ecf8427e *empty\n");
scene
.ucmd()
.arg("-a")
.arg("md5")
.arg("--check")
.arg("f")
.succeeds()
.stdout_contains("empty: OK");
// with a second file
at.write("not-empty", "42");
at.write("f2", "a1d0c6e83f027327d8461063f4ac58a6 *not-empty\n");
scene
.ucmd()
.arg("-a")
.arg("md5")
.arg("--check")
.arg("f")
.arg("f2")
.succeeds()
.stdout_contains("empty: OK")
.stdout_contains("not-empty: OK");
}
// Manage the mixed behavior
// cksum --check -a sm3 CHECKSUMS
// when CHECKSUM contains among other lines:
// SHA384 (input) = f392fd0ae43879ced890c665a1d47179116b5eddf6fb5b49f4982746418afdcbd54ba5eedcd422af3592f57f666da285
#[test]
fn test_cksum_mixed() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let commands = [
vec!["-a", "sha384"],
vec!["-a", "blake2b"],
vec!["-a", "blake2b", "-l", "384"],
vec!["-a", "sm3"],
];
at.touch("f");
at.touch("CHECKSUM");
for command in &commands {
let result = scene.ucmd().args(command).arg("f").succeeds();
at.append("CHECKSUM", result.stdout_str());
}
scene
.ucmd()
.arg("--check")
.arg("-a")
.arg("sm3")
.arg("CHECKSUM")
.succeeds()
.stdout_contains("f: OK")
.stderr_contains("3 lines are improperly formatted");
}
#[test]
fn test_cksum_garbage() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
// Incorrect data at the start
at.write(
"check-file",
"garbage MD5 (README.md) = e5773576fc75ff0f8eba14f61587ae28",
);
scene
.ucmd()
.arg("--check")
.arg("check-file")
.fails()
.stderr_contains("check-file: no properly formatted checksum lines found");
// Incorrect data at the end
at.write(
"check-file",
"MD5 (README.md) = e5773576fc75ff0f8eba14f61587ae28 garbage",
);
scene
.ucmd()
.arg("--check")
.arg("check-file")
.fails()
.stderr_contains("check-file: no properly formatted checksum lines found");
}