Skip to content

Commit

Permalink
cksum: implement check & strict
Browse files Browse the repository at this point in the history
  • Loading branch information
sylvestre committed May 10, 2024
1 parent 2f0cc9d commit 6b8ce37
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 27 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/uu/cksum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
184 changes: 157 additions & 27 deletions src/uu/cksum/src/cksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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";
}
Expand Down Expand Up @@ -357,20 +364,16 @@ 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(
io::ErrorKind::InvalidInput,
"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.
Expand All @@ -391,47 +394,146 @@ 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();
let had_reset = had_reset(&args);

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 success = true;
let mut properly_formatted = false;
let mut bad_format = 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!(

Check warning on line 432 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L431-L432

Added lines #L431 - L432 were not covered by tests
"Failed to open file: {}",
filename_input.to_string_lossy()
)));
return Err(io::Error::new(io::ErrorKind::Other, "Failed to open file").into());

Check warning on line 436 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L434-L436

Added lines #L434 - L436 were not covered by tests
}
}
};
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

Check warning on line 457 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L457

Added line #L457 was not covered by tests
} else {
match File::open(filename_to_check) {
Ok(f) => Box::new(f),
Err(err) => {
show!(err.map_err_context(|| format!(

Check warning on line 462 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L461-L462

Added lines #L461 - L462 were not covered by tests
"Failed to open file: {}",
filename_to_check
)));
return Err(io::Error::new(
io::ErrorKind::Other,

Check warning on line 467 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L465-L467

Added lines #L465 - L467 were not covered by tests
"Failed to open file",
)
.into());
}
}
};
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())?;

Check warning on line 477 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L477

Added line #L477 was not covered by tests
if expected_checksum == calculated_checksum {
println!("{}: OK", filename_to_check);
} else {
println!("{}: FAILED", filename_to_check);
success = false;
}
} else {
bad_format += 1;
}
}
}

Check warning on line 488 in src/uu/cksum/src/cksum.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L488

Added line #L488 was not covered by tests
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))

// 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 => {}
};

// strict means that we should have an exit code.
if strict && bad_format > 0 {
set_exit_code(1);
}

if !success {
return Err(io::Error::new(io::ErrorKind::Other, "Checksum verification failed").into());
}
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,
};

let input_length = matches.get_one::<usize>(options::LENGTH);

let length = match input_length {
Some(length) => calculate_length(algo_name, *length)?,
None => None,
None => {
if check {
// if we are doing a --check, we should not default to crc
""
} else {
ALGORITHM_OPTIONS_CRC
}
}
};

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,
Expand All @@ -440,6 +542,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) {
Expand Down Expand Up @@ -532,12 +662,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')
Expand Down Expand Up @@ -577,8 +707,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() {
Expand Down
79 changes: 79 additions & 0 deletions tests/by-util/test_cksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,3 +663,82 @@ 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");
// inject invalid content
/* at.append("CHECKSUM", "again incorrect data");
at.append("CHECKSUM", "again incorrect data");
scene
.ucmd()
.arg("--check")
.arg("--strict")
.arg("CHECKSUM")
.fails()
.stdout_contains("f: OK\nf: OK\nf: OK\nf: OK\n")
.stderr_contains("2 lines");*/
}

0 comments on commit 6b8ce37

Please sign in to comment.