blazehash 0.2.5

Forensic file hasher — hashdeep for the modern era, BLAKE3 by default
Documentation
use assert_cmd::Command;
use tempfile::TempDir;
use std::fs;

fn write_manifest(dir: &TempDir) -> std::path::PathBuf {
    let p = dir.path().join("case.hash");
    fs::write(&p, concat!(
        "## case: CASE-001\n",
        "sha256  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  docs/contract.pdf\n",
        "blake3  bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb  images/photo.jpg\n",
    )).unwrap();
    p
}

#[test]
fn test_export_csv_has_header_row() {
    let dir = TempDir::new().unwrap();
    let manifest = write_manifest(&dir);
    let out = Command::cargo_bin("blazehash").unwrap()
        .args(["export", manifest.to_str().unwrap(), "--export-format", "csv"])
        .output().unwrap();
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    let first_line = stdout.lines().next().unwrap_or("");
    assert!(first_line.to_ascii_lowercase().contains("algo") ||
            first_line.to_ascii_lowercase().contains("algorithm"),
        "CSV first line must be a header with algo column, got: {first_line}");
}

#[test]
fn test_export_csv_has_data_rows() {
    let dir = TempDir::new().unwrap();
    let manifest = write_manifest(&dir);
    let out = Command::cargo_bin("blazehash").unwrap()
        .args(["export", manifest.to_str().unwrap(), "--export-format", "csv"])
        .output().unwrap();
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("contract.pdf"), "CSV must contain path");
    assert!(stdout.contains("sha256"), "CSV must contain algo");
}

#[test]
fn test_export_jsonl_one_json_object_per_line() {
    let dir = TempDir::new().unwrap();
    let manifest = write_manifest(&dir);
    let out = Command::cargo_bin("blazehash").unwrap()
        .args(["export", manifest.to_str().unwrap(), "--export-format", "jsonl"])
        .output().unwrap();
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    for line in stdout.lines().filter(|l| !l.trim().is_empty()) {
        let obj: serde_json::Value = serde_json::from_str(line)
            .unwrap_or_else(|e| panic!("JSONL line is not valid JSON: {line}\n{e}"));
        assert!(obj["path"].is_string(), "JSON line must have path field");
        assert!(obj["hash"].is_string(), "JSON line must have hash field");
        assert!(obj["algo"].is_string(), "JSON line must have algo field");
    }
}

#[test]
fn test_export_tsv_tab_separated() {
    let dir = TempDir::new().unwrap();
    let manifest = write_manifest(&dir);
    let out = Command::cargo_bin("blazehash").unwrap()
        .args(["export", manifest.to_str().unwrap(), "--export-format", "tsv"])
        .output().unwrap();
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);
    let has_tab = stdout.lines()
        .filter(|l| !l.trim().is_empty())
        .any(|l| l.contains('\t'));
    assert!(has_tab, "TSV output must contain tab characters");
}

#[test]
fn test_export_unknown_format_fails() {
    let dir = TempDir::new().unwrap();
    let manifest = write_manifest(&dir);
    Command::cargo_bin("blazehash").unwrap()
        .args(["export", manifest.to_str().unwrap(), "--export-format", "xls"])
        .assert().failure();
}

#[test]
fn test_export_output_to_file() {
    let dir = TempDir::new().unwrap();
    let manifest = write_manifest(&dir);
    let out_path = dir.path().join("out.csv");
    Command::cargo_bin("blazehash").unwrap()
        .args(["export", manifest.to_str().unwrap(), "--export-format", "csv",
               "-o", out_path.to_str().unwrap()])
        .assert().success();
    let content = fs::read_to_string(&out_path).unwrap();
    assert!(content.contains("contract.pdf"));
}