Skip to content

Commit

Permalink
cksum: implement --check
Browse files Browse the repository at this point in the history
  • Loading branch information
tertsdiepraam authored and sylvestre committed Dec 25, 2023
1 parent 2f7b963 commit 13002d6
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 20 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 @@ -16,6 +16,7 @@ path = "src/cksum.rs"

[dependencies]
clap = { workspace = true }
regex = { workspace = true }
uucore = { workspace = true, features = ["sum"] }
hex = { workspace = true }

Expand Down
248 changes: 228 additions & 20 deletions src/uu/cksum/src/cksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
// spell-checker:ignore (ToDO) fname, algo
use clap::{crate_version, Arg, ArgAction, Command};
use hex::encode;
use regex::{Captures, Regex};
use std::error::Error;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{self, stdin, BufReader, Read};
use std::io::{self, stdin, BufRead, BufReader, Read};
use std::iter;
use std::num::ParseIntError;
use std::path::Path;
use uucore::display::Quotable;
use uucore::error::UError;
use uucore::show_warning;
use uucore::{
error::{FromIo, UResult},
format_usage, help_about, help_section, help_usage,
Expand Down Expand Up @@ -62,6 +67,22 @@ mod options {

const BINARY_FLAG_DEFAULT: bool = cfg!(windows);

#[derive(Debug)]

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L70

Added line #L70 was not covered by tests
enum CksumError {
InvalidFormat,
}

impl Error for CksumError {}
impl UError for CksumError {}

impl std::fmt::Display for CksumError {
fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::InvalidFormat => Ok(()),
}
}
}

