use alloc::vec;
use alloc::vec::Vec;
use crate::{Error, Result};
const HASH_COUNT: usize = 3;
const POSITION_SEEDS: [u64; HASH_COUNT] = [
0x9e37_79b9_7f4a_7c15,
0xbf58_476d_1ce4_e5b9,
0x94d0_49bb_1331_11eb,
];
pub fn splitmix64(mut value: u64) -> u64 {
value = value.wrapping_add(0x9e37_79b9_7f4a_7c15);
value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
value ^ (value >> 31)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Cell {
pub count: i32,
pub key_sum: u64,
pub value_sum: u64,
pub hash_sum: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IbltEntry {
pub key: u64,
pub value: u64,
pub present_in_self: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Iblt {
cells: Vec<Cell>,
}
impl Iblt {
pub fn new(num_cells: usize) -> Self {
Self {
cells: vec![Cell::default(); num_cells.max(1)],
}
}
pub fn num_cells(&self) -> usize {
self.cells.len()
}
pub fn occupied(&self) -> usize {
self.cells.iter().filter(|cell| cell.count != 0).count()
}
pub fn is_empty(&self) -> bool {
self.cells.iter().all(|cell| cell.count == 0)
}
pub fn insert(&mut self, key: u64, value: u64) {
let hash = splitmix64(key);
for seed in POSITION_SEEDS {
let position = (splitmix64(key ^ seed) as usize) % self.cells.len();
let cell = &mut self.cells[position];
cell.count += 1;
cell.key_sum ^= key;
cell.value_sum ^= value;
cell.hash_sum ^= hash;
}
}
pub fn delete(&mut self, key: u64, value: u64) {
let hash = splitmix64(key);
for seed in POSITION_SEEDS {
let position = (splitmix64(key ^ seed) as usize) % self.cells.len();
let cell = &mut self.cells[position];
cell.count -= 1;
cell.key_sum ^= key;
cell.value_sum ^= value;
cell.hash_sum ^= hash;
}
}
pub fn subtract(&mut self, other: &Iblt) -> Result<()> {
if self.cells.len() != other.cells.len() {
return Err(Error::Iblt("IBLT size mismatch in subtract"));
}
for (left, right) in self.cells.iter_mut().zip(&other.cells) {
left.count -= right.count;
left.key_sum ^= right.key_sum;
left.value_sum ^= right.value_sum;
left.hash_sum ^= right.hash_sum;
}
Ok(())
}
pub fn decode(&self) -> Result<Vec<IbltEntry>> {
let mut table = self.clone();
let mut entries = Vec::new();
loop {
let mut progress = false;
for position in 0..table.cells.len() {
let count = table.cells[position].count;
if count == 1 || count == -1 {
let key = table.cells[position].key_sum;
let value = table.cells[position].value_sum;
if splitmix64(key) != table.cells[position].hash_sum {
return Err(Error::Iblt("IBLT cell hash does not verify"));
}
if count == 1 {
table.delete(key, value);
entries.push(IbltEntry {
key,
value,
present_in_self: true,
});
} else {
table.insert(key, value);
entries.push(IbltEntry {
key,
value,
present_in_self: false,
});
}
progress = true;
break;
}
}
if !progress {
break;
}
}
if !table.is_empty() {
return Err(Error::Iblt("IBLT decode incomplete (table too small?)"));
}
Ok(entries)
}
pub fn cells(&self) -> &[Cell] {
&self.cells
}
}
pub fn encode_set(entries: &[(u64, u64)], num_cells: usize) -> Iblt {
let mut table = Iblt::new(num_cells);
for &(key, value) in entries {
table.insert(key, value);
}
table
}
pub fn reconcile(mine: &Iblt, theirs: &Iblt) -> Result<Vec<IbltEntry>> {
let mut difference = mine.clone();
difference.subtract(theirs)?;
difference.decode()
}
#[cfg(test)]
mod tests {
use super::*;
fn random_pairs(seed: u64, count: usize) -> Vec<(u64, u64)> {
let mut state = seed;
let mut pairs = Vec::with_capacity(count);
for index in 0..count {
state = splitmix64(state);
let key = state ^ (index as u64).wrapping_mul(0x9e37_79b9);
pairs.push((key, state.wrapping_mul(3) + index as u64));
}
pairs
}
#[test]
fn insert_delete_roundtrips_and_decodes_empty() {
let mut table = Iblt::new(16);
let pairs = random_pairs(0x1234, 8);
for &(key, value) in &pairs {
table.insert(key, value);
}
assert!(!table.is_empty());
for &(key, value) in &pairs {
table.delete(key, value);
}
assert!(table.is_empty());
assert_eq!(table.decode().unwrap(), Vec::new());
}
#[test]
fn reconcile_recovers_exact_difference() {
let common = random_pairs(0xabcd, 200);
let mut mine_set = common.clone();
mine_set.extend(random_pairs(0x1111, 10));
let mut theirs_set = common.clone();
theirs_set.extend(random_pairs(0x2222, 7));
let mine = encode_set(&mine_set, 256);
let theirs = encode_set(&theirs_set, 256);
let difference = reconcile(&mine, &theirs).unwrap();
let only_mine: Vec<(u64, u64)> = difference
.iter()
.filter(|entry| entry.present_in_self)
.map(|entry| (entry.key, entry.value))
.collect();
let only_theirs: Vec<(u64, u64)> = difference
.iter()
.filter(|entry| !entry.present_in_self)
.map(|entry| (entry.key, entry.value))
.collect();
let mut expected_mine = random_pairs(0x1111, 10);
expected_mine.sort_unstable();
let mut expected_theirs = random_pairs(0x2222, 7);
expected_theirs.sort_unstable();
let mut actual_mine = only_mine;
actual_mine.sort_unstable();
let mut actual_theirs = only_theirs;
actual_theirs.sort_unstable();
assert_eq!(actual_mine, expected_mine);
assert_eq!(actual_theirs, expected_theirs);
}
#[test]
fn undersized_table_reports_incomplete_decode() {
let mine_set = random_pairs(0x3333, 30);
let theirs_set: Vec<(u64, u64)> = Vec::new();
let mine = encode_set(&mine_set, 8);
let theirs = encode_set(&theirs_set, 8);
assert!(matches!(
reconcile(&mine, &theirs),
Err(Error::Iblt("IBLT decode incomplete (table too small?)"))
));
}
#[test]
fn identical_sets_reconcile_to_empty() {
let pairs = random_pairs(0x5555, 64);
let mine = encode_set(&pairs, 128);
let theirs = encode_set(&pairs, 128);
assert!(reconcile(&mine, &theirs).unwrap().is_empty());
}
}