use std::{
fs::File,
io::{self, Result as IoResult, Write},
path::{Path, PathBuf},
};
use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use wholesum::{hashfile::HashFile, Algorithm, Mode};
#[derive(Parser)]
#[command(about, author, version)]
struct Opt {
#[arg(short, long, value_enum, default_value_t)]
mode: Mode,
#[arg(short, long, value_enum, default_value_t)]
algorithm: Algorithm,
#[arg(short, long)]
prefix: bool,
files: Vec<PathBuf>,
#[command(subcommand)]
cmd: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Check {
file: PathBuf,
},
Completions {
#[arg(value_enum)]
shell: Shell,
},
Manpages {
dir: PathBuf,
},
}
const IS_A_DIRECTORY: i32 = 21;
fn main() -> Result<()> {
let opt = Opt::parse();
if let Some(cmd) = opt.cmd {
match cmd {
Command::Check { file } => verify_files(file),
Command::Completions { shell } => {
print_completions(shell);
Ok(())
}
Command::Manpages { dir } => print_manpages(&dir),
}
} else {
hash_files(opt)
}
}
fn verify_files(file: PathBuf) -> Result<()> {
let hash_file = HashFile::parse(file)?;
let matches = hash_file
.entries
.into_par_iter()
.map(|entry| {
let hasher = entry
.algorithm
.or(hash_file.algorithm)
.unwrap_or(Algorithm::Blake3)
.into_hasher();
let hash = hasher(Path::new(&entry.file))?;
Ok((entry.file, hash == entry.hash))
})
.collect::<Result<Vec<_>>>()?;
let width = matches
.iter()
.map(|(file, _)| file.len())
.max()
.unwrap_or_default();
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
for (file, valid) in matches {
writeln!(
stdout,
"{:width$} {}",
file,
if valid { "OK" } else { "ERR" },
width = width
)?;
}
stdout.flush()?;
Ok(())
}
fn print_completions(shell: Shell) {
clap_complete::generate(
shell,
&mut Opt::command(),
env!("CARGO_PKG_NAME"),
&mut io::stdout().lock(),
);
}
fn print_manpages(dir: &Path) -> Result<()> {
fn print(dir: &Path, app: &clap::Command) -> Result<()> {
let name = app.get_display_name().unwrap_or_else(|| app.get_name());
let mut out = File::create(dir.join(format!("{name}.1")))?;
clap_mangen::Man::new(app.clone()).render(&mut out)?;
out.flush()?;
for sub in app.get_subcommands() {
print(dir, sub)?;
}
Ok(())
}
let mut app = Opt::command();
app.build();
print(dir, &app)
}
fn hash_files(opt: Opt) -> Result<()> {
let hasher = opt.algorithm.into_hasher();
let mut hashes = opt
.files
.into_par_iter()
.filter_map(|file| match hasher(&file) {
Ok(hash) => Some(Ok((hash, file))),
Err(e) if e.raw_os_error() == Some(IS_A_DIRECTORY) => None,
Err(e) => Some(Err(e)),
})
.collect::<IoResult<Vec<_>>>()?;
hashes.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
for (hash, path) in hashes {
if opt.prefix {
write!(stdout, "{} ", opt.algorithm)?;
}
writeln!(stdout, "{} {}{}", hash, opt.mode, path.display())?;
}
stdout.flush()?;
Ok(())
}