use clap::builder::ValueParser;
use clap::{crate_version, value_parser, Arg, ArgAction, Command};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{self, stdin, stdout, BufReader, Read, Write};
use std::iter;
use std::path::Path;
use uucore::checksum::{
calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation,
ChecksumError, ChecksumOptions, ChecksumVerbose, ALGORITHM_OPTIONS_BLAKE2B,
ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV,
SUPPORTED_ALGORITHMS,
};
use uucore::{
encoding,
error::{FromIo, UResult, USimpleError},
format_usage, help_about, help_section, help_usage,
line_ending::LineEnding,
os_str_as_bytes, show,
sum::Digest,
};
const USAGE: &str = help_usage!("cksum.md");
const ABOUT: &str = help_about!("cksum.md");
const AFTER_HELP: &str = help_section!("after help", "cksum.md");
#[derive(Debug, PartialEq)]
enum OutputFormat {
Hexadecimal,
Raw,
Base64,
}
struct Options {
algo_name: &'static str,
digest: Box<dyn Digest + 'static>,
output_bits: usize,
tag: bool, length: Option<usize>,
output_format: OutputFormat,
asterisk: bool, line_ending: LineEnding,
}
#[allow(clippy::cognitive_complexity)]
fn cksum<'a, I>(mut options: Options, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
let files: Vec<_> = files.collect();
if options.output_format == OutputFormat::Raw && files.len() > 1 {
return Err(Box::new(ChecksumError::RawMultipleFiles));
}
for filename in files {
let filename = Path::new(filename);
let stdin_buf;
let file_buf;
let not_file = filename == OsStr::new("-");
let mut file = BufReader::new(if not_file {
stdin_buf = stdin();
Box::new(stdin_buf) as Box<dyn Read>
} else if filename.is_dir() {
Box::new(BufReader::new(io::empty())) as Box<dyn Read>
} else {
file_buf = match File::open(filename) {
Ok(file) => file,
Err(err) => {
show!(err.map_err_context(|| filename.to_string_lossy().to_string()));
continue;
}
};
Box::new(file_buf) as Box<dyn Read>
});
if filename.is_dir() {
show!(USimpleError::new(
1,
format!("{}: Is a directory", filename.display())
));
continue;
}
let (sum_hex, sz) =
digest_reader(&mut options.digest, &mut file, false, options.output_bits)
.map_err_context(|| "failed to read input".to_string())?;
let sum = match options.output_format {
OutputFormat::Raw => {
let bytes = match options.algo_name {
ALGORITHM_OPTIONS_CRC => sum_hex.parse::<u32>().unwrap().to_be_bytes().to_vec(),
ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => {
sum_hex.parse::<u16>().unwrap().to_be_bytes().to_vec()
}
_ => hex::decode(sum_hex).unwrap(),
};
stdout().write_all(&bytes)?;
return Ok(());
}
OutputFormat::Hexadecimal => sum_hex,
OutputFormat::Base64 => match options.algo_name {
ALGORITHM_OPTIONS_CRC
| ALGORITHM_OPTIONS_CRC32B
| ALGORITHM_OPTIONS_SYSV
| ALGORITHM_OPTIONS_BSD => sum_hex,
_ => encoding::for_cksum::BASE64.encode(&hex::decode(sum_hex).unwrap()),
},
};
let bsd_width = 5;
let (before_filename, should_print_filename, after_filename) = match options.algo_name {
ALGORITHM_OPTIONS_SYSV => (
format!(
"{} {}{}",
sum.parse::<u16>().unwrap(),
sz.div_ceil(options.output_bits),
if not_file { "" } else { " " }
),
!not_file,
String::new(),
),
ALGORITHM_OPTIONS_BSD => (
format!(
"{:0bsd_width$} {:bsd_width$}{}",
sum.parse::<u16>().unwrap(),
sz.div_ceil(options.output_bits),
if not_file { "" } else { " " }
),
!not_file,
String::new(),
),
ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => (
format!("{sum} {sz}{}", if not_file { "" } else { " " }),
!not_file,
String::new(),
),
ALGORITHM_OPTIONS_BLAKE2B if options.tag => {
(
if let Some(length) = options.length {
format!("BLAKE2b-{} (", length * 8)
} else {
"BLAKE2b (".to_owned()
},
true,
format!(") = {sum}"),
)
}
_ => {
if options.tag {
(
format!("{} (", options.algo_name.to_ascii_uppercase()),
true,
format!(") = {sum}"),
)
} else {
let prefix = if options.asterisk { "*" } else { " " };
(format!("{sum} {prefix}"), true, String::new())
}
}
};
print!("{before_filename}");
if should_print_filename {
let _dropped_result = stdout().write_all(os_str_as_bytes(filename.as_os_str())?);
}
print!("{after_filename}{}", options.line_ending);
}
Ok(())
}
mod options {
pub const ALGORITHM: &str = "algorithm";
pub const FILE: &str = "file";
pub const UNTAGGED: &str = "untagged";
pub const TAG: &str = "tag";
pub const LENGTH: &str = "length";
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";
pub const STATUS: &str = "status";
pub const WARN: &str = "warn";
pub const IGNORE_MISSING: &str = "ignore-missing";
pub const QUIET: &str = "quiet";
pub const ZERO: &str = "zero";
}
fn handle_tag_text_binary_flags<S: AsRef<OsStr>>(
args: impl Iterator<Item = S>,
) -> UResult<(bool, bool)> {
let mut tag = true;
let mut binary = false;
for arg in args {
let arg = arg.as_ref();
if arg == "-b" || arg == "--binary" {
binary = true;
} else if arg == "--tag" {
tag = true;
binary = false;
} else if arg == "--untagged" {
tag = false;
}
}
Ok((tag, !tag && binary))
}
#[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 => {
if check {
""
} else {
ALGORITHM_OPTIONS_CRC
}
}
};
let input_length = matches.get_one::<usize>(options::LENGTH);
let length = match input_length {
Some(length) => {
if algo_name == ALGORITHM_OPTIONS_BLAKE2B {
calculate_blake2b_length(*length)?
} else {
return Err(ChecksumError::LengthOnlyForBlake2b.into());
}
}
None => None,
};
if ["bsd", "crc", "sysv", "crc32b"].contains(&algo_name) && check {
return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into());
}
if check {
let text_flag = matches.get_flag(options::TEXT);
let binary_flag = matches.get_flag(options::BINARY);
let strict = matches.get_flag(options::STRICT);
let status = matches.get_flag(options::STATUS);
let warn = matches.get_flag(options::WARN);
let ignore_missing = matches.get_flag(options::IGNORE_MISSING);
let quiet = matches.get_flag(options::QUIET);
let tag = matches.get_flag(options::TAG);
if tag || binary_flag || text_flag {
return Err(ChecksumError::BinaryTextConflict.into());
}
let algo_option = if algo_name.is_empty() {
None
} else {
Some(algo_name)
};
let files = matches.get_many::<OsString>(options::FILE).map_or_else(
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|files| files.map(OsStr::new).collect::<Vec<_>>(),
);
let verbose = ChecksumVerbose::new(status, quiet, warn);
let opts = ChecksumOptions {
binary: binary_flag,
ignore_missing,
strict,
verbose,
};
return perform_checksum_validation(files.iter().copied(), algo_option, length, opts);
}
let (tag, asterisk) = handle_tag_text_binary_flags(std::env::args_os())?;
let algo = detect_algo(algo_name, length)?;
let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO));
let output_format = if matches.get_flag(options::RAW) {
OutputFormat::Raw
} else if matches.get_flag(options::BASE64) {
OutputFormat::Base64
} else {
OutputFormat::Hexadecimal
};
let opts = Options {
algo_name: algo.name,
digest: (algo.create_fn)(),
output_bits: algo.bits,
length,
tag,
output_format,
asterisk,
line_ending,
};
match matches.get_many::<OsString>(options::FILE) {
Some(files) => cksum(opts, files.map(OsStr::new))?,
None => cksum(opts, iter::once(OsStr::new("-")))?,
};
Ok(())
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.args_override_self(true)
.arg(
Arg::new(options::FILE)
.hide(true)
.action(clap::ArgAction::Append)
.value_parser(ValueParser::os_string())
.value_hint(clap::ValueHint::FilePath),
)
.arg(
Arg::new(options::ALGORITHM)
.long(options::ALGORITHM)
.short('a')
.help("select the digest type to use. See DIGEST below")
.value_name("ALGORITHM")
.value_parser(SUPPORTED_ALGORITHMS),
)
.arg(
Arg::new(options::UNTAGGED)
.long(options::UNTAGGED)
.help("create a reversed style checksum, without digest type")
.action(ArgAction::SetTrue)
.overrides_with(options::TAG),
)
.arg(
Arg::new(options::TAG)
.long(options::TAG)
.help("create a BSD style checksum, undo --untagged (default)")
.action(ArgAction::SetTrue)
.overrides_with(options::UNTAGGED),
)
.arg(
Arg::new(options::LENGTH)
.long(options::LENGTH)
.value_parser(value_parser!(usize))
.short('l')
.help(
"digest length in bits; must not exceed the max for the blake2 algorithm \
and must be a multiple of 8",
)
.action(ArgAction::Set),
)
.arg(
Arg::new(options::RAW)
.long(options::RAW)
.help("emit a raw binary digest, not hexadecimal")
.action(ArgAction::SetTrue),
)
.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')
.long(options::CHECK)
.help("read hashsums from the FILEs and check them")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::BASE64)
.long(options::BASE64)
.help("emit a base64 digest, not hexadecimal")
.action(ArgAction::SetTrue)
.conflicts_with(options::RAW),
)
.arg(
Arg::new(options::TEXT)
.long(options::TEXT)
.short('t')
.hide(true)
.overrides_with(options::BINARY)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::BINARY)
.long(options::BINARY)
.short('b')
.hide(true)
.overrides_with(options::TEXT)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::WARN)
.short('w')
.long("warn")
.help("warn about improperly formatted checksum lines")
.action(ArgAction::SetTrue)
.overrides_with_all([options::STATUS, options::QUIET]),
)
.arg(
Arg::new(options::STATUS)
.long("status")
.help("don't output anything, status code shows success")
.action(ArgAction::SetTrue)
.overrides_with_all([options::WARN, options::QUIET]),
)
.arg(
Arg::new(options::QUIET)
.long(options::QUIET)
.help("don't print OK for each successfully verified file")
.action(ArgAction::SetTrue)
.overrides_with_all([options::WARN, options::STATUS]),
)
.arg(
Arg::new(options::IGNORE_MISSING)
.long(options::IGNORE_MISSING)
.help("don't fail or report status for missing files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ZERO)
.long(options::ZERO)
.short('z')
.help(
"end each output line with NUL, not newline,\n and disable file name escaping",
)
.action(ArgAction::SetTrue),
)
.after_help(AFTER_HELP)
}
#[cfg(test)]
mod tests {
use crate::calculate_blake2b_length;
#[test]
fn test_calculate_length() {
assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32));
assert_eq!(calculate_blake2b_length(512).unwrap(), None);
assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32));
calculate_blake2b_length(255).unwrap_err();
calculate_blake2b_length(33).unwrap_err();
calculate_blake2b_length(513).unwrap_err();
}
}