#![forbid(unsafe_code)]
use crate::error::Result;
use crate::inode::InodeReader;
use crate::ondisk::FileType;
use std::io::{Read, Seek};
#[derive(Debug, Clone)]
pub struct FileHash {
pub ino: u64,
pub size: u64,
pub blake3: String,
pub sha256: String,
pub md5: String,
pub sha1: String,
}
pub fn hash_file<R: Read + Seek>(reader: &mut InodeReader<R>, ino: u64) -> Result<FileHash> {
let inode = reader.read_inode(ino)?;
let data = reader.read_inode_data(ino)?;
use blazehash::algorithm::{hash_bytes, Algorithm};
Ok(FileHash {
ino,
size: inode.size,
blake3: hash_bytes(Algorithm::Blake3, &data),
sha256: hash_bytes(Algorithm::Sha256, &data),
md5: hash_bytes(Algorithm::Md5, &data),
sha1: hash_bytes(Algorithm::Sha1, &data),
})
}
pub fn hash_all_files<R: Read + Seek>(reader: &mut InodeReader<R>) -> Result<Vec<FileHash>> {
let all_inodes = reader.iter_all_inodes()?;
let mut results = Vec::new();
for (ino, inode) in &all_inodes {
if inode.file_type() != FileType::RegularFile || inode.size == 0 || inode.is_deleted() {
continue;
}
if let Ok(hash) = hash_file(reader, *ino) {
results.push(hash);
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::BlockReader;
use std::io::Cursor;
fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
let data = std::fs::read(path).ok()?;
let br = BlockReader::open(Cursor::new(data)).ok()?;
Some(InodeReader::new(br))
}
#[test]
fn hash_file_correct_lengths() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let hash = hash_file(&mut reader, 12).unwrap();
assert_eq!(hash.blake3.len(), 64, "BLAKE3 hex should be 64 chars");
assert_eq!(hash.sha256.len(), 64, "SHA-256 hex should be 64 chars");
assert_eq!(hash.md5.len(), 32, "MD5 hex should be 32 chars");
assert_eq!(hash.sha1.len(), 40, "SHA-1 hex should be 40 chars");
}
#[test]
fn hash_file_deterministic() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let h1 = hash_file(&mut reader, 12).unwrap();
let h2 = hash_file(&mut reader, 12).unwrap();
assert_eq!(h1.blake3, h2.blake3);
assert_eq!(h1.sha256, h2.sha256);
assert_eq!(h1.md5, h2.md5);
assert_eq!(h1.sha1, h2.sha1);
}
#[test]
fn hash_file_known_content() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let hash = hash_file(&mut reader, 12).unwrap();
let content = reader.read_inode_data(12).unwrap();
let expected_sha256 =
blazehash::algorithm::hash_bytes(blazehash::algorithm::Algorithm::Sha256, &content);
assert_eq!(hash.sha256, expected_sha256);
}
#[test]
fn hash_all_files_returns_multiple() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let hashes = hash_all_files(&mut reader).unwrap();
assert!(
hashes.len() >= 2,
"forensic.img should have multiple files, got {}",
hashes.len()
);
for h in &hashes {
assert_eq!(h.blake3.len(), 64);
assert_eq!(h.sha256.len(), 64);
assert_eq!(h.md5.len(), 32);
assert_eq!(h.sha1.len(), 40);
assert!(h.size > 0);
}
}
}