1use std::io::{self, Write};
27
28pub(crate) const FNV_OFFSET: u64 = 0xcbf29ce484222325;
29pub(crate) const FNV_PRIME: u64 = 0x100000001b3;
30pub const BUCKET_SIZE: usize = 4;
33pub const MAX_KICKS: usize = 500;
35pub const FINGERPRINT_BITS: u32 = 8;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CuckooError {
42 NotEnoughSpace,
46 GeometryMismatch { lhs: usize, rhs: usize },
50}
51
52impl std::fmt::Display for CuckooError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 CuckooError::NotEnoughSpace => write!(f, "cuckoo filter is saturated"),
56 CuckooError::GeometryMismatch { lhs, rhs } => {
57 write!(f, "incompatible cuckoo geometry: {lhs} buckets vs {rhs}")
58 }
59 }
60 }
61}
62
63impl std::error::Error for CuckooError {}
64
65pub struct CuckooFilter {
66 buckets: Vec<[u8; BUCKET_SIZE]>,
67 mask: usize,
68 count: usize,
69 rng_state: u64,
70 victim_fp: u8,
75 victim_bucket: usize,
76}
77
78impl CuckooFilter {
79 pub fn with_capacity(expected_entries: usize) -> Self {
82 let needed = (expected_entries.max(1) * 105 / 100) / BUCKET_SIZE + 1;
83 let num_buckets = needed.max(2).next_power_of_two();
84 Self {
85 buckets: vec![[0u8; BUCKET_SIZE]; num_buckets],
86 mask: num_buckets - 1,
87 count: 0,
88 rng_state: 0x9e3779b97f4a7c15,
89 victim_fp: 0,
90 victim_bucket: 0,
91 }
92 }
93
94 pub fn len(&self) -> usize {
95 self.count
96 }
97 pub fn is_empty(&self) -> bool {
98 self.count == 0
99 }
100 pub fn bucket_count(&self) -> usize {
101 self.buckets.len()
102 }
103
104 pub fn capacity(&self) -> usize {
107 self.buckets.len() * BUCKET_SIZE
108 }
109
110 pub fn load_factor(&self) -> f64 {
113 self.count as f64 / self.capacity() as f64
114 }
115
116 pub fn size_in_bytes(&self) -> usize {
119 self.buckets.len() * BUCKET_SIZE
120 }
121
122 pub fn estimated_fpp(&self) -> f64 {
127 let alpha = self.load_factor();
128 if alpha <= 0.0 {
129 return 0.0;
130 }
131 let per_slot = 1.0 - 2f64.powi(-(FINGERPRINT_BITS as i32));
132 1.0 - per_slot.powf(2.0 * BUCKET_SIZE as f64 * alpha)
133 }
134
135 pub fn clear(&mut self) {
139 self.buckets.fill([0u8; BUCKET_SIZE]);
140 self.count = 0;
141 self.victim_fp = 0;
142 self.victim_bucket = 0;
143 }
144
145 pub fn insert(&mut self, key: &str) -> bool {
149 self.insert_bytes(key.as_bytes())
150 }
151
152 pub fn insert_bytes(&mut self, key: &[u8]) -> bool {
156 let (fp, i1, i2) = self.indices_bytes(key);
157 self.place(fp, i1, i2)
158 }
159
160 pub fn insert_if_absent(&mut self, key: &str) -> bool {
166 self.insert_if_absent_bytes(key.as_bytes())
167 }
168
169 pub fn insert_if_absent_bytes(&mut self, key: &[u8]) -> bool {
170 let (fp, i1, i2) = self.indices_bytes(key);
171 if self.bucket_has(i1, fp) || self.bucket_has(i2, fp) || self.victim_matches(fp, i1, i2) {
172 return false;
173 }
174 self.place(fp, i1, i2)
175 }
176
177 pub fn try_insert(&mut self, key: &str) -> Result<(), CuckooError> {
179 if self.insert(key) {
180 Ok(())
181 } else {
182 Err(CuckooError::NotEnoughSpace)
183 }
184 }
185
186 pub fn contains(&self, key: &str) -> bool {
192 self.contains_bytes(key.as_bytes())
193 }
194
195 pub fn contains_bytes(&self, key: &[u8]) -> bool {
196 let (fp, i1, i2) = self.indices_bytes(key);
197 self.bucket_has(i1, fp) || self.bucket_has(i2, fp) || self.victim_matches(fp, i1, i2)
198 }
199
200 pub fn delete(&mut self, key: &str) -> bool {
202 self.delete_bytes(key.as_bytes())
203 }
204
205 pub fn delete_bytes(&mut self, key: &[u8]) -> bool {
206 let (fp, i1, i2) = self.indices_bytes(key);
207 if self.bucket_remove(i1, fp) || self.bucket_remove(i2, fp) {
208 self.count -= 1;
209 self.rehome_victim();
210 return true;
211 }
212 if self.victim_matches(fp, i1, i2) {
213 self.victim_fp = 0;
214 self.count -= 1;
215 return true;
216 }
217 false
218 }
219
220 pub fn union(&mut self, other: &CuckooFilter) -> Result<(), CuckooError> {
230 if self.buckets.len() != other.buckets.len() {
231 return Err(CuckooError::GeometryMismatch {
232 lhs: self.buckets.len(),
233 rhs: other.buckets.len(),
234 });
235 }
236 for (i, bucket) in other.buckets.iter().enumerate() {
237 for &fp in bucket {
238 if fp != 0 && !self.place(fp, i, (i ^ alt_index_of_fp(fp)) & self.mask) {
239 return Err(CuckooError::NotEnoughSpace);
240 }
241 }
242 }
243 if other.victim_fp != 0 {
244 let i1 = other.victim_bucket;
245 let i2 = (i1 ^ alt_index_of_fp(other.victim_fp)) & self.mask;
246 if !self.place(other.victim_fp, i1, i2) {
247 return Err(CuckooError::NotEnoughSpace);
248 }
249 }
250 Ok(())
251 }
252
253 pub fn write_to<W: Write>(&self, out: &mut W) -> io::Result<()> {
259 out.write_all(&(self.buckets.len() as u32).to_be_bytes())?;
260 out.write_all(&(self.count as u64).to_be_bytes())?;
261 out.write_all(&[self.victim_fp])?;
262 out.write_all(&(self.victim_bucket as u32).to_be_bytes())?;
263 for b in &self.buckets {
264 out.write_all(b)?;
265 }
266 Ok(())
267 }
268
269 pub fn parse(buf: &[u8]) -> io::Result<Self> {
274 const HEADER: usize = 4 + 8 + 1 + 4;
275 if buf.len() < HEADER {
276 return Err(io::Error::new(
277 io::ErrorKind::InvalidData,
278 "cuckoo header too short",
279 ));
280 }
281 let num_buckets = u32::from_be_bytes(buf[0..4].try_into().unwrap()) as usize;
282 let count = u64::from_be_bytes(buf[4..12].try_into().unwrap()) as usize;
283 let victim_fp = buf[12];
284 let victim_bucket = u32::from_be_bytes(buf[13..17].try_into().unwrap()) as usize;
285 if num_buckets < 2 || !num_buckets.is_power_of_two() {
286 return Err(io::Error::new(
287 io::ErrorKind::InvalidData,
288 "cuckoo bucket count must be a power of two >= 2",
289 ));
290 }
291 if buf.len() < HEADER + num_buckets * BUCKET_SIZE {
292 return Err(io::Error::new(
293 io::ErrorKind::InvalidData,
294 "cuckoo body truncated",
295 ));
296 }
297 if victim_bucket >= num_buckets {
298 return Err(io::Error::new(
299 io::ErrorKind::InvalidData,
300 "cuckoo victim bucket out of range",
301 ));
302 }
303 let mut buckets = Vec::with_capacity(num_buckets);
304 for i in 0..num_buckets {
305 let off = HEADER + i * BUCKET_SIZE;
306 let mut b = [0u8; BUCKET_SIZE];
307 b.copy_from_slice(&buf[off..off + BUCKET_SIZE]);
308 buckets.push(b);
309 }
310 Ok(Self {
311 buckets,
312 mask: num_buckets - 1,
313 count,
314 rng_state: 0x9e3779b97f4a7c15,
315 victim_fp,
316 victim_bucket,
317 })
318 }
319
320 fn place(&mut self, fp: u8, i1: usize, i2: usize) -> bool {
321 if self.try_place(i1, fp) || self.try_place(i2, fp) {
322 self.count += 1;
323 return true;
324 }
325 if self.victim_fp != 0 {
326 return false;
327 }
328 let mut bucket_idx = if self.rand_bit() { i1 } else { i2 };
329 let mut victim = fp;
330 for _ in 0..MAX_KICKS {
331 let slot = (self.next_random() as usize) % BUCKET_SIZE;
332 std::mem::swap(&mut victim, &mut self.buckets[bucket_idx][slot]);
333 bucket_idx ^= alt_index_of_fp(victim) & self.mask;
334 if self.try_place(bucket_idx, victim) {
335 self.count += 1;
336 return true;
337 }
338 }
339 self.victim_fp = victim;
342 self.victim_bucket = bucket_idx;
343 self.count += 1;
344 true
345 }
346
347 fn victim_matches(&self, fp: u8, i1: usize, i2: usize) -> bool {
348 self.victim_fp == fp && (self.victim_bucket == i1 || self.victim_bucket == i2)
349 }
350
351 fn rehome_victim(&mut self) {
355 if self.victim_fp == 0 {
356 return;
357 }
358 let fp = self.victim_fp;
359 let alt = (self.victim_bucket ^ alt_index_of_fp(fp)) & self.mask;
360 if self.try_place(self.victim_bucket, fp) || self.try_place(alt, fp) {
361 self.victim_fp = 0;
362 }
363 }
364
365 fn indices_bytes(&self, key: &[u8]) -> (u8, usize, usize) {
366 let h = mix(fnv1a64(key));
367 let fp = ((h & 0xff) as u8).max(1);
370 let i1 = (h >> 8) as usize & self.mask;
371 let i2 = (i1 ^ alt_index_of_fp(fp)) & self.mask;
372 (fp, i1, i2)
373 }
374
375 fn try_place(&mut self, i: usize, fp: u8) -> bool {
376 for slot in &mut self.buckets[i] {
377 if *slot == 0 {
378 *slot = fp;
379 return true;
380 }
381 }
382 false
383 }
384
385 fn bucket_has(&self, i: usize, fp: u8) -> bool {
386 self.buckets[i].contains(&fp)
387 }
388
389 fn bucket_remove(&mut self, i: usize, fp: u8) -> bool {
390 for slot in &mut self.buckets[i] {
391 if *slot == fp {
392 *slot = 0;
393 return true;
394 }
395 }
396 false
397 }
398
399 fn rand_bit(&mut self) -> bool {
400 self.next_random() & 1 != 0
401 }
402
403 fn next_random(&mut self) -> u64 {
404 self.rng_state = self
405 .rng_state
406 .wrapping_mul(6364136223846793005)
407 .wrapping_add(1442695040888963407);
408 self.rng_state
409 }
410}
411
412#[cfg(feature = "concurrent-reads")]
413impl CuckooFilter {
414 pub(crate) fn buckets_view(&self) -> &[[u8; BUCKET_SIZE]] {
417 &self.buckets
418 }
419
420 pub(crate) fn mask_view(&self) -> usize {
421 self.mask
422 }
423
424 pub(crate) fn victim_view(&self) -> (u8, usize) {
426 (self.victim_fp, self.victim_bucket)
427 }
428}
429
430pub(crate) fn alt_index_of_fp(fp: u8) -> usize {
434 (fp as u64).wrapping_mul(0x5bd1e9955_u64) as usize
435}
436
437pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
438 let mut h = FNV_OFFSET;
439 for &b in bytes {
440 h ^= b as u64;
441 h = h.wrapping_mul(FNV_PRIME);
442 }
443 h
444}
445
446pub(crate) fn mix(mut h: u64) -> u64 {
447 h ^= h >> 30;
448 h = h.wrapping_mul(0xbf58476d1ce4e5b9);
449 h ^= h >> 27;
450 h = h.wrapping_mul(0x94d049bb133111eb);
451 h ^= h >> 31;
452 h
453}
454
455#[cfg(test)]
456#[path = "cuckoo_tests.rs"]
457mod cuckoo_tests;
458
459#[cfg(test)]
460#[path = "sample_app_tests.rs"]
461mod sample_app_tests;
462
463#[cfg(feature = "harness")]
464pub mod recipe;
465
466#[cfg(any(
469 feature = "variable-fingerprint",
470 feature = "dynamic",
471 feature = "concurrent-reads",
472 feature = "compressed-buckets",
473))]
474pub mod features;
475
476#[cfg(feature = "compressed-buckets")]
477pub use features::compressed_buckets::CompressedCuckooFilter;
478#[cfg(feature = "concurrent-reads")]
479pub use features::concurrent_reads::CuckooSnapshot;
480#[cfg(feature = "dynamic")]
481pub use features::dynamic::DynamicCuckooFilter;
482#[cfg(feature = "variable-fingerprint")]
483pub use features::variable_fingerprint::{FingerprintWidth, VariableFpCuckooFilter};