fn detect_algo(program: &str) -> (&'static str, Box<dyn Digest + 'static>, usize) {
use algorithm::*;
match program {
Expand All @@ -84,7 +105,6 @@ struct Options {
// cksum
algo_name: &'static str,
digest: Box<dyn Digest + 'static>,
untagged: bool,
output_bits: usize,

// common
Expand All @@ -95,7 +115,8 @@ struct Options {
quiet: bool,
strict: bool,
warn: bool,
zero: bool,
// zero is unimplemented
_zero: bool,
}

/// Calculate checksum
Expand All @@ -105,7 +126,195 @@ struct Options {
/// * `options` - CLI options for the assigning checksum algorithm
/// * `files` - A iterator of OsStr which is a bunch of files that are using for calculating checksum
#[allow(clippy::cognitive_complexity)]
fn cksum<'a, I>(mut options: Options, files: I) -> UResult<()>
fn cksum<'a, I>(options: Options, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
if options.check {
cksum_check(options, files)
} else {
cksum_print(options, files)
}
}

/// Creates a Regex for parsing lines based on the given format.
/// The default value of `gnu_re` created with this function has to be recreated
/// after the initial line has been parsed, as this line dictates the format
/// for the rest of them, and mixing of formats is disallowed.
fn gnu_re_template(bytes_marker: &str, format_marker: &str) -> Regex {
Regex::new(&format!(
r"^(?P<digest>[a-fA-F0-9]{bytes_marker}) {format_marker}(?P<fileName>.*)"
))
.expect("internal error: invalid regex")
}

fn handle_captures(
caps: &Captures,
bytes_marker: &str,
bsd_reversed: &mut Option<bool>,
gnu_re: &mut Regex,
) -> (String, String, bool) {
if bsd_reversed.is_none() {
let is_bsd_reversed = caps.name("binary").is_none();
let format_marker = if is_bsd_reversed {
""

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L160

Added line #L160 was not covered by tests
} else {
r"(?P<binary>[ \*])"
}
.to_string();

*bsd_reversed = Some(is_bsd_reversed);
*gnu_re = gnu_re_template(bytes_marker, &format_marker);
}

(
caps.name("fileName").unwrap().as_str().to_string(),
caps.name("digest").unwrap().as_str().to_ascii_lowercase(),
if *bsd_reversed == Some(false) {
caps.name("binary").unwrap().as_str() == "*"
} else {
false

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L176

Added line #L176 was not covered by tests
},
)
}

fn cksum_check<'a, I>(mut options: Options, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
// Set up Regexes for line validation and parsing

Check failure on line 185 in src/uu/cksum/src/cksum.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: Unknown word (Regexes) (file:'src/uu/cksum/src/cksum.rs', line:185)
//
// First, we compute the number of bytes we expect to be in
// the digest string. If the algorithm has a variable number
// of output bits, then we use the `+` modifier in the
// regular expression, otherwise we use the `{n}` modifier,
// where `n` is the number of bytes.
let bytes = options.digest.output_bits() / 4;
let bytes_marker = if bytes > 0 {
format!("{{{bytes}}}")
} else {
"+".to_string()

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L196

Added line #L196 was not covered by tests
};

// BSD reversed mode format is similar to the default mode, but doesn’t use
// a character to distinguish binary and text modes.
let mut bsd_reversed = None;

let mut gnu_re = gnu_re_template(&bytes_marker, r"(?P<binary>[ \*])?");
let bsd_re = Regex::new(&format!(
r"^{algorithm} \((?P<fileName>.*)\) = (?P<digest>[a-fA-F0-9]{digest_size})",
algorithm = options.algo_name,
digest_size = bytes_marker,
))
.expect("internal error: invalid regex");

// Keep track of the number of errors to report at the end
let mut num_bad_format_errors = 0;
let mut num_failed_checksums = 0;
let mut num_failed_to_open = 0;

for filename in files {
let buffer = open_file(filename)?;
for (i, maybe_line) in buffer.lines().enumerate() {
let line = match maybe_line {
Ok(l) => l,
Err(e) => return Err(e.map_err_context(|| "failed to read file".to_string())),

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L221

Added line #L221 was not covered by tests
};
let (ck_filename, sum, binary_check) = match gnu_re.captures(&line) {
Some(caps) => handle_captures(&caps, &bytes_marker, &mut bsd_reversed, &mut gnu_re),
None => match bsd_re.captures(&line) {
Some(caps) => (
caps.name("fileName").unwrap().as_str().to_string(),
caps.name("digest").unwrap().as_str().to_ascii_lowercase(),

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L226-L228

Added lines #L226 - L228 were not covered by tests
true,
),

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L230

Added line #L230 was not covered by tests
None => {
num_bad_format_errors += 1;
if options.strict {
return Err(CksumError::InvalidFormat.into());
}
if options.warn {
show_warning!(

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L237

Added line #L237 was not covered by tests
"{}: {}: improperly formatted {} checksum line",
filename.maybe_quote(),
i + 1,

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L239-L240

Added lines #L239 - L240 were not covered by tests
options.algo_name
);
}
continue;
}
},
};
let f = match File::open(ck_filename.clone()) {
Err(_) => {
num_failed_to_open += 1;
println!(

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L250-L251

Added lines #L250 - L251 were not covered by tests
"{}: {}: No such file or directory",
uucore::util_name(),

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L253

Added line #L253 was not covered by tests
ck_filename
);
println!("{ck_filename}: FAILED open or read");

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L256

Added line #L256 was not covered by tests
continue;
}
Ok(file) => file,
};
let mut ckf = BufReader::new(Box::new(f) as Box<dyn Read>);
let real_sum = digest_read(
&mut options.digest,
&mut ckf,
binary_check,
options.output_bits,
)
.map_err_context(|| "failed to read input".to_string())?

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L268

Added line #L268 was not covered by tests
.0
.to_ascii_lowercase();

// FIXME: Filenames with newlines should be treated specially.
// GNU appears to replace newlines by \n and backslashes by
// \\ and prepend a backslash (to the hash or filename) if it did
// this escaping.
// Different sorts of output (checking vs outputting hashes) may
// handle this differently. Compare carefully to GNU.
// If you can, try to preserve invalid unicode using OsStr(ing)Ext
// and display it using uucore::display::print_verbatim(). This is
// easier (and more important) on Unix than on Windows.
if sum == real_sum {
if !options.quiet {
println!("{ck_filename}: OK");
}
} else {
if !options.status {
println!("{ck_filename}: FAILED");

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L287

Added line #L287 was not covered by tests
}
num_failed_checksums += 1;

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L289

Added line #L289 was not covered by tests
}
}
}

if !options.status {
match num_bad_format_errors {
0 => {}
1 => show_warning!("1 line is improperly formatted"),
_ => show_warning!("{} lines are improperly formatted", num_bad_format_errors),
}
match num_failed_checksums {
0 => {}
1 => show_warning!("WARNING: 1 computed checksum did NOT match"),
_ => show_warning!(

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L302-L303

Added lines #L302 - L303 were not covered by tests
"WARNING: {} computed checksum did NOT match",
num_failed_checksums
),
}
match num_failed_to_open {
0 => {}
1 => show_warning!("1 listed file could not be read"),
_ => show_warning!("{} listed file could not be read", num_failed_to_open),

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

View check run for this annotation

Codecov / codecov/patch

src/uu/cksum/src/cksum.rs#L310-L311

Added lines #L310 - L311 were not covered by tests
}
}
Ok(())
}

fn cksum_print<'a, I>(mut options: Options, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
Expand Down Expand Up @@ -148,39 +357,36 @@ where
),
(algorithm::CRC, true) => println!("{sum} {sz}"),
(algorithm::CRC, false) => println!("{sum} {sz} {}", path.display()),
(algorithm::BLAKE2B, _) if !options.untagged => {
(algorithm::BLAKE2B, _) if options.tag => {
println!("BLAKE2b ({}) = {sum}", path.display());
}
_ => {
if options.untagged {
println!("{sum} {}", path.display());
} else {
if options.tag {
println!(
"{} ({}) = {sum}",
options.algo_name.to_ascii_uppercase(),
path.display()
);
} else {
println!("{sum} {}", path.display());
}
}
}
};
}

