cksum: implement check & strict

This commit is contained in:
Sylvestre Ledru 2024-05-09 10:10:47 +02:00
parent 5f0b48e649
commit 81500ae3b8
4 changed files with 326 additions and 27 deletions

1
Cargo.lock generated
View file

@ -2494,6 +2494,7 @@ version = "0.0.26"
dependencies = [
"clap",
"hex",
"regex",
"uucore",
]

View file

@ -18,6 +18,7 @@ path = "src/cksum.rs"
clap = { workspace = true }
uucore = { workspace = true, features = ["encoding", "sum"] }
hex = { workspace = true }
regex = { workspace = true }
[[bin]]
name = "cksum"

View file

@ -5,13 +5,18 @@
// spell-checker:ignore (ToDO) fname, algo
use clap::{crate_version, value_parser, Arg, ArgAction, Command};
use regex::Regex;
use std::cmp::Ordering;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt::Display;
use std::fs::File;
use std::io::BufRead;
use std::io::{self, stdin, stdout, BufReader, Read, Write};
use std::iter;
use std::path::Path;
use uucore::error::set_exit_code;
use uucore::show_warning_caps;
use uucore::{
encoding,
error::{FromIo, UError, UResult, USimpleError},
@ -212,7 +217,8 @@ where
OutputFormat::Hexadecimal => sum_hex,
OutputFormat::Base64 => match options.algo_name {
ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => sum_hex,
_ => encoding::encode(encoding::Format::Base64, &hex::decode(sum_hex).unwrap()).unwrap(),
_ => encoding::encode(encoding::Format::Base64, &hex::decode(sum_hex).unwrap())
.unwrap(),
},
};
// The BSD checksum output is 5 digit integer
@ -310,6 +316,7 @@ mod options {
pub const RAW: &str = "raw";
pub const BASE64: &str = "base64";
pub const CHECK: &str = "check";
pub const STRICT: &str = "strict";
pub const TEXT: &str = "text";
pub const BINARY: &str = "binary";
}
@ -357,12 +364,8 @@ fn calculate_length(algo_name: &str, length: usize) -> UResult<Option<usize>> {
0 => Ok(None),
n if n % 8 != 0 => {
uucore::show_error!("invalid length: \u{2018}{length}\u{2019}");
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"length is not a multiple of 8",
)
.into())
},
Err(io::Error::new(io::ErrorKind::InvalidInput, "length is not a multiple of 8").into())
}
n if n > 512 => {
uucore::show_error!("invalid length: \u{2018}{length}\u{2019}");
Err(io::Error::new(
@ -370,7 +373,7 @@ fn calculate_length(algo_name: &str, length: usize) -> UResult<Option<usize>> {
"maximum digest length for \u{2018}BLAKE2b\u{2019} is 512 bits",
)
.into())
},
}
n => {
if algo_name == ALGORITHM_OPTIONS_BLAKE2B {
// Divide by 8, as our blake2b implementation expects bytes instead of bits.
@ -391,12 +394,11 @@ fn calculate_length(algo_name: &str, length: usize) -> UResult<Option<usize>> {
* We handle this in this function to make sure they are self contained
* and "easier" to understand
*/
fn handle_tag_text_binary_flags(matches: &clap::ArgMatches, check: bool) -> UResult<(bool, bool)> {
fn handle_tag_text_binary_flags(matches: &clap::ArgMatches) -> UResult<(bool, bool)> {
let untagged: bool = matches.get_flag(options::UNTAGGED);
let tag: bool = matches.get_flag(options::TAG);
let tag: bool = tag || !untagged;
let text_flag: bool = matches.get_flag(options::TEXT);
let binary_flag: bool = matches.get_flag(options::BINARY);
let args: Vec<String> = std::env::args().collect();
@ -404,34 +406,158 @@ fn handle_tag_text_binary_flags(matches: &clap::ArgMatches, check: bool) -> URes
let asterisk: bool = prompt_asterisk(tag, binary_flag, had_reset);
if (binary_flag || text_flag) && check {
Ok((tag, asterisk))
}
/***
* Do the checksum validation (can be strict or not)
*/
fn perform_checksum_validation<'a, I>(files: I, strict: bool) -> 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;
for filename_input in files {
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());
}
}
};
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
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);
let file_to_check: Box<dyn Read> = if filename_to_check == "-" {
Box::new(stdin()) // Use stdin if "-" is specified in the checksum file
} else {
match File::open(filename_to_check) {
Ok(f) => Box::new(f),
Err(err) => {
show!(err.map_err_context(|| format!(
"Failed to open file: {}",
filename_to_check
)));
failed_open_file += 1;
// we could not open the file but we want to continue
continue;
}
}
};
let mut file_reader = BufReader::new(file_to_check);
let (calculated_checksum, _) = digest_read(&mut algo, &mut file_reader, bits)
.map_err_context(|| "failed to read input".to_string())?;
if expected_checksum == calculated_checksum {
println!("{}: OK", filename_to_check);
} else {
println!("{}: FAILED", filename_to_check);
failed_cksum += 1;
}
} else {
bad_format += 1;
}
}
}
// not a single line correctly formatted found
// return an error
if !properly_formatted {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"the --binary and --text options are meaningless when verifying checksums",
io::ErrorKind::Other,
"no properly formatted checksum lines found",
)
.into());
}
Ok((tag, asterisk))
// strict means that we should have an exit code.
if strict && bad_format > 0 {
set_exit_code(1);
}
// if any incorrectly formatted line, show it
match bad_format.cmp(&1) {
Ordering::Equal => {
show_warning_caps!("{} line is improperly formatted", bad_format);
}
Ordering::Greater => {
show_warning_caps!("{} lines are improperly formatted", bad_format);
}
Ordering::Less => {}
};
// if we have any failed checksum verification, we set an exit code
if failed_cksum > 0 || failed_open_file > 0 {
set_exit_code(1);
}
match failed_open_file.cmp(&1) {
Ordering::Equal => {
show_warning_caps!("{} listed file could not be read", failed_open_file);
}
Ordering::Greater => {
show_warning_caps!("{} listed files could not be read", failed_open_file);
}
Ordering::Less => {}
}
match failed_cksum.cmp(&1) {
Ordering::Equal => {
show_warning_caps!("{} computed checksum did NOT match", failed_cksum);
}
Ordering::Greater => {
show_warning_caps!("{} computed checksums did NOT match", failed_cksum);
}
Ordering::Less => {}
};
Ok(())
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let check = matches.get_flag(options::CHECK);
let algo_name: &str = match matches.get_one::<String>(options::ALGORITHM) {
Some(v) => v,
None => ALGORITHM_OPTIONS_CRC,
None => {
if check {
// if we are doing a --check, we should not default to crc
""
} else {
ALGORITHM_OPTIONS_CRC
}
}
};
let input_length = matches.get_one::<usize>(options::LENGTH);
let length = match input_length {
Some(length) => calculate_length(algo_name, *length)?,
None => None,
};
let (tag, asterisk) = handle_tag_text_binary_flags(&matches, check)?;
if ["bsd", "crc", "sysv"].contains(&algo_name) && check {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
@ -440,6 +566,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.into());
}
if check {
let text_flag: bool = matches.get_flag(options::TEXT);
let binary_flag: bool = matches.get_flag(options::BINARY);
let strict = matches.get_flag(options::STRICT);
if (binary_flag || text_flag) && check {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"the --binary and --text options are meaningless when verifying checksums",
)
.into());
}
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),
};
}
let input_length = matches.get_one::<usize>(options::LENGTH);
let length = match input_length {
Some(length) => calculate_length(algo_name, *length)?,
None => None,
};
let (tag, asterisk) = handle_tag_text_binary_flags(&matches)?;
let (name, algo, bits) = detect_algo(algo_name, length);
let output_format = if matches.get_flag(options::RAW) {
@ -532,12 +686,12 @@ pub fn uu_app() -> Command {
.help("emit a raw binary digest, not hexadecimal")
.action(ArgAction::SetTrue),
)
/*.arg(
.arg(
Arg::new(options::STRICT)
.long(options::STRICT)
.help("exit non-zero for improperly formatted checksum lines")
.action(ArgAction::SetTrue),
)*/
)
.arg(
Arg::new(options::CHECK)
.short('c')
@ -577,8 +731,8 @@ pub fn uu_app() -> Command {
#[cfg(test)]
mod tests {
use super::had_reset;
use crate::prompt_asterisk;
use crate::calculate_length;
use crate::prompt_asterisk;
#[test]
fn test_had_reset() {

View file

@ -663,3 +663,146 @@ fn test_conflicting_options() {
)
.code_is(1);
}
#[test]
fn test_check_algo_err() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("f");
scene
.ucmd()
.arg("--a")
.arg("sm3")
.arg("--check")
.arg("f")
.fails()
.no_stdout()
.stderr_contains("cksum: no properly formatted checksum lines found")
.code_is(1);
}
#[test]
fn test_cksum_check() {
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("CHECKSUM")
.succeeds()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n");
scene
.ucmd()
.arg("--check")
.arg("--strict")
.arg("CHECKSUM")
.succeeds()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n");
// inject invalid content
at.append("CHECKSUM", "incorrect data");
scene
.ucmd()
.arg("--check")
.arg("CHECKSUM")
.succeeds()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
.stderr_contains("line is improperly formatted");
scene
.ucmd()
.arg("--check")
.arg("--strict")
.arg("CHECKSUM")
.fails()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
.stderr_contains("line is improperly formatted");
}
#[test]
fn test_cksum_check_invalid() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let commands = [vec!["-a", "sha384"]];
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());
}
// inject invalid content
at.append("CHECKSUM", "again incorrect data\naze\n");
scene
.ucmd()
.arg("--check")
.arg("--strict")
.arg("CHECKSUM")
.fails()
.stdout_contains("f: OK\n")
.stderr_contains("2 lines");
// without strict, it passes
scene
.ucmd()
.arg("--check")
.arg("CHECKSUM")
.succeeds()
.stdout_contains("f: OK\n")
.stderr_contains("2 lines");
}
#[test]
fn test_cksum_check_failed() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let commands = [vec!["-a", "sha384"]];
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());
}
// inject invalid content
at.append("CHECKSUM", "again incorrect data\naze\nSM3 (input) = 7cfb120d4fabea2a904948538a438fdb57c725157cb40b5aee8d937b8351477e\n");
let result = scene
.ucmd()
.arg("--check")
.arg("--strict")
.arg("CHECKSUM")
.fails();
assert!(result.stderr_str().contains("Failed to open file: input"));
assert!(result
.stderr_str()
.contains("2 lines are improperly formatted\n"));
assert!(result
.stderr_str()
.contains("1 listed file could not be read\n"));
assert!(result.stdout_str().contains("f: OK\n"));
// without strict
let result = scene.ucmd().arg("--check").arg("CHECKSUM").fails();
assert!(result.stderr_str().contains("Failed to open file: input"));
assert!(result
.stderr_str()
.contains("2 lines are improperly formatted\n"));
assert!(result
.stderr_str()
.contains("1 listed file could not be read\n"));
assert!(result.stdout_str().contains("f: OK\n"));
}