use blazehash::algorithm::Algorithm;
use clap::Parser;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Parser, Debug)]
#[command(
name = "blazehash",
version,
about = "Forensic file hasher — hashdeep for the modern era"
)]
pub struct Cli {
#[arg()]
pub paths: Vec<PathBuf>,
#[arg(short = 'c', long = "compute", value_parser = parse_algorithms, default_value = "blake3")]
pub algorithms: Vec<Vec<Algorithm>>,
#[arg(short = 'r', long = "recursive")]
pub recursive: bool,
#[arg(short = 'o', long = "output", num_args = 0..=1, default_missing_value = "__auto__")]
pub output: Option<PathBuf>,
#[arg(short = 'a', long = "audit")]
pub audit: bool,
#[arg(short = 'k', long = "known")]
pub known: Vec<PathBuf>,
#[arg(long = "verify-image")]
pub verify_image: bool,
#[arg(short = 's', long = "size-only")]
pub size_only: bool,
#[arg(short = 'b', long = "bare")]
pub bare: bool,
#[arg(short = 'p', long = "piecewise")]
pub piecewise: Option<String>,
#[arg(long = "resume")]
pub resume: bool,
#[arg(long = "stdin")]
pub stdin: bool,
#[arg(long = "no-cache")]
pub no_cache: bool,
#[arg(long = "no-gpu")]
pub no_gpu: bool,
#[arg(long = "format", default_value = "hashdeep")]
pub format: String,
#[arg(long = "gpu", help = "Run GPU calibration benchmark")]
pub gpu: bool,
#[arg(
long = "no-calibrate",
help = "Use conservative defaults; do not run benchmark or write config"
)]
pub no_calibrate: bool,
#[arg(long = "fuzzy-threshold", default_value = "50", value_parser = clap::value_parser!(u32).range(0..=100))]
pub fuzzy_threshold: u32,
#[arg(long = "fuzzy-top", default_value = "5")]
pub fuzzy_top: usize,
#[arg(long = "min-size", value_parser = parse_chunk_size)]
pub min_size: Option<usize>,
#[arg(long = "max-size", value_parser = parse_chunk_size)]
pub max_size: Option<usize>,
#[arg(long = "newer", value_parser = parse_date)]
pub newer: Option<std::time::SystemTime>,
#[arg(long = "include")]
pub include: Vec<String>,
#[arg(long = "exclude")]
pub exclude: Vec<String>,
#[arg(long = "ads")]
pub ads: bool,
#[arg(long = "dedup-unique")]
pub dedup_unique: bool,
#[arg(long = "dedup-dupes")]
pub dedup_dupes: bool,
#[arg(long = "nsrl", value_name = "FILE")]
pub nsrl: Option<PathBuf>,
#[cfg(feature = "hashdb")]
#[arg(long = "nsrl-hsh", value_name = "FILE")]
pub nsrl_hsh: Option<PathBuf>,
#[cfg(feature = "hashdb")]
#[arg(long = "hashdb-bad", value_name = "FILE")]
pub hashdb_bad: Option<std::path::PathBuf>,
#[arg(long = "nsrl-exclude")]
pub nsrl_exclude: bool,
#[arg(long = "expected-pubkey", value_name = "HEX")]
pub expected_pubkey: Option<String>,
#[arg(long = "sign")]
pub sign: bool,
#[arg(long = "ignore-sig")]
pub ignore_sig: bool,
#[arg(long = "fail-on-unknown")]
pub fail_on_unknown: bool,
#[arg(long = "threshold", default_value = "1")]
pub threshold: usize,
#[arg(long = "patch")]
pub patch: bool,
#[arg(long = "compare-by", default_value = "content", value_parser = ["content", "paranoid", "size-time", "name"])]
pub compare_by: String,
#[arg(long = "show-identical")]
pub show_identical: bool,
#[arg(long = "mft")]
pub mft: bool,
#[arg(long = "_mft-worker", value_name = "FILE", hide = true)]
pub mft_worker_output: Option<PathBuf>,
#[arg(long = "entropy", help = "Compute Shannon entropy for each file")]
pub entropy: bool,
#[cfg(feature = "yara")]
#[arg(
long = "yara",
value_name = "FILE",
help = "YARA rules file to scan files during hashing"
)]
pub yara: Option<PathBuf>,
#[cfg(feature = "yara")]
#[arg(
long = "yara-max-size",
value_name = "MB",
default_value = "256",
help = "Maximum file size (MB) for YARA scanning; larger files skip YARA"
)]
pub yara_max_size: u64,
#[arg(long = "api-key", value_name = "KEY")]
pub api_key: Option<String>,
#[arg(long = "examiner", value_name = "NAME")]
pub examiner: Option<String>,
#[arg(long = "case", value_name = "ID")]
pub case_id: Option<String>,
#[arg(long = "progress")]
pub progress: bool,
#[arg(long = "path", value_name = "PATH")]
pub merkle_path: Option<String>,
#[arg(long = "sha256", value_name = "HEX")]
pub merkle_sha256: Option<String>,
#[arg(long = "paths", value_name = "PATHS")]
pub disclose_paths: Option<String>,
#[arg(long = "proof", value_name = "JSON")]
pub merkle_proof: Option<String>,
#[arg(long = "root", value_name = "HEX")]
pub merkle_root: Option<String>,
#[arg(long = "sector-size", default_value = "512")]
pub sector_size: usize,
#[arg(long = "json")]
pub json: bool,
#[arg(long = "algo", value_name = "ALGO")]
pub filter_algo: Option<String>,
#[arg(long = "strip-prefix", value_name = "PREFIX")]
pub strip_prefix: Option<String>,
#[arg(long = "add-prefix", value_name = "PREFIX")]
pub add_prefix: Option<String>,
#[arg(long = "from", value_name = "FORMAT")]
pub from_format: Option<String>,
#[arg(long = "count", short = 'n', default_value = "10")]
pub count: usize,
#[arg(long = "search-path", value_name = "QUERY")]
pub search_path: Option<String>,
#[arg(long = "hash", value_name = "PREFIX")]
pub search_hash: Option<String>,
#[arg(long = "ignore-case", short = 'i')]
pub ignore_case: bool,
#[arg(long = "export-format", value_name = "FORMAT")]
pub export_format: Option<String>,
#[arg(long = "sort-by", value_name = "FIELD", default_value = "path")]
pub sort_by: String,
#[arg(long = "verify-algo", value_name = "ALGO")]
pub verify_algo: Option<String>,
#[arg(long = "set", value_name = "KEY=VALUE")]
pub tag_set: Vec<String>,
#[arg(long = "unset", value_name = "KEY")]
pub tag_unset: Vec<String>,
#[arg(long = "chunk", default_value = "1000")]
pub split_chunk: usize,
#[arg(long = "parts")]
pub balance_parts: Option<usize>,
#[arg(long = "pivot-algo")]
pub pivot_algo: Option<String>,
#[arg(long = "rename-from")]
pub rename_from: Option<String>,
#[arg(long = "rename-to", default_value = "")]
pub rename_to: String,
#[arg(long = "offset", default_value = "0")]
pub slice_offset: usize,
#[arg(long = "pattern")]
pub grep_pattern: Option<String>,
#[arg(long = "tally-by", value_name = "FIELD", default_value = "ext",
value_parser = ["ext", "dir", "algo"])]
pub tally_by: String,
#[arg(long = "exclude-pattern", value_name = "GLOB")]
pub exclude_pattern: Option<String>,
#[arg(long = "hash-only-algo", value_name = "ALGO")]
pub hash_only_algo: Option<String>,
#[arg(long = "note")]
pub annotate_note: Option<String>,
#[arg(long = "seed")]
pub shuffle_seed: Option<u64>,
#[arg(long = "remote-config", value_name = "KEY=VALUE")]
pub remote_config: Vec<String>,
}
pub fn parse_chunk_size(s: &str) -> Result<usize, String> {
let s = s.trim();
let (num_str, multiplier) = if s.ends_with('G') || s.ends_with('g') {
(&s[..s.len() - 1], 1024 * 1024 * 1024)
} else if s.ends_with('M') || s.ends_with('m') {
(&s[..s.len() - 1], 1024 * 1024)
} else if s.ends_with('K') || s.ends_with('k') {
(&s[..s.len() - 1], 1024)
} else {
(s, 1usize)
};
let num: usize = num_str
.parse()
.map_err(|e: std::num::ParseIntError| e.to_string())?;
Ok(num * multiplier)
}
fn parse_date(s: &str) -> Result<std::time::SystemTime, String> {
let d = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map_err(|e| format!("invalid date {s:?}: {e}"))?;
let dt = d.and_hms_opt(0, 0, 0).unwrap();
let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
let secs = (dt - epoch).num_seconds() as u64;
Ok(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs))
}
fn parse_algorithms(s: &str) -> Result<Vec<Algorithm>, String> {
s.split(',')
.map(|name| Algorithm::from_str(name.trim()).map_err(|e| e.to_string()))
.collect()
}
#[derive(Debug)]
pub enum Mode {
Mcp,
Bench,
Diff,
Dedup,
NsrlBuildBloom,
SizeOnly,
Audit,
VerifyImage,
Piecewise,
Stdin,
Sign,
VerifySig,
PqSign,
PqVerifySig,
Cosign,
VerifyMsig,
Merge,
Update,
Vt,
Watch,
#[cfg(feature = "report")]
Report,
Completions,
Merkle,
MerkleProof,
MerkleVerify,
Disclose,
ProveMembership,
#[cfg(feature = "ots")]
OtsStamp,
#[cfg(feature = "ots")]
OtsVerify,
#[cfg(feature = "tui")]
Tui,
#[cfg(feature = "qr")]
Qr,
Timeline,
Lint,
Redact,
Sample,
Stats,
Filter,
Normalize,
Selfcheck,
Archive,
Convert,
Head,
Tail,
Search,
Export,
Sort,
Intersect,
Subtract,
ApplyPatch,
Verify,
Info,
Missing,
Tag,
Count,
Cat,
Split,
Uniq,
Checksum,
Pivot,
Rename,
Slice,
Stamp,
Grep,
Tally,
Exclude,
Contains,
PathOnly,
HashOnly,
Duplicates,
Repair,
SymDiff,
First,
Annotate,
Shuffle,
Reverse,
UniqueHash,
Balance,
Interleave,
GDriveCollect,
Hash,
}
impl Cli {
pub fn flat_algorithms(&self) -> Vec<Algorithm> {
let flat: Vec<Algorithm> = self.algorithms.iter().flatten().copied().collect();
if flat.is_empty() {
vec![Algorithm::Blake3]
} else {
flat
}
}
pub fn resolve_output(&self) -> Option<PathBuf> {
let raw = self.output.as_ref()?;
if raw.as_os_str() != "__auto__" {
return Some(raw.clone());
}
let name = self
.paths
.iter()
.find(|p| p.is_dir() || p.exists())
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.filter(|n| *n != "." && *n != "..")
.unwrap_or("manifest");
Some(PathBuf::from(format!("{name}.hash")))
}
pub fn build_walk_filter(&self) -> anyhow::Result<blazehash::walk_filter::WalkFilter> {
let mut b = blazehash::walk_filter::WalkFilter::builder();
for pat in &self.include {
b = b.include(pat);
}
for pat in &self.exclude {
b = b.exclude(pat);
}
if let Some(min) = self.min_size {
b = b.min_size(min as u64);
}
if let Some(max) = self.max_size {
b = b.max_size(max as u64);
}
if let Some(newer) = self.newer {
b = b.newer_than(newer);
}
b.build()
}
pub fn mode(&self) -> Mode {
if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("mcp")) {
Mode::Mcp
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("bench")) {
Mode::Bench
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("diff")) {
Mode::Diff
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("dedup")) {
Mode::Dedup
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("nsrl"))
&& self.paths.get(1).and_then(|p| p.to_str()) == Some("build-bloom")
{
Mode::NsrlBuildBloom
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("sign")) {
Mode::Sign
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("verify-sig"))
{
Mode::VerifySig
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("pq-sign"))
{
Mode::PqSign
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("pq-verify-sig"))
{
Mode::PqVerifySig
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("cosign"))
{
Mode::Cosign
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("verify-msig"))
{
Mode::VerifyMsig
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("merge")) {
Mode::Merge
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("update"))
{
Mode::Update
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("watch")) {
Mode::Watch
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("report"))
{
#[cfg(feature = "report")]
return Mode::Report;
#[cfg(not(feature = "report"))]
Mode::Hash
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("vt")) {
Mode::Vt
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("completions"))
{
Mode::Completions
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("tui")) {
#[cfg(feature = "tui")]
return Mode::Tui;
#[cfg(not(feature = "tui"))]
return Mode::Hash;
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("file-proof"))
{
Mode::MerkleProof
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("verify-proof"))
{
Mode::MerkleVerify
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("seal"))
{
Mode::Merkle
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("qr")) {
#[cfg(feature = "qr")]
return Mode::Qr;
#[cfg(not(feature = "qr"))]
return Mode::Hash;
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("disclose"))
{
Mode::Disclose
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("check-file"))
{
Mode::ProveMembership
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("timeline"))
{
Mode::Timeline
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("lint")) {
Mode::Lint
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("redact"))
{
Mode::Redact
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("sample"))
{
Mode::Sample
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("stats"))
{
Mode::Stats
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("filter"))
{
Mode::Filter
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("normalize"))
{
Mode::Normalize
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("selfcheck"))
{
Mode::Selfcheck
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("archive"))
{
Mode::Archive
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("convert"))
{
Mode::Convert
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("head")) {
Mode::Head
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("tail")) {
Mode::Tail
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("search"))
{
Mode::Search
} else if self.paths.first().map(|p| p.as_os_str())
== Some(std::ffi::OsStr::new("export"))
{
Mode::Export
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("sort")) {
Mode::Sort
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("intersect")) {
Mode::Intersect
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("subtract")) {
Mode::Subtract
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("apply-patch")) {
Mode::ApplyPatch
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("verify")) {
Mode::Verify
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("ots")) {
match self.paths.get(1).and_then(|p| p.to_str()) {
Some("stamp") => {
#[cfg(feature = "ots")]
return Mode::OtsStamp;
#[cfg(not(feature = "ots"))]
return Mode::Hash;
}
Some("verify") => {
#[cfg(feature = "ots")]
return Mode::OtsVerify;
#[cfg(not(feature = "ots"))]
return Mode::Hash;
}
_ => return Mode::Hash,
}
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("info")) {
Mode::Info
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("missing")) {
Mode::Missing
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("tag")) {
Mode::Tag
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("count")) {
Mode::Count
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("cat")) {
Mode::Cat
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("split")) {
Mode::Split
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("uniq")) {
Mode::Uniq
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("checksum")) {
Mode::Checksum
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("pivot")) {
Mode::Pivot
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("rename")) {
Mode::Rename
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("slice")) {
Mode::Slice
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("stamp")) {
Mode::Stamp
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("grep")) {
Mode::Grep
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("tally")) {
Mode::Tally
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("exclude")) {
Mode::Exclude
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("contains")) {
Mode::Contains
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("path-only")) {
Mode::PathOnly
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("hash-only")) {
Mode::HashOnly
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("duplicates")) {
Mode::Duplicates
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("repair")) {
Mode::Repair
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("sym-diff")) {
Mode::SymDiff
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("first")) {
Mode::First
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("annotate")) {
Mode::Annotate
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("shuffle")) {
Mode::Shuffle
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("reverse")) {
Mode::Reverse
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("unique-hash")) {
Mode::UniqueHash
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("balance")) {
Mode::Balance
} else if self.paths.first().map(|p| p.as_os_str()) == Some(std::ffi::OsStr::new("interleave")) {
Mode::Interleave
} else if self.paths.first().map(|p| {
let s = p.to_string_lossy();
s.starts_with("gdrive://")
|| s.starts_with("https://drive.google.com/")
|| s.starts_with("http://drive.google.com/")
}) == Some(true)
{
#[cfg(feature = "remote")]
return Mode::GDriveCollect;
#[cfg(not(feature = "remote"))]
{
eprintln!(
"error: gdrive requires the `remote` feature \
— recompile with --features remote"
);
std::process::exit(1);
}
} else if self.size_only {
Mode::SizeOnly
} else if self.audit {
Mode::Audit
} else if self.verify_image {
Mode::VerifyImage
} else if self.piecewise.is_some() {
Mode::Piecewise
} else if self.stdin {
Mode::Stdin
} else {
Mode::Hash
}
}
}