use std::collections::BTreeMap;
use sha2::{Digest, Sha256};
pub type Fingerprint = [u8; 32];
pub const EMPTY: Fingerprint = [0u8; 32];
pub type Item = (String, BTreeMap<String, i32>);
pub fn item_fingerprint(subject: &str, vv: &BTreeMap<String, i32>) -> Fingerprint {
let pairs = vv
.iter()
.map(|(peer, counter)| format!("{peer}:{counter}"))
.collect::<Vec<_>>()
.join(",");
let mut hasher = Sha256::new();
hasher.update(subject.as_bytes());
hasher.update(b"=");
hasher.update(pairs.as_bytes());
hasher.finalize().into()
}
fn xor_into(acc: &mut Fingerprint, x: &Fingerprint) {
for (a, b) in acc.iter_mut().zip(x.iter()) {
*a ^= *b;
}
}
pub fn range_fingerprint(items: &[Item], lo: &str, hi: Option<&str>) -> Fingerprint {
let mut fp = EMPTY;
for (subject, vv) in items_in_range(items, lo, hi) {
let item = item_fingerprint(subject, vv);
xor_into(&mut fp, &item);
}
fp
}
fn items_in_range<'a>(
items: &'a [Item],
lo: &str,
hi: Option<&str>,
) -> impl Iterator<Item = &'a Item> {
let lo = lo.to_string();
let hi = hi.map(|h| h.to_string());
items.iter().filter(move |(s, _)| {
s.as_str() >= lo.as_str() && hi.as_deref().map(|h| s.as_str() < h).unwrap_or(true)
})
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Diff {
pub only_local: Vec<String>,
pub only_remote: Vec<String>,
pub differ: Vec<String>,
}
impl Diff {
pub fn is_empty(&self) -> bool {
self.only_local.is_empty() && self.only_remote.is_empty() && self.differ.is_empty()
}
}
pub trait RemoteRange {
fn fingerprint(&mut self, lo: &str, hi: Option<&str>) -> Fingerprint;
fn items(&mut self, lo: &str, hi: Option<&str>) -> Vec<Item>;
}
pub fn reconcile(local: &[Item], remote: &mut impl RemoteRange, split: usize, leaf: usize) -> Diff {
let split = split.max(2);
let leaf = leaf.max(1);
let mut diff = Diff::default();
reconcile_range(local, "", None, remote, split, leaf, &mut diff);
diff
}
#[allow(clippy::too_many_arguments)]
fn reconcile_range(
local: &[Item],
lo: &str,
hi: Option<&str>,
remote: &mut impl RemoteRange,
split: usize,
leaf: usize,
out: &mut Diff,
) {
let local_fp = range_fingerprint(local, lo, hi);
let remote_fp = remote.fingerprint(lo, hi);
if local_fp == remote_fp {
return;
}
let local_slice: Vec<&Item> = items_in_range(local, lo, hi).collect();
if local_slice.len() <= leaf {
let remote_slice = remote.items(lo, hi);
diff_slices(&local_slice, &remote_slice, out);
return;
}
let chunk = local_slice.len().div_ceil(split);
let mut idx = 0;
while idx < local_slice.len() {
let chunk_lo = if idx == 0 {
lo
} else {
local_slice[idx].0.as_str()
};
let next = (idx + chunk).min(local_slice.len());
let chunk_hi = if next < local_slice.len() {
Some(local_slice[next].0.as_str())
} else {
hi
};
reconcile_range(local, chunk_lo, chunk_hi, remote, split, leaf, out);
idx = next;
}
}
fn diff_slices(local: &[&Item], remote: &[Item], out: &mut Diff) {
let mut i = 0;
let mut j = 0;
while i < local.len() && j < remote.len() {
let (ls, lvv) = local[i];
let (rs, rvv) = &remote[j];
match ls.as_str().cmp(rs.as_str()) {
std::cmp::Ordering::Less => {
out.only_local.push(ls.clone());
i += 1;
}
std::cmp::Ordering::Greater => {
out.only_remote.push(rs.clone());
j += 1;
}
std::cmp::Ordering::Equal => {
if lvv != rvv {
out.differ.push(ls.clone());
}
i += 1;
j += 1;
}
}
}
for (ls, _) in &local[i..] {
out.only_local.push(ls.clone());
}
for (rs, _) in &remote[j..] {
out.only_remote.push(rs.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vv(pairs: &[(&str, i32)]) -> BTreeMap<String, i32> {
pairs.iter().map(|(p, c)| (p.to_string(), *c)).collect()
}
fn item(subject: &str, pairs: &[(&str, i32)]) -> Item {
(subject.to_string(), vv(pairs))
}
struct MemRemote {
items: Vec<Item>,
fp_calls: usize,
}
impl MemRemote {
fn new(mut items: Vec<Item>) -> Self {
items.sort_by(|a, b| a.0.cmp(&b.0));
Self { items, fp_calls: 0 }
}
}
impl RemoteRange for MemRemote {
fn fingerprint(&mut self, lo: &str, hi: Option<&str>) -> Fingerprint {
self.fp_calls += 1;
range_fingerprint(&self.items, lo, hi)
}
fn items(&mut self, lo: &str, hi: Option<&str>) -> Vec<Item> {
items_in_range(&self.items, lo, hi).cloned().collect()
}
}
fn sorted(mut items: Vec<Item>) -> Vec<Item> {
items.sort_by(|a, b| a.0.cmp(&b.0));
items
}
#[test]
fn a_remote_only_subject_below_every_local_key_is_still_found() {
let local = sorted(vec![
item("b", &[("p1", 1)]),
item("c", &[("p1", 1)]),
item("d", &[("p1", 1)]),
item("e", &[("p1", 1)]),
item("f", &[("p1", 1)]),
]);
let mut remote = MemRemote::new({
let mut items = local.clone();
items.push(item("a", &[("p1", 1)]));
items
});
let diff = reconcile(&local, &mut remote, 4, 2);
assert_eq!(
diff.only_remote,
vec!["a".to_string()],
"the remote-only subject sorting below every local key must be \
reported, or it never syncs"
);
assert!(diff.only_local.is_empty());
assert!(diff.differ.is_empty());
}
#[test]
fn a_remote_only_subject_below_everything_is_found_through_deep_recursion() {
let local = sorted(
(0..24)
.map(|n| item(&format!("k{n:02}"), &[("p1", 1)]))
.collect(),
);
let mut remote = MemRemote::new({
let mut items = local.clone();
items.push(item("a", &[("p1", 1)]));
items
});
let diff = reconcile(&local, &mut remote, 4, 2);
assert_eq!(diff.only_remote, vec!["a".to_string()]);
assert!(diff.only_local.is_empty());
assert!(diff.differ.is_empty());
}
#[test]
fn identical_sets_reconcile_with_a_single_root_comparison() {
let items = sorted(vec![
item("a", &[("p1", 1)]),
item("b", &[("p1", 2)]),
item("c", &[("p2", 3)]),
]);
let mut remote = MemRemote::new(items.clone());
let diff = reconcile(&items, &mut remote, 4, 2);
assert!(
diff.is_empty(),
"identical sets must produce no diff: {diff:?}"
);
assert_eq!(
remote.fp_calls, 1,
"a matching root fingerprint must end the reconcile in one comparison"
);
}
#[test]
fn detects_a_single_changed_version_vector() {
let local = sorted(vec![item("a", &[("p1", 1)]), item("b", &[("p1", 2)])]);
let mut remote = MemRemote::new(vec![item("a", &[("p1", 1)]), item("b", &[("p1", 5)])]);
let diff = reconcile(&local, &mut remote, 4, 2);
assert_eq!(diff.differ, vec!["b".to_string()]);
assert!(diff.only_local.is_empty() && diff.only_remote.is_empty());
}
#[test]
fn detects_local_only_and_remote_only_subjects() {
let local = sorted(vec![item("a", &[("p1", 1)]), item("local", &[("p1", 1)])]);
let mut remote =
MemRemote::new(vec![item("a", &[("p1", 1)]), item("remote", &[("p1", 1)])]);
let diff = reconcile(&local, &mut remote, 4, 2);
assert_eq!(diff.only_local, vec!["local".to_string()]);
assert_eq!(diff.only_remote, vec!["remote".to_string()]);
assert!(diff.differ.is_empty());
}
#[test]
fn empty_local_pulls_every_remote_subject() {
let local: Vec<Item> = vec![];
let mut remote = MemRemote::new(vec![item("a", &[("p1", 1)]), item("b", &[("p1", 2)])]);
let diff = reconcile(&local, &mut remote, 4, 2);
assert_eq!(diff.only_remote, vec!["a".to_string(), "b".to_string()]);
assert!(diff.only_local.is_empty() && diff.differ.is_empty());
}
#[test]
fn finds_one_change_in_a_large_set_without_scanning_everything() {
let n = 256usize;
let mut base: Vec<Item> = (0..n)
.map(|i| item(&format!("subject-{i:04}"), &[("p1", i as i32)]))
.collect();
base = sorted(base);
let local = base.clone();
let mut remote_items = base.clone();
let target = "subject-0123".to_string();
for (s, v) in remote_items.iter_mut() {
if *s == target {
v.insert("p1".to_string(), 9999);
}
}
let mut remote = MemRemote::new(remote_items);
let diff = reconcile(&local, &mut remote, 4, 4);
assert_eq!(diff.differ, vec![target]);
assert!(diff.only_local.is_empty() && diff.only_remote.is_empty());
assert!(
remote.fp_calls < 40,
"expected a logarithmic number of fingerprint comparisons, got {} for {n} items",
remote.fp_calls
);
}
#[test]
fn item_fingerprint_matches_golden_vector() {
assert_eq!(
hex::encode(item_fingerprint("s", &vv(&[("p1", 1), ("p2", 2)]))),
"8b6067440e370aeaf5e85936d9d67477224a664f8b2811a2008309b590edd5d8",
);
}
#[test]
fn item_fingerprint_is_deterministic_and_order_independent_in_vv() {
let a = item_fingerprint("s", &vv(&[("p1", 1), ("p2", 2)]));
let b = item_fingerprint("s", &vv(&[("p2", 2), ("p1", 1)]));
assert_eq!(a, b);
let c = item_fingerprint("s", &vv(&[("p1", 1), ("p2", 3)]));
assert_ne!(a, c);
}
}