forensic_hashdb/
known_good.rs1use std::path::Path;
2
3use memmap2::Mmap;
4
5#[derive(Debug)]
7pub enum KnownGoodError {
8 Io(std::io::Error),
9 InvalidFileSize { bytes: u64 },
10}
11
12impl std::fmt::Display for KnownGoodError {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 match self {
15 Self::Io(e) => write!(f, "I/O error: {e}"),
16 Self::InvalidFileSize { bytes } => {
17 write!(f, "file size {bytes} is not a multiple of 32 bytes")
18 }
19 }
20 }
21}
22
23impl std::error::Error for KnownGoodError {}
24
25impl From<std::io::Error> for KnownGoodError {
26 fn from(e: std::io::Error) -> Self {
27 Self::Io(e)
28 }
29}
30
31pub struct KnownGoodDb {
37 mmap: Option<Mmap>,
39}
40
41impl KnownGoodDb {
42 pub fn open(path: &Path) -> Result<Self, KnownGoodError> {
45 let file = std::fs::File::open(path)?;
46 let meta = file.metadata()?;
47 let bytes = meta.len();
48 if bytes % 32 != 0 {
49 return Err(KnownGoodError::InvalidFileSize { bytes });
50 }
51 if bytes == 0 {
52 return Ok(Self { mmap: None });
53 }
54 #[allow(unsafe_code)]
59 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
60 Ok(Self { mmap: Some(mmap) })
61 }
62
63 pub fn is_known_good(&self, sha256: &[u8; 32]) -> bool {
66 let Some(ref mmap) = self.mmap else {
67 return false;
68 };
69 let count = mmap.len() / 32;
70 let data = mmap.as_ref();
71 let mut lo = 0usize;
72 let mut hi = count;
73 while lo < hi {
74 let mid = lo + (hi - lo) / 2;
75 let Some(entry) = data
79 .get(mid * 32..(mid + 1) * 32)
80 .and_then(|s| <&[u8; 32]>::try_from(s).ok())
81 else {
82 return false; };
84 match entry.cmp(sha256) {
85 std::cmp::Ordering::Equal => return true,
86 std::cmp::Ordering::Less => lo = mid + 1,
87 std::cmp::Ordering::Greater => hi = mid,
88 }
89 }
90 false
91 }
92
93 pub fn len(&self) -> usize {
95 self.mmap.as_ref().map_or(0, |m| m.len() / 32)
96 }
97
98 pub fn is_empty(&self) -> bool {
99 self.len() == 0
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::io::Write;
107
108 fn write_sorted_hashes(hashes: &mut [[u8; 32]]) -> tempfile::NamedTempFile {
109 hashes.sort_unstable();
110 let mut f = tempfile::NamedTempFile::new().unwrap();
111 for h in hashes.iter() {
112 f.write_all(h).unwrap();
113 }
114 f
115 }
116
117 #[test]
118 fn known_good_exact_hit_returns_true() {
119 let mut hashes: Vec<[u8; 32]> = vec![[0xaau8; 32], [0xbbu8; 32], [0xccu8; 32]];
120 let f = write_sorted_hashes(&mut hashes);
121 let db = KnownGoodDb::open(f.path()).unwrap();
122 assert!(db.is_known_good(&[0xaau8; 32]));
123 }
124
125 #[test]
126 fn known_good_miss_returns_false() {
127 let mut hashes: Vec<[u8; 32]> = vec![[0xaau8; 32], [0xbbu8; 32]];
128 let f = write_sorted_hashes(&mut hashes);
129 let db = KnownGoodDb::open(f.path()).unwrap();
130 assert!(!db.is_known_good(&[0xccu8; 32]));
131 }
132
133 #[test]
134 fn known_good_empty_db_returns_false() {
135 let mut hashes: Vec<[u8; 32]> = vec![];
136 let f = write_sorted_hashes(&mut hashes);
137 let db = KnownGoodDb::open(f.path()).unwrap();
138 assert!(!db.is_known_good(&[0x01u8; 32]));
139 }
140
141 #[test]
142 fn known_good_invalid_file_size_returns_error() {
143 let mut f = tempfile::NamedTempFile::new().unwrap();
144 f.write_all(&[0u8; 31]).unwrap(); let result = KnownGoodDb::open(f.path());
146 assert!(
147 matches!(result, Err(KnownGoodError::InvalidFileSize { bytes: 31 })),
148 "expected InvalidFileSize error"
149 );
150 }
151
152 #[test]
153 fn known_good_first_entry_found() {
154 let mut hashes: Vec<[u8; 32]> = vec![[0x01u8; 32], [0x80u8; 32], [0xffu8; 32]];
155 let f = write_sorted_hashes(&mut hashes);
156 let db = KnownGoodDb::open(f.path()).unwrap();
157 assert!(db.is_known_good(&[0x01u8; 32]));
159 }
160
161 #[test]
162 fn known_good_last_entry_found() {
163 let mut hashes: Vec<[u8; 32]> = vec![[0x01u8; 32], [0x80u8; 32], [0xffu8; 32]];
164 let f = write_sorted_hashes(&mut hashes);
165 let db = KnownGoodDb::open(f.path()).unwrap();
166 assert!(db.is_known_good(&[0xffu8; 32]));
168 }
169
170 #[test]
171 fn known_good_len_correct() {
172 let mut hashes: Vec<[u8; 32]> = vec![[0x01u8; 32], [0x02u8; 32], [0x03u8; 32]];
173 let f = write_sorted_hashes(&mut hashes);
174 let db = KnownGoodDb::open(f.path()).unwrap();
175 assert_eq!(db.len(), 3);
176 }
177}