Ok(())
}

fn open_file(filename: &OsStr) -> UResult<BufReader<Box<dyn Read>>> {
let stdin_buf;
let file_buf;
let is_stdin = filename == OsStr::new("-");

let path = Path::new(filename);
let reader = if is_stdin {
stdin_buf = stdin();
let stdin_buf = stdin();
Box::new(stdin_buf) as Box<dyn Read>
} else if path.is_dir() {
Box::new(BufReader::new(io::empty())) as Box<dyn Read>
} else {
file_buf =
let file_buf =
File::open(filename).map_err_context(|| filename.to_str().unwrap().to_string())?;
Box::new(file_buf) as Box<dyn Read>
};
Expand Down Expand Up @@ -233,17 +439,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {

let (algo_name, digest, output_bits) = detect_algo(algo_name);

let untagged = matches.get_flag(options::UNTAGGED);

// TODO: This is not supported by GNU. It is added here so we can use cksum
// as a base for the specialized utils, but it should ultimately be hidden
// on cksum itself.
let binary = if matches.get_flag(options::BINARY) {
true
} else if matches.get_flag(options::TEXT) {
false
} else {
BINARY_FLAG_DEFAULT
};

let check = matches.get_flag(options::CHECK);
let tag = matches.get_flag(options::TAG);
let tag = matches.get_flag(options::TAG) || !matches.get_flag(options::UNTAGGED);
let status = matches.get_flag(options::STATUS);
let quiet = matches.get_flag(options::QUIET) || status;
let strict = matches.get_flag(options::STRICT);
Expand All @@ -254,15 +462,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
algo_name,
digest,
output_bits,
untagged,
binary,
check,
tag,
status,
quiet,
strict,
warn,
zero,
_zero: zero,
};

match matches.get_many::<String>(options::FILE) {
Expand Down Expand Up @@ -391,7 +598,8 @@ pub fn uu_app() -> Command {
Arg::new(options::UNTAGGED)
.long(options::UNTAGGED)
.help("create a reversed style checksum, without digest type")
.action(ArgAction::SetTrue),
.action(ArgAction::SetTrue)
.overrides_with(options::TAG),
)
.args(common_args())
.arg(length_arg())
Expand Down

0 comments on commit 13002d6

Please sign in to comment.