1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use crate::ondisk::FileType;
6use std::io::{Read, Seek};
7
8#[derive(Debug, Clone)]
10pub struct FileHash {
11 pub ino: u64,
12 pub size: u64,
13 pub blake3: String,
14 pub sha256: String,
15 pub md5: String,
16 pub sha1: String,
17}
18
19pub fn hash_file<R: Read + Seek>(reader: &mut InodeReader<R>, ino: u64) -> Result<FileHash> {
21 let inode = reader.read_inode(ino)?;
22 let data = reader.read_inode_data(ino)?;
23
24 use blazehash::algorithm::{hash_bytes, Algorithm};
25
26 Ok(FileHash {
27 ino,
28 size: inode.size,
29 blake3: hash_bytes(Algorithm::Blake3, &data),
30 sha256: hash_bytes(Algorithm::Sha256, &data),
31 md5: hash_bytes(Algorithm::Md5, &data),
32 sha1: hash_bytes(Algorithm::Sha1, &data),
33 })
34}
35
36pub fn hash_all_files<R: Read + Seek>(reader: &mut InodeReader<R>) -> Result<Vec<FileHash>> {
38 let all_inodes = reader.iter_all_inodes()?;
39 let mut results = Vec::new();
40
41 for (ino, inode) in &all_inodes {
42 if inode.file_type() != FileType::RegularFile || inode.size == 0 || inode.is_deleted() {
43 continue;
44 }
45 if let Ok(hash) = hash_file(reader, *ino) {
47 results.push(hash);
48 }
49 }
50
51 Ok(results)
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use crate::block::BlockReader;
58 use std::io::Cursor;
59
60 fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
61 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
62 let data = std::fs::read(path).ok()?;
63 let br = BlockReader::open(Cursor::new(data)).ok()?;
64 Some(InodeReader::new(br))
65 }
66
67 #[test]
68 fn hash_file_correct_lengths() {
69 let mut reader = if let Some(r) = open_forensic() {
70 r
71 } else {
72 eprintln!("skip");
73 return;
74 };
75 let hash = hash_file(&mut reader, 12).unwrap();
76 assert_eq!(hash.blake3.len(), 64, "BLAKE3 hex should be 64 chars");
77 assert_eq!(hash.sha256.len(), 64, "SHA-256 hex should be 64 chars");
78 assert_eq!(hash.md5.len(), 32, "MD5 hex should be 32 chars");
79 assert_eq!(hash.sha1.len(), 40, "SHA-1 hex should be 40 chars");
80 }
81
82 #[test]
83 fn hash_file_deterministic() {
84 let mut reader = if let Some(r) = open_forensic() {
85 r
86 } else {
87 eprintln!("skip");
88 return;
89 };
90 let h1 = hash_file(&mut reader, 12).unwrap();
91 let h2 = hash_file(&mut reader, 12).unwrap();
92 assert_eq!(h1.blake3, h2.blake3);
93 assert_eq!(h1.sha256, h2.sha256);
94 assert_eq!(h1.md5, h2.md5);
95 assert_eq!(h1.sha1, h2.sha1);
96 }
97
98 #[test]
99 fn hash_file_known_content() {
100 let mut reader = if let Some(r) = open_forensic() {
101 r
102 } else {
103 eprintln!("skip");
104 return;
105 };
106 let hash = hash_file(&mut reader, 12).unwrap();
108 let content = reader.read_inode_data(12).unwrap();
110 let expected_sha256 =
111 blazehash::algorithm::hash_bytes(blazehash::algorithm::Algorithm::Sha256, &content);
112 assert_eq!(hash.sha256, expected_sha256);
113 }
114
115 #[test]
116 fn hash_all_files_returns_multiple() {
117 let mut reader = if let Some(r) = open_forensic() {
118 r
119 } else {
120 eprintln!("skip");
121 return;
122 };
123 let hashes = hash_all_files(&mut reader).unwrap();
124 assert!(
125 hashes.len() >= 2,
126 "forensic.img should have multiple files, got {}",
127 hashes.len()
128 );
129 for h in &hashes {
131 assert_eq!(h.blake3.len(), 64);
132 assert_eq!(h.sha256.len(), 64);
133 assert_eq!(h.md5.len(), 32);
134 assert_eq!(h.sha1.len(), 40);
135 assert!(h.size > 0);
136 }
137 }
138}