blazehash 0.2.4

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

fn write_hashdeep(path: &std::path::Path, entries: &[(&str, &str)]) {
    use std::io::Write;
    let mut f = fs::File::create(path).unwrap();
    writeln!(f, "%%%% HASHDEEP-1.0").unwrap();
    writeln!(f, "%%%% size,blake3,filename").unwrap();
    writeln!(f, "##").unwrap();
    for (hash, name) in entries {
        writeln!(f, "5,{hash},{name}").unwrap();
    }
}

#[test]
fn test_diff_detects_added_file() {
    let dir = tempdir().unwrap();
    let before = dir.path().join("before.hash");
    let after = dir.path().join("after.hash");
    write_hashdeep(&before, &[("aaaa", "/file1.bin")]);
    write_hashdeep(&after, &[("aaaa", "/file1.bin"), ("bbbb", "/file2.bin")]);

    let output = assert_cmd::Command::cargo_bin("blazehash")
        .unwrap()
        .args(["diff", before.to_str().unwrap(), after.to_str().unwrap()])
        .output()
        .unwrap();
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("ADDED") || stdout.contains("[+]"),
        "expected ADDED: {stdout}"
    );
}

#[test]
fn test_diff_detects_removed_file() {
    let dir = tempdir().unwrap();
    let before = dir.path().join("before.hash");
    let after = dir.path().join("after.hash");
    write_hashdeep(&before, &[("aaaa", "/file1.bin"), ("bbbb", "/file2.bin")]);
    write_hashdeep(&after, &[("aaaa", "/file1.bin")]);

    let output = assert_cmd::Command::cargo_bin("blazehash")
        .unwrap()
        .args(["diff", before.to_str().unwrap(), after.to_str().unwrap()])
        .output()
        .unwrap();
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("REMOVED") || stdout.contains("[-]"),
        "expected REMOVED: {stdout}"
    );
}

#[test]
fn test_diff_detects_modified_file() {
    let dir = tempdir().unwrap();
    let before = dir.path().join("before.hash");
    let after = dir.path().join("after.hash");
    write_hashdeep(&before, &[("aaaa", "/file1.bin")]);
    write_hashdeep(&after, &[("zzzz", "/file1.bin")]);

    let output = assert_cmd::Command::cargo_bin("blazehash")
        .unwrap()
        .args(["diff", before.to_str().unwrap(), after.to_str().unwrap()])
        .output()
        .unwrap();
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("MODIFIED") || stdout.contains("[!]"),
        "expected MODIFIED: {stdout}"
    );
}

#[test]
fn test_diff_exits_zero_when_identical() {
    let dir = tempdir().unwrap();
    let before = dir.path().join("before.hash");
    let after = dir.path().join("after.hash");
    write_hashdeep(&before, &[("aaaa", "/file1.bin")]);
    write_hashdeep(&after, &[("aaaa", "/file1.bin")]);

    assert_cmd::Command::cargo_bin("blazehash")
        .unwrap()
        .args(["diff", before.to_str().unwrap(), after.to_str().unwrap()])
        .assert()
        .code(0);
}

#[test]
fn test_diff_exits_one_when_differences() {
    let dir = tempdir().unwrap();
    let before = dir.path().join("before.hash");
    let after = dir.path().join("after.hash");
    write_hashdeep(&before, &[("aaaa", "/file1.bin")]);
    write_hashdeep(&after, &[("bbbb", "/file1.bin")]);

    assert_cmd::Command::cargo_bin("blazehash")
        .unwrap()
        .args(["diff", before.to_str().unwrap(), after.to_str().unwrap()])
        .assert()
        .code(1);
}