use crate::{
types::{ArchivedInactivityDiff, InactivityDiff},
Error,
};
pub fn diff_inactivity(base: &[u64], target: &[u64]) -> InactivityDiff {
let target_is_zero = target.iter().all(|&v| v == 0);
if target_is_zero {
let base_has_non_zero = base.iter().any(|&v| v != 0);
if base_has_non_zero {
let len =
u32::try_from(target.len()).expect("inactivity-score vector length exceeds u32");
return InactivityDiff::AllZeros(len);
}
}
let common_len = base.len().min(target.len());
let mut indices = Vec::with_capacity(100);
let mut new_values = Vec::with_capacity(100);
for (i, (&v1, &v2)) in base.iter().zip(target.iter()).take(common_len).enumerate() {
if v1 != v2 {
let idx = u32::try_from(i).expect("inactivity-score index exceeds u32");
indices.push(idx);
new_values.push(v2);
}
}
let extensions = target
.get(common_len..)
.expect("common_len is bounded by target length")
.to_vec();
InactivityDiff::Sparse {
indices,
new_values,
extensions,
}
}
pub fn apply_inactivity(base: &mut Vec<u64>, delta: &ArchivedInactivityDiff) -> Result<(), Error> {
match delta {
ArchivedInactivityDiff::AllZeros(len) => {
let len = usize::try_from(len.to_native()).map_err(|_| {
Error::InvalidDelta("inactivity-score vector length does not fit in usize".into())
})?;
base.clear();
base.resize(len, 0);
}
ArchivedInactivityDiff::Sparse {
indices,
new_values,
extensions,
} => {
if indices.len() != new_values.len() {
return Err(Error::InvalidDelta(format!(
"inactivity-score delta contains {} indices but {} replacement values",
indices.len(),
new_values.len()
)));
}
for (idx, val) in indices.iter().zip(new_values.iter()) {
let index = usize::try_from(idx.to_native()).map_err(|_| {
Error::InvalidDelta("inactivity-score index does not fit in usize".into())
})?;
let Some(value) = base.get_mut(index) else {
return Err(Error::InvalidDelta(format!(
"inactivity-score index {index} is outside base vector of length {}",
base.len()
)));
};
*value = val.to_native();
}
base.reserve(extensions.len());
base.extend(extensions.iter().map(|value| value.to_native()));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ArchivedInactivityDiff;
#[test]
fn test_diff_all_zeros_fast_path() {
let base = vec![10, 20, 30];
let target = vec![0, 0, 0];
let delta = diff_inactivity(&base, &target);
assert!(matches!(delta, InactivityDiff::AllZeros(3)));
}
#[test]
fn test_diff_sparse_identical() {
let base = vec![10, 20, 30];
let target = vec![10, 20, 30];
let delta = diff_inactivity(&base, &target);
match delta {
InactivityDiff::Sparse {
indices,
new_values,
extensions,
} => {
assert!(indices.is_empty());
assert!(new_values.is_empty());
assert!(extensions.is_empty());
}
_ => panic!("test setup: expected Sparse"),
}
}
#[test]
fn test_sparse_with_changes_and_extensions() {
let base = vec![10, 20, 30, 40];
let target = vec![10, 25, 30, 50, 60];
let delta = diff_inactivity(&base, &target);
match delta {
InactivityDiff::Sparse {
indices,
new_values,
extensions,
} => {
assert_eq!(*indices, [1u32, 3u32]);
assert_eq!(*new_values, [25u64, 50u64]);
assert_eq!(*extensions, [60u64]); }
_ => panic!("test setup: expected Sparse"),
}
}
#[test]
fn test_apply_all_zeros() {
let mut base = vec![10, 20, 30];
let delta = InactivityDiff::AllZeros(5);
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedInactivityDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
apply_inactivity(&mut base, archived).expect("test setup: apply");
assert_eq!(base, vec![0; 5]);
}
#[test]
fn test_apply_sparse_roundtrip() {
let base = vec![10, 20, 30, 40];
let target = vec![10, 25, 30, 50, 60];
let delta = diff_inactivity(&base, &target);
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedInactivityDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut reconstructed = base;
apply_inactivity(&mut reconstructed, archived).expect("test setup: apply");
assert_eq!(reconstructed, target);
}
#[test]
fn test_apply_sparse_mismatched_lengths() {
let delta = InactivityDiff::Sparse {
indices: vec![0u32],
new_values: vec![],
extensions: vec![],
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedInactivityDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut base = vec![0u64; 5];
let result = apply_inactivity(&mut base, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("indices but 0 replacement values"));
}
#[test]
fn test_apply_sparse_index_out_of_bounds() {
let delta = InactivityDiff::Sparse {
indices: vec![10u32],
new_values: vec![99u64],
extensions: vec![],
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedInactivityDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut base = vec![0u64; 5];
let result = apply_inactivity(&mut base, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("index 10 is outside base vector of length 5"));
}
}