use std::io::{self, BufRead, BufReader, BufWriter, Write};
#[cfg(unix)]
use std::mem::ManuallyDrop;
#[cfg(unix)]
use std::os::unix::io::FromRawFd;
use std::path::Path;
use std::process;
use coreutils_rs::common::io_error_msg;
use coreutils_rs::hash::{self, HashAlgorithm};
const TOOL_NAME: &str = "sha512sum";
const SHA512_HEX_LEN: usize = 128;
struct Cli {
binary: bool,
check: bool,
tag: bool,
text: bool,
ignore_missing: bool,
quiet: bool,
status: bool,
strict: bool,
warn: bool,
zero: bool,
files: Vec<String>,
}
fn parse_args() -> Cli {
let mut cli = Cli {
binary: false,
check: false,
tag: false,
text: false,
ignore_missing: false,
quiet: false,
status: false,
strict: false,
warn: false,
zero: false,
files: Vec::new(),
};
let args = std::env::args_os().skip(1);
let mut saw_dashdash = false;
for arg in args {
let bytes = arg.as_encoded_bytes();
if saw_dashdash {
cli.files.push(arg.to_string_lossy().into_owned());
continue;
}
if bytes == b"--" {
saw_dashdash = true;
continue;
}
if bytes.starts_with(b"--") {
match bytes {
b"--binary" => cli.binary = true,
b"--check" => cli.check = true,
b"--tag" => cli.tag = true,
b"--text" => cli.text = true,
b"--ignore-missing" => cli.ignore_missing = true,
b"--quiet" => cli.quiet = true,
b"--status" => cli.status = true,
b"--strict" => cli.strict = true,
b"--warn" => cli.warn = true,
b"--zero" => cli.zero = true,
b"--help" => {
print!(
"Usage: {} [OPTION]... [FILE]...\n\
Print or check SHA512 (512-bit) checksums.\n\n\
With no FILE, or when FILE is -, read standard input.\n\n\
\x20 -b, --binary read in binary mode\n\
\x20 -c, --check read checksums from the FILEs and check them\n\
\x20 --tag create a BSD-style checksum\n\
\x20 -t, --text read in text mode (default)\n\
\x20 -z, --zero end each output line with NUL, not newline\n\n\
The following five options are useful only when verifying checksums:\n\
\x20 --ignore-missing don't fail or report status for missing files\n\
\x20 --quiet don't print OK for each successfully verified file\n\
\x20 --status don't output anything, status code shows success\n\
\x20 --strict exit non-zero for improperly formatted checksum lines\n\
\x20 -w, --warn warn about improperly formatted checksum lines\n\n\
\x20 --help display this help and exit\n\
\x20 --version output version information and exit\n",
TOOL_NAME
);
process::exit(0);
}
b"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, env!("CARGO_PKG_VERSION"));
process::exit(0);
}
_ => {
eprintln!(
"{}: unrecognized option '{}'",
TOOL_NAME,
arg.to_string_lossy()
);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
} else if bytes.len() > 1 && bytes[0] == b'-' {
for &b in &bytes[1..] {
match b {
b'b' => cli.binary = true,
b'c' => cli.check = true,
b't' => cli.text = true,
b'w' => cli.warn = true,
b'z' => cli.zero = true,
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, b as char);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
} else {
cli.files.push(arg.to_string_lossy().into_owned());
}
}
cli
}
fn needs_escape(name: &str) -> bool {
name.bytes().any(|b| b == b'\\' || b == b'\n')
}
fn escape_filename(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 8);
for b in name.bytes() {
match b {
b'\\' => out.push_str("\\\\"),
b'\n' => out.push_str("\\n"),
_ => out.push(b as char),
}
}
out
}
fn unescape_filename(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('\\') => out.push('\\'),
Some('n') => out.push('\n'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}
#[cfg(target_os = "linux")]
fn enlarge_pipes() {
const PIPE_SIZE: i32 = 8 * 1024 * 1024;
unsafe {
libc::fcntl(0, libc::F_SETPIPE_SZ, PIPE_SIZE);
libc::fcntl(1, libc::F_SETPIPE_SZ, PIPE_SIZE);
}
}
#[cfg(target_os = "linux")]
fn single_file_fast(path: &Path) -> ! {
let mut out_buf = [0u8; 4096];
match hash::hash_file_raw_to_buf(HashAlgorithm::Sha512, path, &mut out_buf) {
Ok(hex_len) => {
let name_bytes = {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes()
};
let mut pos = hex_len;
out_buf[pos] = b' ';
pos += 1;
out_buf[pos] = b' ';
pos += 1;
if pos + name_bytes.len() < out_buf.len() {
out_buf[pos..pos + name_bytes.len()].copy_from_slice(name_bytes);
pos += name_bytes.len();
out_buf[pos] = b'\n';
pos += 1;
unsafe {
libc::write(1, out_buf.as_ptr() as *const libc::c_void, pos as _);
}
} else {
let mut v = Vec::with_capacity(pos + name_bytes.len() + 1);
v.extend_from_slice(&out_buf[..pos]);
v.extend_from_slice(name_bytes);
v.push(b'\n');
unsafe {
libc::write(1, v.as_ptr() as *const libc::c_void, v.len() as _);
}
}
process::exit(0);
}
Err(e) => {
eprintln!(
"{}: {}: {}",
TOOL_NAME,
path.to_string_lossy(),
io_error_msg(&e)
);
process::exit(1);
}
}
}
fn main() {
coreutils_rs::common::reset_sigpipe();
#[cfg(target_os = "linux")]
{
let mut args = std::env::args_os();
let _ = args.next();
if let Some(arg) = args.next()
&& args.next().is_none()
{
let bytes = arg.as_encoded_bytes();
if !bytes.is_empty() && bytes[0] != b'-' {
single_file_fast(Path::new(&arg));
}
}
}
let cli = parse_args();
let algo = HashAlgorithm::Sha512;
if cli.tag && cli.check {
eprintln!(
"{}: the --tag option is meaningless when verifying checksums",
TOOL_NAME
);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
let files = if cli.files.is_empty() {
vec!["-".to_string()]
} else {
cli.files.clone()
};
#[cfg(target_os = "linux")]
if files.iter().any(|f| f == "-") {
enlarge_pipes();
}
#[cfg(unix)]
let mut raw = unsafe { ManuallyDrop::new(std::fs::File::from_raw_fd(1)) };
#[cfg(unix)]
let mut out = BufWriter::with_capacity(256 * 1024, &mut *raw);
#[cfg(not(unix))]
let stdout = io::stdout();
#[cfg(not(unix))]
let mut out = BufWriter::with_capacity(256 * 1024, stdout.lock());
let mut had_error = false;
if cli.check {
run_check_mode(&cli, algo, &files, &mut out, &mut had_error);
} else {
run_hash_mode(&cli, algo, &files, &mut out, &mut had_error);
}
let _ = out.flush();
if had_error {
process::exit(1);
}
}
fn run_hash_mode(
cli: &Cli,
algo: HashAlgorithm,
files: &[String],
out: &mut impl Write,
had_error: &mut bool,
) {
let has_stdin = files.iter().any(|f| f == "-");
if has_stdin || files.len() <= 1 {
for filename in files {
let hash_result = if filename == "-" {
hash::hash_stdin(algo)
} else {
hash::hash_file(algo, Path::new(filename))
};
match hash_result {
Ok(h) => {
let name = if filename == "-" {
"-"
} else {
filename.as_str()
};
write_output(out, cli, algo, &h, name);
}
Err(e) => {
let _ = out.flush();
eprintln!("{}: {}: {}", TOOL_NAME, filename, io_error_msg(&e));
*had_error = true;
}
}
}
} else {
let paths: Vec<_> = files.iter().map(|f| Path::new(f.as_str())).collect();
let results = hash::hash_files_auto(&paths, algo);
for (filename, result) in files.iter().zip(results) {
match result {
Ok(h) => {
write_output(out, cli, algo, &h, filename);
}
Err(e) => {
let _ = out.flush();
eprintln!("{}: {}: {}", TOOL_NAME, filename, io_error_msg(&e));
*had_error = true;
}
}
}
}
}
#[inline]
fn write_output(out: &mut impl Write, cli: &Cli, algo: HashAlgorithm, hash: &str, filename: &str) {
let binary = cli.binary || (!cli.text && cfg!(windows));
if cli.tag {
let _ = hash::write_hash_tag_line(out, algo.name(), hash, filename, cli.zero);
} else if !cli.zero && needs_escape(filename) {
let escaped = escape_filename(filename);
let _ = hash::write_hash_line(out, hash, &escaped, binary, cli.zero, true);
} else {
let _ = hash::write_hash_line(out, hash, filename, binary, cli.zero, false);
}
}
fn run_check_mode(
cli: &Cli,
algo: HashAlgorithm,
files: &[String],
out: &mut impl Write,
had_error: &mut bool,
) {
let mut _total_ok: usize = 0;
let mut total_mismatches: usize = 0;
let mut total_fmt_errors: usize = 0;
let mut total_read_errors: usize = 0;
for filename in files {
let reader: Box<dyn io::BufRead> = if filename == "-" {
Box::new(BufReader::new(io::stdin().lock()))
} else {
match std::fs::File::open(filename) {
Ok(f) => Box::new(BufReader::new(f)),
Err(e) => {
eprintln!("{}: {}: {}", TOOL_NAME, filename, io_error_msg(&e));
*had_error = true;
continue;
}
}
};
let display_name = if filename == "-" {
"standard input".to_string()
} else {
filename.clone()
};
let (file_ok, file_fail, file_fmt, file_read, file_ignored) =
check_one(cli, algo, reader, &display_name, out);
_total_ok += file_ok;
total_mismatches += file_fail;
total_fmt_errors += file_fmt;
total_read_errors += file_read;
if file_fail > 0 || file_read > 0 {
*had_error = true;
}
if cli.strict && file_fmt > 0 {
*had_error = true;
}
if file_ok == 0 && file_fail == 0 && file_read == 0 && file_ignored == 0 && file_fmt > 0 {
if !cli.status {
let _ = out.flush();
eprintln!(
"{}: {}: no properly formatted SHA512 checksum lines found",
TOOL_NAME, display_name
);
}
total_fmt_errors -= file_fmt;
*had_error = true;
}
if cli.ignore_missing && file_ok == 0 && file_fail == 0 && file_ignored > 0 {
if !cli.status {
let _ = out.flush();
eprintln!("{}: {}: no file was verified", TOOL_NAME, display_name);
}
*had_error = true;
}
}
let _ = out.flush();
if !cli.status {
if total_mismatches > 0 {
let checksum_word = if total_mismatches == 1 {
"computed checksum did NOT match"
} else {
"computed checksums did NOT match"
};
eprintln!(
"{}: WARNING: {} {}",
TOOL_NAME, total_mismatches, checksum_word
);
}
if total_read_errors > 0 {
let word = if total_read_errors == 1 {
"listed file could not be read"
} else {
"listed files could not be read"
};
eprintln!("{}: WARNING: {} {}", TOOL_NAME, total_read_errors, word);
}
if total_fmt_errors > 0 {
let line_word = if total_fmt_errors == 1 {
"line is"
} else {
"lines are"
};
eprintln!(
"{}: WARNING: {} {} improperly formatted",
TOOL_NAME, total_fmt_errors, line_word
);
}
}
}
fn check_one(
cli: &Cli,
algo: HashAlgorithm,
reader: Box<dyn BufRead>,
display_name: &str,
out: &mut impl Write,
) -> (usize, usize, usize, usize, usize) {
let mut ok_count: usize = 0;
let mut mismatch_count: usize = 0;
let mut format_errors: usize = 0;
let mut read_errors: usize = 0;
let mut ignored_missing: usize = 0;
let mut line_num: usize = 0;
for line_result in reader.lines() {
line_num += 1;
let line = match line_result {
Ok(l) => l,
Err(e) => {
eprintln!("{}: {}: {}", TOOL_NAME, display_name, io_error_msg(&e));
break;
}
};
let line = line.trim_end();
if line.is_empty() {
continue;
}
let line_content = line.strip_prefix('\\').unwrap_or(line);
let (expected_hash, parsed_filename) = match hash::parse_check_line(line_content) {
Some(v) => v,
None => {
format_errors += 1;
if cli.warn {
let _ = out.flush();
eprintln!(
"{}: {}: {}: improperly formatted SHA512 checksum line",
TOOL_NAME, display_name, line_num
);
}
continue;
}
};
if expected_hash.len() != SHA512_HEX_LEN
|| !expected_hash.bytes().all(|b| b.is_ascii_hexdigit())
{
format_errors += 1;
if cli.warn {
let _ = out.flush();
eprintln!(
"{}: {}: {}: improperly formatted SHA512 checksum line",
TOOL_NAME, display_name, line_num
);
}
continue;
}
let check_filename = if line.starts_with('\\') {
unescape_filename(parsed_filename)
} else {
parsed_filename.to_string()
};
let actual = match hash::hash_file(algo, Path::new(&check_filename)) {
Ok(h) => h,
Err(e) => {
if cli.ignore_missing && e.kind() == io::ErrorKind::NotFound {
ignored_missing += 1;
continue;
}
read_errors += 1;
if !cli.status {
let _ = out.flush();
eprintln!("{}: {}: {}", TOOL_NAME, check_filename, io_error_msg(&e));
let _ = writeln!(out, "{}: FAILED open or read", check_filename);
}
continue;
}
};
if actual.eq_ignore_ascii_case(expected_hash) {
ok_count += 1;
if !cli.quiet && !cli.status {
let _ = writeln!(out, "{}: OK", check_filename);
}
} else {
mismatch_count += 1;
if !cli.status {
let _ = writeln!(out, "{}: FAILED", check_filename);
}
}
}
(
ok_count,
mismatch_count,
format_errors,
read_errors,
ignored_missing,
)
}
#[cfg(test)]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fsha512sum");
Command::new(path)
}
#[cfg(unix)]
#[test]
fn test_hash_stdin() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"hello\n").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout = stdout.trim();
assert!(stdout.contains(" -"), "Should contain filename marker");
}
#[test]
fn test_hash_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "hello\n").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("test.txt"));
}
#[test]
fn test_check_mode() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "hello\n").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
let checksum_line = String::from_utf8_lossy(&output.stdout);
let checksums = dir.path().join("checksums.txt");
std::fs::write(&checksums, checksum_line.as_ref()).unwrap();
let output = cmd()
.args(["--check", checksums.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("OK"));
}
#[test]
fn test_known_empty_hash() {
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
drop(child.stdin.take().unwrap());
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"));
}
#[test]
fn test_nonexistent_file() {
let output = cmd().arg("/nonexistent_xyz_sha512").output().unwrap();
assert!(!output.status.success());
}
#[test]
fn test_check_tampered() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "original\n").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
let checksums = dir.path().join("sums.txt");
std::fs::write(&checksums, String::from_utf8_lossy(&output.stdout).as_ref()).unwrap();
std::fs::write(&file, "tampered\n").unwrap();
let output = cmd()
.args(["--check", checksums.to_str().unwrap()])
.output()
.unwrap();
assert!(!output.status.success());
}
#[test]
fn test_tag_format() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "x").unwrap();
let output = cmd()
.args(["--tag", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("SHA512"));
}
#[test]
fn test_multiple_files() {
let dir = tempfile::tempdir().unwrap();
let f1 = dir.path().join("a.txt");
let f2 = dir.path().join("b.txt");
std::fs::write(&f1, "aaa\n").unwrap();
std::fs::write(&f2, "bbb\n").unwrap();
let output = cmd()
.args([f1.to_str().unwrap(), f2.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout).lines().count(), 2);
}
#[cfg(unix)]
#[test]
fn test_hash_length() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "test").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout = stdout.trim();
let hash_part: &str = stdout.split_whitespace().next().unwrap();
assert_eq!(hash_part.len(), 128); }
}