use std::io::{self, BufRead, IsTerminal};
use anyhow::{anyhow, bail, Result};
use clap::{Args, ValueEnum};
use crate::hasher;
#[derive(Args)]
pub struct HashArgs {
pub input: Option<String>,
#[arg(short, long, default_value = "sha256", value_parser = hasher::algo_value_parser())]
pub algo: Vec<String>,
#[arg(short, long, default_value = "plain")]
pub format: OutputFormat,
}
#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
Plain,
Json,
}
pub fn run(args: HashArgs) -> Result<()> {
let hashers: Vec<Box<dyn hasher::Hasher>> = args
.algo
.iter()
.map(|name| {
hasher::get_hasher(name)
.ok_or_else(|| anyhow!("Unsupported algorithm: {name}"))
})
.collect::<Result<Vec<_>>>()?;
let inputs = collect_inputs(&args)?;
if inputs.is_empty() {
bail!("No input provided. Pass text as argument or pipe via stdin.");
}
match args.format {
OutputFormat::Plain => print_plain(&inputs, &hashers),
OutputFormat::Json => print_json(&inputs, &hashers)?,
}
Ok(())
}
fn collect_inputs(args: &HashArgs) -> Result<Vec<String>> {
if let Some(ref text) = args.input {
return Ok(vec![text.clone()]);
}
if io::stdin().is_terminal() {
return Ok(vec![]);
}
let reader = io::stdin().lock();
let lines: Vec<String> = reader
.lines()
.collect::<std::result::Result<Vec<_>, _>>()?
.into_iter()
.filter(|line| !line.is_empty())
.collect();
Ok(lines)
}
fn print_plain(inputs: &[String], hashers: &[Box<dyn hasher::Hasher>]) {
let single_algo = hashers.len() == 1;
let single_input = inputs.len() == 1;
for input in inputs {
for hasher in hashers {
let hash = hex::encode(hasher.hash(input.as_bytes()));
if single_input && single_algo {
println!("{hash}");
} else if single_algo {
println!("{hash} {input}");
} else if single_input {
println!("{hash} {}", hasher.name());
} else {
println!("{hash} {} {input}", hasher.name());
}
}
}
}
fn print_json(inputs: &[String], hashers: &[Box<dyn hasher::Hasher>]) -> Result<()> {
#[derive(serde::Serialize)]
struct JsonHash {
input: String,
algorithm: String,
hash: String,
}
let results: Vec<JsonHash> = inputs
.iter()
.flat_map(|input| {
hashers.iter().map(move |hasher| JsonHash {
input: input.clone(),
algorithm: hasher.name().to_string(),
hash: hex::encode(hasher.hash(input.as_bytes())),
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&results)?);
Ok(())
}