use std::path::Path;
use memmap2::Mmap;
use crate::error::Result;
use crate::util::{Advice, advise_mmap, lock_mmap, mmap_file, preload_mmap, unlock_mmap};
const BLOOM_MAGIC: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0, b'v', b'0', b'2', b'\n'];
const BLOOM_BITS_OFFSET: usize = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FilterKind {
Empty,
Bloom,
Unsupported,
}
enum Inner {
Empty,
Bloom {
keys: [u64; 3],
m: u64,
bits_off: usize,
},
Unsupported,
}
pub struct ExistenceFilter {
mmap: Mmap,
inner: Inner,
}
impl ExistenceFilter {
pub fn open(path: impl AsRef<Path>) -> Result<ExistenceFilter> {
let mmap = mmap_file(path.as_ref())?;
let inner = Self::parse(&mmap);
Ok(ExistenceFilter { mmap, inner })
}
fn parse(d: &[u8]) -> Inner {
if d.is_empty() {
return Inner::Empty;
}
if d.len() >= BLOOM_BITS_OFFSET && d[0..12] == BLOOM_MAGIC {
let k = u64::from_le_bytes(d[12..20].try_into().unwrap());
let m = u64::from_le_bytes(d[28..36].try_into().unwrap());
let mut keys = [0u64; 3];
for (i, key) in keys.iter_mut().enumerate() {
*key = u64::from_le_bytes(d[36 + i * 8..44 + i * 8].try_into().unwrap());
}
let nwords = m.div_ceil(64) as usize;
if k == 3 && m >= 2 && BLOOM_BITS_OFFSET + nwords * 8 <= d.len() {
return Inner::Bloom {
keys,
m,
bits_off: BLOOM_BITS_OFFSET,
};
}
}
Inner::Unsupported
}
pub fn advise_random(&self) -> std::io::Result<()> {
advise_mmap(&self.mmap, Advice::Random)
}
pub fn mapped_bytes(&self) -> u64 {
self.mmap.len() as u64
}
pub fn preload(&self) -> u64 {
preload_mmap(&self.mmap) as u64
}
pub fn lock(&self) -> std::io::Result<()> {
lock_mmap(&self.mmap)
}
pub fn unlock(&self) -> std::io::Result<()> {
unlock_mmap(&self.mmap)
}
pub fn kind(&self) -> FilterKind {
match self.inner {
Inner::Empty => FilterKind::Empty,
Inner::Bloom { .. } => FilterKind::Bloom,
Inner::Unsupported => FilterKind::Unsupported,
}
}
pub fn is_accelerating(&self) -> bool {
matches!(self.inner, Inner::Bloom { .. })
}
#[inline]
fn bit_word(&self, bits_off: usize, idx: usize) -> u64 {
let off = bits_off + idx * 8;
u64::from_le_bytes(self.mmap[off..off + 8].try_into().unwrap())
}
#[inline]
pub fn contains_hash(&self, mut hash: u64) -> bool {
let (keys, m, bits_off) = match &self.inner {
Inner::Bloom { keys, m, bits_off } => (keys, *m, *bits_off),
Inner::Empty | Inner::Unsupported => return true,
};
let mut r = 1u64;
for &key in keys {
if r == 0 {
break;
}
hash = hash.rotate_left(17) ^ key;
let i = hash % m;
r &= (self.bit_word(bits_off, (i >> 6) as usize) >> (i & 0x3f)) & 1;
}
r != 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::murmur3_x64_128_h1;
#[test]
fn bloom_roundtrip_via_file() {
let m: u64 = 4096;
let keys: [u64; 3] = [
0x1111_2222_3333_4444,
0xaaaa_bbbb_cccc_dddd,
0xdead_beef_0bad_f00d,
];
let nwords = (m as usize).div_ceil(64);
let mut bits = vec![0u64; nwords];
let add = |bits: &mut [u64], mut h: u64| {
for &k in &keys {
h = h.rotate_left(17) ^ k;
let i = h % m;
bits[(i >> 6) as usize] |= 1 << (i & 63);
}
};
let present_keys: [&[u8]; 3] = [b"alpha", b"bravo-key", b"0123456789abcdef0123"];
let present: Vec<u64> = present_keys
.iter()
.map(|k| murmur3_x64_128_h1(k, 9))
.collect();
for &h in &present {
add(&mut bits, h);
}
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&BLOOM_MAGIC);
buf.extend_from_slice(&3u64.to_le_bytes());
buf.extend_from_slice(&(present.len() as u64).to_le_bytes());
buf.extend_from_slice(&m.to_le_bytes());
for k in keys {
buf.extend_from_slice(&k.to_le_bytes());
}
for w in &bits {
buf.extend_from_slice(&w.to_le_bytes());
}
buf.extend_from_slice(&[0u8; 48]);
let path =
std::env::temp_dir().join(format!("erigon_seg_bloom_{}.kvei", std::process::id()));
std::fs::write(&path, &buf).unwrap();
let f = ExistenceFilter::open(&path).expect("open bloom");
let _ = std::fs::remove_file(&path);
assert_eq!(f.kind(), FilterKind::Bloom);
assert!(f.is_accelerating());
for (k, &h) in present_keys.iter().zip(&present) {
assert!(f.contains_hash(h), "added key {k:?} must be present");
assert!(f.contains_hash(murmur3_x64_128_h1(k, 9)));
}
assert!(!f.contains_hash(murmur3_x64_128_h1(b"definitely-not-added", 9)));
}
#[test]
fn empty_and_unsupported_match_all() {
let dir = std::env::temp_dir();
let empty = dir.join(format!("erigon_seg_empty_{}.kvei", std::process::id()));
std::fs::write(&empty, []).unwrap();
let f = ExistenceFilter::open(&empty).unwrap();
let _ = std::fs::remove_file(&empty);
assert_eq!(f.kind(), FilterKind::Empty);
assert!(!f.is_accelerating());
assert!(f.contains_hash(0xdead_beef));
let fuse = dir.join(format!("erigon_seg_fuse_{}.kvei", std::process::id()));
std::fs::write(
&fuse,
[1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
)
.unwrap();
let f = ExistenceFilter::open(&fuse).unwrap();
let _ = std::fs::remove_file(&fuse);
assert_eq!(f.kind(), FilterKind::Unsupported);
assert!(f.contains_hash(123));
}
}