#![forbid(unsafe_code)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::io::Read;
use std::process::ExitCode;
use base64::Engine as _;
use blob_decoder::{identify, BlobKind, Candidate, Confidence};
use clap::Parser;
#[derive(Parser, Debug)]
#[command(
name = "blob-decode",
version,
about = "Identify and decode opaque blobs of unknown type — scored, cited candidates."
)]
struct Cli {
file: Option<String>,
#[arg(long, conflicts_with_all = ["hex", "base64", "file"])]
string: Option<String>,
#[arg(long, conflicts_with_all = ["string", "base64", "file"])]
hex: Option<String>,
#[arg(long = "base64", conflicts_with_all = ["string", "hex", "file"])]
base64: Option<String>,
#[arg(long)]
json: bool,
#[arg(long, value_name = "N")]
top: Option<usize>,
}
fn main() -> ExitCode {
let cli = Cli::parse();
let bytes = match load(&cli) {
Ok(b) => b,
Err(e) => {
eprintln!("blob-decode: {e}");
return ExitCode::from(1);
}
};
let mut cands = identify(&bytes);
if let Some(n) = cli.top {
cands.truncate(n);
}
if cli.json {
match serde_json::to_string_pretty(&cands) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("blob-decode: serialization failed: {e}");
return ExitCode::from(1);
}
}
} else {
print_human(&bytes, &cands);
}
if cands.first().is_none_or(|c| c.kind == BlobKind::Unknown) {
ExitCode::from(2)
} else {
ExitCode::SUCCESS
}
}
fn load(cli: &Cli) -> Result<Vec<u8>, String> {
if let Some(s) = &cli.string {
return Ok(s.clone().into_bytes());
}
if let Some(h) = &cli.hex {
return hex::decode(h.trim()).map_err(|e| format!("invalid hex input: {e}"));
}
if let Some(b) = &cli.base64 {
return base64::engine::general_purpose::STANDARD
.decode(b.trim())
.map_err(|e| format!("invalid base64 input: {e}"));
}
match cli.file.as_deref() {
None | Some("-") => {
let mut buf = Vec::new();
std::io::stdin()
.read_to_end(&mut buf)
.map_err(|e| format!("reading stdin: {e}"))?;
Ok(buf)
}
Some(path) => std::fs::read(path).map_err(|e| format!("{path}: {e}")),
}
}
fn print_human(bytes: &[u8], cands: &[Candidate]) {
println!(
"blob-decode: {} bytes; {} candidate reading(s), best first:",
bytes.len(),
cands.len()
);
for c in cands {
print_candidate(c, 1);
}
}
fn print_candidate(c: &Candidate, indent: usize) {
let pad = " ".repeat(indent);
println!(
"{pad}[{}] {} — {}",
conf_str(c.score),
c.kind.label(),
c.summary
);
println!("{pad} cite: {}", c.citation);
if let Some(chain) = &c.inner {
println!(
"{pad} decodes to {} bytes{}:",
chain.decoded_len,
if chain.capped { " (capped)" } else { "" }
);
print_candidate(&chain.best, indent + 1);
}
}
fn conf_str(c: Confidence) -> &'static str {
match c {
Confidence::High => "HIGH",
Confidence::Medium => "MED ",
Confidence::Low => "LOW ",
}
}