1#![allow(dead_code)]
2
3use md5::{Digest, Md5};
4use std::collections::HashSet;
5use std::io;
6use std::path::Path;
7
8pub fn compute_md5(data: &[u8]) -> String {
10 let mut hasher = Md5::new();
11 hasher.update(data);
12 format!("{:x}", hasher.finalize())
13}
14
15pub trait FilterDb {
17 fn contains_md5(&self, md5: &str) -> bool;
19}
20
21pub struct CustomDb {
23 hashes: HashSet<String>,
24}
25
26impl CustomDb {
27 pub fn load(path: &Path) -> io::Result<Self> {
28 let content = std::fs::read_to_string(path)?;
29 let hashes: HashSet<String> = content
30 .lines()
31 .map(|line| line.trim().to_lowercase())
32 .filter(|line| !line.is_empty() && !line.starts_with('#'))
33 .collect();
34 Ok(Self { hashes })
35 }
36}
37
38impl FilterDb for CustomDb {
39 fn contains_md5(&self, md5: &str) -> bool {
40 self.hashes.contains(&md5.to_lowercase())
41 }
42}
43
44pub struct HashKeeperDb {
47 hashes: HashSet<String>,
48}
49
50impl HashKeeperDb {
51 pub fn load(path: &Path) -> io::Result<Self> {
52 let content = std::fs::read_to_string(path)?;
53 let mut hashes = HashSet::new();
54 for line in content.lines() {
55 let line = line.trim();
56 if line.is_empty() || line.starts_with('#') || line.starts_with('%') {
57 continue;
58 }
59 for field in line.split([',', '\t']) {
62 let field = field.trim().to_lowercase();
63 if field.len() == 32 && field.chars().all(|c| c.is_ascii_hexdigit()) {
64 hashes.insert(field);
65 break;
66 }
67 }
68 }
69 Ok(Self { hashes })
70 }
71}
72
73impl FilterDb for HashKeeperDb {
74 fn contains_md5(&self, md5: &str) -> bool {
75 self.hashes.contains(&md5.to_lowercase())
76 }
77}
78
79pub struct NsrlDb {
81 hashes: HashSet<String>,
82}
83
84impl NsrlDb {
85 pub fn load(path: &Path) -> io::Result<Self> {
89 if let Ok(db) = Self::load_sqlite(path) {
91 return Ok(db);
92 }
93 let custom = CustomDb::load(path)?;
95 Ok(Self {
96 hashes: custom.hashes,
97 })
98 }
99
100 fn load_sqlite(path: &Path) -> io::Result<Self> {
101 let conn =
102 rusqlite::Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
103 .map_err(io::Error::other)?;
104
105 let mut hashes = HashSet::new();
106
107 for query in &[
110 "SELECT md5 FROM FILE",
111 "SELECT md5 FROM file",
112 "SELECT MD5 FROM FILE",
113 "SELECT hash FROM hashes WHERE type='md5'",
114 ] {
115 if let Ok(mut stmt) = conn.prepare(query) {
116 let rows = stmt.query_map([], |row| row.get::<_, String>(0));
117 if let Ok(rows) = rows {
118 for row in rows.flatten() {
119 hashes.insert(row.to_lowercase());
120 }
121 if !hashes.is_empty() {
122 break;
123 }
124 }
125 }
126 }
127
128 if hashes.is_empty() {
129 return Err(io::Error::new(
130 io::ErrorKind::NotFound,
131 "no MD5 hashes found in SQLite DB",
132 ));
133 }
134
135 Ok(Self { hashes })
136 }
137}
138
139impl FilterDb for NsrlDb {
140 fn contains_md5(&self, md5: &str) -> bool {
141 self.hashes.contains(&md5.to_lowercase())
142 }
143}
144
145pub struct FilterChain {
147 dbs: Vec<Box<dyn FilterDb>>,
148}
149
150impl Default for FilterChain {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl FilterChain {
157 pub fn new() -> Self {
158 Self { dbs: Vec::new() }
159 }
160
161 pub fn add(&mut self, db: Box<dyn FilterDb>) {
162 self.dbs.push(db);
163 }
164
165 pub fn is_empty(&self) -> bool {
166 self.dbs.is_empty()
167 }
168}
169
170impl FilterDb for FilterChain {
171 fn contains_md5(&self, md5: &str) -> bool {
172 self.dbs.iter().any(|db| db.contains_md5(md5))
173 }
174}
175
176#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
178pub struct FilterCache {
179 pub entries: std::collections::HashMap<u64, FilterCacheEntry>,
181}
182
183#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
184pub struct FilterCacheEntry {
185 pub md5: String,
186 pub is_known: bool,
187}
188
189impl FilterCache {
190 pub fn save(&self, path: &Path) -> io::Result<()> {
191 let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
192 std::fs::write(path, json)
193 }
194
195 pub fn load(path: &Path) -> io::Result<Self> {
196 let json = std::fs::read_to_string(path)?;
197 serde_json::from_str(&json).map_err(io::Error::other)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use std::io::Write;
205
206 #[test]
207 fn compute_md5_known_value() {
208 let hash = compute_md5(b"");
210 assert_eq!(hash, "d41d8cd98f00b204e9800998ecf8427e");
211 }
212
213 #[test]
214 fn compute_md5_hello() {
215 let hash = compute_md5(b"Hello, ext4!");
216 assert_eq!(hash.len(), 32);
217 assert_eq!(hash, compute_md5(b"Hello, ext4!"));
219 }
220
221 #[test]
222 fn custom_db_lookup() {
223 let tmp = std::env::temp_dir().join("forensic_mount_test_custom_db.txt");
224 let mut f = std::fs::File::create(&tmp).unwrap();
225 writeln!(f, "d41d8cd98f00b204e9800998ecf8427e").unwrap();
226 writeln!(f, "# comment line").unwrap();
227 writeln!(f, "098f6bcd4621d373cade4e832627b4f6").unwrap();
228 drop(f);
229
230 let db = CustomDb::load(&tmp).unwrap();
231 assert!(db.contains_md5("d41d8cd98f00b204e9800998ecf8427e"));
232 assert!(db.contains_md5("098f6bcd4621d373cade4e832627b4f6"));
233 assert!(!db.contains_md5("0000000000000000000000000000000"));
234 assert!(db.contains_md5("D41D8CD98F00B204E9800998ECF8427E"));
236
237 let _ = std::fs::remove_file(&tmp);
238 }
239
240 #[test]
241 fn hashkeeper_db_lookup() {
242 let tmp = std::env::temp_dir().join("forensic_mount_test_hk_db.txt");
243 let mut f = std::fs::File::create(&tmp).unwrap();
244 writeln!(f, "% header line").unwrap();
245 writeln!(f, "1,2,file.txt,100,d41d8cd98f00b204e9800998ecf8427e").unwrap();
246 writeln!(f, "3,4,other.dll,200,098f6bcd4621d373cade4e832627b4f6").unwrap();
247 drop(f);
248
249 let db = HashKeeperDb::load(&tmp).unwrap();
250 assert!(db.contains_md5("d41d8cd98f00b204e9800998ecf8427e"));
251 assert!(db.contains_md5("098f6bcd4621d373cade4e832627b4f6"));
252 assert!(!db.contains_md5("ffffffffffffffffffffffffffffffff"));
253
254 let _ = std::fs::remove_file(&tmp);
255 }
256
257 #[test]
258 fn filter_chain_combines_dbs() {
259 let tmp1 = std::env::temp_dir().join("forensic_mount_test_chain1.txt");
260 let tmp2 = std::env::temp_dir().join("forensic_mount_test_chain2.txt");
261 std::fs::write(&tmp1, "d41d8cd98f00b204e9800998ecf8427e\n").unwrap();
262 std::fs::write(&tmp2, "098f6bcd4621d373cade4e832627b4f6\n").unwrap();
263
264 let mut chain = FilterChain::new();
265 chain.add(Box::new(CustomDb::load(&tmp1).unwrap()));
266 chain.add(Box::new(CustomDb::load(&tmp2).unwrap()));
267
268 assert!(chain.contains_md5("d41d8cd98f00b204e9800998ecf8427e"));
269 assert!(chain.contains_md5("098f6bcd4621d373cade4e832627b4f6"));
270 assert!(!chain.contains_md5("0000000000000000000000000000000"));
271
272 let _ = std::fs::remove_file(&tmp1);
273 let _ = std::fs::remove_file(&tmp2);
274 }
275
276 #[test]
277 fn filter_cache_roundtrip() {
278 let tmp = std::env::temp_dir().join("forensic_mount_test_filter_cache.json");
279 let mut cache = FilterCache::default();
280 cache.entries.insert(
281 12,
282 FilterCacheEntry {
283 md5: "d41d8cd98f00b204e9800998ecf8427e".to_string(),
284 is_known: true,
285 },
286 );
287 cache.save(&tmp).unwrap();
288
289 let loaded = FilterCache::load(&tmp).unwrap();
290 assert_eq!(loaded.entries.len(), 1);
291 assert!(loaded.entries[&12].is_known);
292
293 let _ = std::fs::remove_file(&tmp);
294 }
295}