1use std::path::Path;
16
17use memmap2::Mmap;
18
19use crate::error::Result;
20use crate::util::{Advice, advise_mmap, lock_mmap, mmap_file, preload_mmap, unlock_mmap};
21
22const BLOOM_MAGIC: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0, b'v', b'0', b'2', b'\n'];
24const BLOOM_BITS_OFFSET: usize = 60;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum FilterKind {
31 Empty,
33 Bloom,
35 Unsupported,
37}
38
39enum Inner {
40 Empty,
41 Bloom {
42 keys: [u64; 3],
43 m: u64,
44 bits_off: usize,
45 },
46 Unsupported,
47}
48
49pub struct ExistenceFilter {
51 mmap: Mmap,
53 inner: Inner,
54}
55
56impl ExistenceFilter {
57 pub fn open(path: impl AsRef<Path>) -> Result<ExistenceFilter> {
63 let mmap = mmap_file(path.as_ref())?;
64 let inner = Self::parse(&mmap);
65 Ok(ExistenceFilter { mmap, inner })
66 }
67
68 fn parse(d: &[u8]) -> Inner {
69 if d.is_empty() {
70 return Inner::Empty;
71 }
72 if d.len() >= BLOOM_BITS_OFFSET && d[0..12] == BLOOM_MAGIC {
73 let k = u64::from_le_bytes(d[12..20].try_into().unwrap());
74 let m = u64::from_le_bytes(d[28..36].try_into().unwrap());
75 let mut keys = [0u64; 3];
76 for (i, key) in keys.iter_mut().enumerate() {
77 *key = u64::from_le_bytes(d[36 + i * 8..44 + i * 8].try_into().unwrap());
78 }
79 let nwords = m.div_ceil(64) as usize;
80 if k == 3 && m >= 2 && BLOOM_BITS_OFFSET + nwords * 8 <= d.len() {
81 return Inner::Bloom {
82 keys,
83 m,
84 bits_off: BLOOM_BITS_OFFSET,
85 };
86 }
87 }
88 Inner::Unsupported
91 }
92
93 pub fn advise_random(&self) -> std::io::Result<()> {
96 advise_mmap(&self.mmap, Advice::Random)
97 }
98
99 pub fn mapped_bytes(&self) -> u64 {
101 self.mmap.len() as u64
102 }
103
104 pub fn preload(&self) -> u64 {
107 preload_mmap(&self.mmap) as u64
108 }
109
110 pub fn lock(&self) -> std::io::Result<()> {
113 lock_mmap(&self.mmap)
114 }
115
116 pub fn unlock(&self) -> std::io::Result<()> {
118 unlock_mmap(&self.mmap)
119 }
120
121 pub fn kind(&self) -> FilterKind {
123 match self.inner {
124 Inner::Empty => FilterKind::Empty,
125 Inner::Bloom { .. } => FilterKind::Bloom,
126 Inner::Unsupported => FilterKind::Unsupported,
127 }
128 }
129
130 pub fn is_accelerating(&self) -> bool {
133 matches!(self.inner, Inner::Bloom { .. })
134 }
135
136 #[inline]
137 fn bit_word(&self, bits_off: usize, idx: usize) -> u64 {
138 let off = bits_off + idx * 8;
139 u64::from_le_bytes(self.mmap[off..off + 8].try_into().unwrap())
140 }
141
142 #[inline]
146 pub fn contains_hash(&self, mut hash: u64) -> bool {
147 let (keys, m, bits_off) = match &self.inner {
148 Inner::Bloom { keys, m, bits_off } => (keys, *m, *bits_off),
149 Inner::Empty | Inner::Unsupported => return true,
150 };
151 let mut r = 1u64;
152 for &key in keys {
153 if r == 0 {
154 break;
155 }
156 hash = hash.rotate_left(17) ^ key;
157 let i = hash % m;
158 r &= (self.bit_word(bits_off, (i >> 6) as usize) >> (i & 0x3f)) & 1;
159 }
160 r != 0
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::hash::murmur3_x64_128_h1;
168
169 #[test]
171 fn bloom_roundtrip_via_file() {
172 let m: u64 = 4096;
173 let keys: [u64; 3] = [
174 0x1111_2222_3333_4444,
175 0xaaaa_bbbb_cccc_dddd,
176 0xdead_beef_0bad_f00d,
177 ];
178 let nwords = (m as usize).div_ceil(64);
179 let mut bits = vec![0u64; nwords];
180 let add = |bits: &mut [u64], mut h: u64| {
182 for &k in &keys {
183 h = h.rotate_left(17) ^ k;
184 let i = h % m;
185 bits[(i >> 6) as usize] |= 1 << (i & 63);
186 }
187 };
188 let present_keys: [&[u8]; 3] = [b"alpha", b"bravo-key", b"0123456789abcdef0123"];
190 let present: Vec<u64> = present_keys
191 .iter()
192 .map(|k| murmur3_x64_128_h1(k, 9))
193 .collect();
194 for &h in &present {
195 add(&mut bits, h);
196 }
197 let mut buf: Vec<u8> = Vec::new();
199 buf.extend_from_slice(&BLOOM_MAGIC);
200 buf.extend_from_slice(&3u64.to_le_bytes());
201 buf.extend_from_slice(&(present.len() as u64).to_le_bytes());
202 buf.extend_from_slice(&m.to_le_bytes());
203 for k in keys {
204 buf.extend_from_slice(&k.to_le_bytes());
205 }
206 for w in &bits {
207 buf.extend_from_slice(&w.to_le_bytes());
208 }
209 buf.extend_from_slice(&[0u8; 48]);
210
211 let path =
212 std::env::temp_dir().join(format!("erigon_seg_bloom_{}.kvei", std::process::id()));
213 std::fs::write(&path, &buf).unwrap();
214 let f = ExistenceFilter::open(&path).expect("open bloom");
215 let _ = std::fs::remove_file(&path);
216
217 assert_eq!(f.kind(), FilterKind::Bloom);
218 assert!(f.is_accelerating());
219 for (k, &h) in present_keys.iter().zip(&present) {
220 assert!(f.contains_hash(h), "added key {k:?} must be present");
221 assert!(f.contains_hash(murmur3_x64_128_h1(k, 9)));
222 }
223 assert!(!f.contains_hash(murmur3_x64_128_h1(b"definitely-not-added", 9)));
225 }
226
227 #[test]
228 fn empty_and_unsupported_match_all() {
229 let dir = std::env::temp_dir();
230 let empty = dir.join(format!("erigon_seg_empty_{}.kvei", std::process::id()));
231 std::fs::write(&empty, []).unwrap();
232 let f = ExistenceFilter::open(&empty).unwrap();
233 let _ = std::fs::remove_file(&empty);
234 assert_eq!(f.kind(), FilterKind::Empty);
235 assert!(!f.is_accelerating());
236 assert!(f.contains_hash(0xdead_beef)); let fuse = dir.join(format!("erigon_seg_fuse_{}.kvei", std::process::id()));
240 std::fs::write(
241 &fuse,
242 [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
243 )
244 .unwrap();
245 let f = ExistenceFilter::open(&fuse).unwrap();
246 let _ = std::fs::remove_file(&fuse);
247 assert_eq!(f.kind(), FilterKind::Unsupported);
248 assert!(f.contains_hash(123));
249 }
250}