Skip to main content

ext4fs/forensic/
hash.rs

1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use crate::ondisk::FileType;
6use std::io::{Read, Seek};
7
8/// Hash results for a single file (BLAKE3, SHA-256, MD5, SHA-1).
9#[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
19/// Compute all four hashes for a file by inode number.
20pub 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
36/// Hash all allocated regular files on the filesystem.
37pub 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        // Skip inodes without valid block mappings
46        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        // hello.txt contains "Hello, forensic world!\n"
107        let hash = hash_file(&mut reader, 12).unwrap();
108        // Compute expected SHA-256 independently
109        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        // All hashes should have correct lengths
130        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}