use clap::Parser; use std::collections::{HashMap, VecDeque}; use std::fs::{self, read_to_string, File}; use std::path::PathBuf; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::{self, available_parallelism, JoinHandle}; mod common; mod hashers; const ALGORITHMS: [&'static str; 37] = [ "ascon", "belt", "blake2b512", "blake2s256", "fsb160", "fsb224", "fsb256", "fsb384", "fsb512", "groestl224", "groestl256", "groestl384", "groestl512", "jh224", "jh256", "jh384", "jh512", "ripemd128", "ripemd160", "ripemd256", "ripemd320", "sha224", "sha256", "sha384", "sha512", "sha3_224", "sha3_256", "sha3_384", "sha3_512", "shabal192", "shabal224", "shabal256", "shabal384", "shabal512", "sm3", "tiger", "whirlpool", ]; const UNSECURE_ALGORITHMS: [&'static str; 7] = [ "gost94", "md2", "md4", "md5", "sha1", "streebog256", "streebog512", ]; #[derive(Parser)] #[command(name = "psha")] #[command(version = option_env!("CARGO_PKG_VERSION"))] #[command(about = "A parallel checksum tool for various algorithms")] #[command(long_about = None)] struct Args { #[arg( short, long, help = "Use at most this number of threads, 0 means as many as there are processor cores", default_value = "0" )] threads: usize, #[arg( short, long, help = "Enable debug output (thread info, algorithm used/detected)" )] debug: bool, #[arg( short, long, help = "Specify an algorithm for hashing", default_value = "sha256", value_parser = { let mut cleaned: Vec<&str> = vec![]; for i in ALGORITHMS { cleaned.push(i); } for i in UNSECURE_ALGORITHMS { cleaned.push(i); } clap::builder::PossibleValuesParser::new(Vec::from(cleaned)) } )] algorithm: String, #[arg( short = 'f', long, help = "Show canonicalized (relative paths converted to absolute) file paths" )] canonicalize: bool, #[arg( short = 'c', long, help = "Read checksums from the file(s) and verify them" )] check: Vec, #[arg( short = 'q', long, help = "(only used with -c) Only print checksums that fail; do not print OK for files that are successful" )] quiet: bool, #[arg(trailing_var_arg = true)] files: Vec, } struct ThreadInfo { debug: bool, quiet: bool, thread_id: usize, filenames: Arc>>, algorithm: Arc, hash_map: Option>>>, file_errors: Arc, hash_errors: Arc, } fn hash(info: ThreadInfo) -> Result<(), String> { loop { let filename = match info.filenames.lock().unwrap().pop_front() { Some(f) => f, None => break, }; if !info.quiet && info.debug { common::debug(format!( "thread {} is hashing file '{}'", info.thread_id, filename.as_path().display() )); } if filename.is_dir() { common::error(format!("{}: Is a directory", filename.as_path().display())); info.file_errors.fetch_add(1, Ordering::SeqCst); continue; } let file = match File::open(&filename) { Err(e) => { println!("{}: FAILED ({})", filename.as_path().display(), e); info.file_errors.fetch_add(1, Ordering::SeqCst); continue; }, Ok(f) => f, }; let res = match &*info.algorithm.as_str() { "ascon" => hashers::hash_ascon(file), "belt" => hashers::hash_belt(file), "blake2b512" => hashers::hash_blake2b512(file), "blake2s256" => hashers::hash_blake2s256(file), "fsb160" => hashers::hash_fsb160(file), "fsb224" => hashers::hash_fsb224(file), "fsb256" => hashers::hash_fsb256(file), "fsb384" => hashers::hash_fsb384(file), "fsb512" => hashers::hash_fsb512(file), "gost94" => hashers::hash_gost94(file), "groestl224" => hashers::hash_groestl224(file), "groestl256" => hashers::hash_groestl256(file), "groestl384" => hashers::hash_groestl384(file), "groestl512" => hashers::hash_groestl512(file), "jh224" => hashers::hash_jh224(file), "jh256" => hashers::hash_jh256(file), "jh384" => hashers::hash_jh384(file), "jh512" => hashers::hash_jh512(file), "md2" => hashers::hash_md2(file), "md4" => hashers::hash_md4(file), "md5" => hashers::hash_md5(file), "ripemd128" => hashers::hash_ripemd128(file), "ripemd160" => hashers::hash_ripemd160(file), "ripemd256" => hashers::hash_ripemd256(file), "ripemd320" => hashers::hash_ripemd320(file), "sha1" => hashers::hash_sha1(file), "sha224" => hashers::hash_sha224(file), "sha256" => hashers::hash_sha256(file), "sha384" => hashers::hash_sha384(file), "sha512" => hashers::hash_sha512(file), "sha3_224" => hashers::hash_sha3_224(file), "sha3_256" => hashers::hash_sha3_256(file), "sha3_384" => hashers::hash_sha3_384(file), "sha3_512" => hashers::hash_sha3_512(file), "shabal192" => hashers::hash_shabal192(file), "shabal224" => hashers::hash_shabal224(file), "shabal256" => hashers::hash_shabal256(file), "shabal384" => hashers::hash_shabal384(file), "shabal512" => hashers::hash_shabal512(file), "sm3" => hashers::hash_sm3(file), "streebog256" => hashers::hash_streebog256(file), "streebog512" => hashers::hash_streebog512(file), "tiger" => hashers::hash_tiger(file), "whirlpool" => hashers::hash_whirlpool(file), _ => panic!("Somehow did not pass a supported algorithm"), }; match &info.hash_map { Some(h) => { if h.lock().unwrap()[&filename] == res { if !info.quiet { println!("{}: OK", filename.as_path().display()); } } else { println!("{}: FAILED", filename.as_path().display()); info.hash_errors.fetch_add(1, Ordering::SeqCst); } }, None => { if !info.quiet { println!("{} {}", res, filename.as_path().display()); } }, } } if !info.quiet && info.debug { common::debug(format!("thread {} has ran out of work", info.thread_id)); } Ok(()) } fn verify( cpus: usize, algorithm: String, debug: bool, quiet: bool, checksum_files: Vec, ) -> ( Vec>>, Arc, Arc, ) { let mut handles = vec![]; let mut hash_map: HashMap = HashMap::new(); let mut buffer = VecDeque::new(); for file in checksum_files { match read_to_string(&file) { Err(e) => { common::error(format!("{}: {}", file.as_path().display(), e)); continue; }, Ok(f) => { for line in f.lines() { let split: Vec = line.split_whitespace().map(|x| x.to_string()).collect(); // println!("{}, {}", split.size_hint().0, split.size_hint().1) match split.len() { 2 => { hash_map.insert(PathBuf::from(split[1].clone()), split[0].clone()); buffer.push_back(PathBuf::from(split[1].clone())); }, _ => common::error(format!("malformed line: {}", line)), } } }, }; } let arc_fe = Arc::new(AtomicI32::new(0)); let arc_he = Arc::new(AtomicI32::new(0)); let arc_buf = Arc::new(Mutex::new(buffer)); let arc_hash = Arc::new(Mutex::new(hash_map)); for i in 0..cpus { let safe_fe = Arc::clone(&arc_fe); let safe_he = Arc::clone(&arc_he); let safe_buf = Arc::clone(&arc_buf); let safe_alg = Arc::new(algorithm.clone()); let safe_hash = Arc::clone(&arc_hash); handles.push(thread::spawn(move || { hash(ThreadInfo { debug, quiet, thread_id: i, filenames: safe_buf, algorithm: safe_alg, hash_map: Some(safe_hash), file_errors: safe_fe, hash_errors: safe_he, }) })) } return (handles, arc_fe, arc_he); } fn generate( cpus: usize, buffer: VecDeque, algorithm: String, debug: bool, quiet: bool, ) -> ( Vec>>, Arc, Arc, ) { let mut handles = vec![]; let arc_fe = Arc::new(AtomicI32::new(0)); let arc_he = Arc::new(AtomicI32::new(0)); let arc_buf = Arc::new(Mutex::new(buffer)); for i in 0..cpus { let safe_fe = Arc::clone(&arc_fe); let safe_he = Arc::clone(&arc_he); let safe_buf = Arc::clone(&arc_buf); let safe_alg = Arc::new(algorithm.clone()); handles.push(thread::spawn(move || { hash(ThreadInfo { debug, quiet, thread_id: i, filenames: safe_buf, algorithm: safe_alg, hash_map: None, file_errors: safe_fe, hash_errors: safe_he, }) })) } return (handles, arc_fe, arc_he); } fn main() { let args = Args::parse(); let cpus = match args.threads { 0 => available_parallelism().unwrap().get(), _ => args.threads, }; if args.debug { common::debug(format!( "Starting psha using algorithm {} with {} threads", args.algorithm, cpus )); } if UNSECURE_ALGORITHMS.contains(&args.algorithm.as_str()) { common::warning(format!( "{} is an unsecure hashing algorithm!", &args.algorithm )); } let handles; let arc_fe; let arc_he; if &args.check.len() >= &1 { (handles, arc_fe, arc_he) = verify(cpus, args.algorithm, args.debug, args.quiet, args.check); } else { let mut buffer = VecDeque::new(); for file in args.files { if args.canonicalize { match fs::canonicalize(file.as_path()) { Ok(p) => buffer.push_back(p), Err(e) => panic!("unable to canonicalize {}: {}", file.as_path().display(), e), }; } else { buffer.push_back(file); } } (handles, arc_fe, arc_he) = generate(cpus, buffer, args.algorithm, args.debug, args.quiet); } for handle in handles { match handle.join().unwrap() { Err(e) => panic!("{}", e), Ok(_) => (), } } let fe = arc_fe.load(Ordering::SeqCst); let he = arc_he.load(Ordering::SeqCst); if fe != 0 { common::warning(format!("{} listed files could not be read", fe)); } if he != 0 { common::warning(format!("{} computed checksums did NOT match", he)); } }