use std::fs;
use std::io::{self, BufRead};
use std::path::PathBuf;
use clap::Parser;
use sha1collisiondetection::*;
mod cli;
use cli::Opt;
fn main() -> io::Result<()> {
let mut opt = Opt::parse();
if cfg!(windows) && opt.text {
eprintln!("Opening files in text mode is not supported.");
std::process::exit(1);
}
if opt.files.is_empty() {
opt.files.push("-".into());
}
if opt.check {
check(opt)
} else {
compute(opt)
}
}
fn check(opt: Opt) -> io::Result<()> {
let mut ok = true;
for checkfile_name in opt.files.iter() {
ok &= check_file(&opt, checkfile_name).unwrap_or(false);
}
if ! ok {
std::process::exit(1);
}
Ok(())
}
fn check_file(opt: &Opt, checkfile_name: &PathBuf) -> io::Result<bool> {
let checkfile_name_str =
checkfile_name.as_os_str().to_string_lossy().to_string();
let mut stdin_read = false;
let mut improperly_formatted_lines = 0;
let mut open_or_read_failures = 0;
let mut mismatched_checksums = 0;
let mut collisions = 0;
let mut properly_formatted_lines = false;
let mut matched_checksums = false;
let source: Box<dyn io::Read> =
if checkfile_name.as_os_str().to_str().map(|s| s == "-")
.unwrap_or(false)
{
Box::new(io::stdin())
} else {
Box::new(fs::File::open(checkfile_name)?)
};
for (lineno, line) in io::BufReader::new(source).lines().enumerate() {
let mut line = line?;
let mut malformed = || {
if opt.warn {
eprintln!("{}: {}: improperly formatted SHA1 checksum line",
checkfile_name_str, lineno + 1);
}
if opt.strict {
std::process::exit(1);
}
improperly_formatted_lines += 1;
};
if line.starts_with("#") {
continue; }
let escaped = line.starts_with("\\");
if escaped {
line = line[1..].to_string();
}
let l = line.len();
let (name, expected_hex) =
if line.starts_with("SHA1 (") {
(&line[6..l - 44], &line[l - 40..])
} else if line.len() > 42 && &line[40..41] == " " {
let binary_indicator = &line[41..42];
if binary_indicator != " " && binary_indicator != "*" {
malformed();
}
(&line[42..], &line[..40])
} else {
malformed();
continue;
};
if ! expected_hex.chars().all(|c| c.is_ascii_hexdigit()) {
malformed();
}
properly_formatted_lines = true;
let mut expected = Output::default();
for (octet, hex) in expected.iter_mut().zip(
expected_hex.as_bytes().chunks(2)
.map(|chunk| std::str::from_utf8(chunk).unwrap()))
{
*octet = match u8::from_str_radix(hex, 16) {
Ok(v) => v,
Err(_) => {
malformed();
continue;
},
};
}
let f = if escaped {
unescape_filename(&name)
} else {
name.to_string()
};
let mut ctx = Sha1CD::default();
if f == "-" {
if stdin_read {
} else {
io::copy(&mut io::stdin(), &mut ctx)?;
stdin_read = true;
}
} else {
let mut file = match fs::File::open(&f) {
Ok(f) => f,
Err(e) => {
if e.kind() == io::ErrorKind::NotFound
&& opt.ignore_missing
{
continue;
}
if ! opt.status {
eprintln!("sha1sum: {:?}: {}", f, e);
println!("{}: FAILED open or read", name);
}
open_or_read_failures += 1;
continue;
},
};
io::copy(&mut file, &mut ctx)?;
};
let mut digest = Output::default();
if let Err(_) = ctx.finalize_into_dirty_cd(&mut digest) {
collisions += 1;
}
if f.contains("\n") {
print!("\\{}", f.replace("\n", "\\n"));
} else {
print!("{}", f);
}
if digest == expected {
matched_checksums = true;
println!(": OK");
} else {
mismatched_checksums += 1;
println!(": FAILED");
}
}
if ! properly_formatted_lines {
eprintln!("{}: no properly formatted SHA1 checksum lines found",
checkfile_name_str);
} else {
if ! opt.status {
if improperly_formatted_lines > 0 {
eprintln!("WARNING: {} line{} are improperly formatted",
improperly_formatted_lines,
if improperly_formatted_lines > 1 { "s" } else { "" },
);
}
if open_or_read_failures > 0 {
eprintln!("WARNING: {} listed file{} could not be read",
open_or_read_failures,
if open_or_read_failures > 1 { "s" } else { "" },
);
}
if mismatched_checksums > 0 {
eprintln!("WARNING: {} computed checksum{} did NOT match",
mismatched_checksums,
if mismatched_checksums > 1 { "s" } else { "" },
);
}
if collisions > 0 {
eprintln!("WARNING: {} collision{} were detected",
collisions,
if collisions > 1 { "s" } else { "" },
);
}
if opt.ignore_missing && ! matched_checksums {
eprintln!("{}: no file was verified", checkfile_name_str);
}
}
}
Ok(properly_formatted_lines
&& matched_checksums
&& mismatched_checksums == 0
&& open_or_read_failures == 0
&& (! opt.strict || improperly_formatted_lines == 0))
}
fn compute(opt: Opt) -> io::Result<()> {
let mut stdin_read = false;
for f in opt.files.iter() {
let mut ctx = Sha1CD::default();
if f.as_os_str().to_str().map(|s| s == "-").unwrap_or(false) {
if stdin_read {
} else {
io::copy(&mut io::stdin(), &mut ctx)?;
stdin_read = true;
}
} else {
io::copy(&mut fs::File::open(f)?, &mut ctx)?;
};
let mut digest = Output::default();
let r = ctx.finalize_into_dirty_cd(&mut digest);
let mut name = f.as_os_str().to_string_lossy().to_string();
if ! opt.zero {
if needs_escape(&name) {
print!("\\");
}
name = escape_filename(name);
}
if opt.tag {
print!("SHA1 ({}) = ", name);
if r.is_err() {
print!("*coll* ");
}
for b in digest {
print!("{:02x}", b);
}
} else {
for b in digest {
print!("{:02x}", b);
}
if r.is_ok() {
print!(" ");
if opt.binary {
print!("*");
} else {
print!(" ");
}
} else {
print!(" *coll* ");
}
print!("{}", name);
}
if opt.zero {
print!("\x00");
} else {
println!();
}
}
Ok(())
}
fn needs_escape<N: AsRef<str>>(n: N) -> bool {
n.as_ref().contains("\n")
|| n.as_ref().contains("\\")
}
fn escape_filename<N: AsRef<str>>(n: N) -> String {
n.as_ref().replace("\\", "\\\\").replace("\n", "\\n")
}
fn unescape_filename<N: AsRef<str>>(n: N) -> String {
n.as_ref().replace("\\n", "\n").replace("\\\\", "\\")
}