use std::collections::HashMap;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use crate::checksum::{RollingChecksum, StrongHash, strong_hash};
pub const MIN_BLOCK: usize = 700;
pub const MAX_BLOCK: usize = 131_072;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Op {
Copy(u32),
Literal(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Delta {
pub block_size: u32,
pub ops: Vec<Op>,
}
impl Delta {
#[must_use]
pub fn literal_bytes(&self) -> usize {
self.ops
.iter()
.map(|op| match op {
Op::Literal(b) => b.len(),
Op::Copy(_) => 0,
})
.sum()
}
#[must_use]
pub fn copy_ops(&self) -> usize {
self.ops.iter().filter(|o| matches!(o, Op::Copy(_))).count()
}
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
pub fn block_size_for(old_len: usize) -> usize {
let approx = (old_len as f64).sqrt().round() as usize;
approx.clamp(MIN_BLOCK, MAX_BLOCK)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockSig {
pub weak: u32,
pub strong: StrongHash,
pub len: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature {
pub block_size: u32,
pub blocks: Vec<BlockSig>,
}
impl Signature {
#[must_use]
pub fn compute(old: &[u8], block_override: Option<usize>) -> Self {
let block = block_override.map_or_else(|| block_size_for(old.len()), |b| b.max(1));
let block_size = u32::try_from(block).unwrap_or(u32::MAX);
let blocks: Vec<BlockSig> = old
.par_chunks(block)
.map(|chunk| BlockSig {
weak: RollingChecksum::new(chunk).value(),
strong: strong_hash(chunk),
len: u32::try_from(chunk.len()).unwrap_or(u32::MAX),
})
.collect();
Self { block_size, blocks }
}
#[must_use]
pub fn len(&self) -> usize {
self.blocks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
}
struct Lookup<'a> {
sig: &'a Signature,
by_weak: HashMap<u32, Vec<u32>>,
}
impl<'a> Lookup<'a> {
fn new(sig: &'a Signature) -> Self {
let mut by_weak: HashMap<u32, Vec<u32>> = HashMap::new();
for (i, block) in sig.blocks.iter().enumerate() {
let idx = u32::try_from(i).unwrap_or(u32::MAX);
by_weak.entry(block.weak).or_default().push(idx);
}
Self { sig, by_weak }
}
fn find(&self, weak: u32, window: &[u8]) -> Option<u32> {
let candidates = self.by_weak.get(&weak)?;
let mut strong: Option<StrongHash> = None;
for &bi in candidates {
let block = &self.sig.blocks[bi as usize];
if block.len as usize != window.len() {
continue;
}
let sh = *strong.get_or_insert_with(|| strong_hash(window));
if block.strong == sh {
return Some(bi);
}
}
None
}
}
#[must_use]
pub fn encode_with_signature(sig: &Signature, new: &[u8]) -> Delta {
let block = sig.block_size as usize;
let block_size = sig.block_size;
if sig.blocks.is_empty() {
let ops = if new.is_empty() {
Vec::new()
} else {
vec![Op::Literal(new.to_vec())]
};
return Delta { block_size, ops };
}
let index = Lookup::new(sig);
let mut ops: Vec<Op> = Vec::new();
let n = new.len();
let mut pos = 0usize; let mut lit_start = 0usize;
let flush = |ops: &mut Vec<Op>, from: usize, to: usize| {
if to > from {
ops.push(Op::Literal(new[from..to].to_vec()));
}
};
if n >= block {
let mut rc = RollingChecksum::new(&new[..block]);
loop {
let window = &new[pos..pos + block];
if let Some(bi) = index.find(rc.value(), window) {
flush(&mut ops, lit_start, pos);
ops.push(Op::Copy(bi));
pos += block;
lit_start = pos;
if pos + block <= n {
rc = RollingChecksum::new(&new[pos..pos + block]);
continue;
}
break;
}
if pos + block < n {
rc.roll(new[pos], new[pos + block]);
pos += 1;
} else {
break;
}
}
}
let remaining = &new[lit_start..n];
if !remaining.is_empty() {
let weak = RollingChecksum::new(remaining).value();
if let Some(bi) = index.find(weak, remaining) {
ops.push(Op::Copy(bi));
} else {
ops.push(Op::Literal(remaining.to_vec()));
}
}
Delta { block_size, ops }
}
#[must_use]
pub fn encode(old: &[u8], new: &[u8], block_override: Option<usize>) -> Delta {
encode_with_signature(&Signature::compute(old, block_override), new)
}
pub fn apply(old: &[u8], delta: &Delta) -> crate::Result<Vec<u8>> {
let block = delta.block_size as usize;
let mut out: Vec<u8> = Vec::new();
for op in &delta.ops {
match op {
Op::Literal(bytes) => out.extend_from_slice(bytes),
Op::Copy(bi) => {
let start = (*bi as usize)
.checked_mul(block)
.ok_or_else(|| crate::Error::DeltaApply("block offset overflow".into()))?;
if start >= old.len() && !(start == 0 && old.is_empty()) {
return Err(crate::Error::DeltaApply(format!(
"copy block {bi} out of range"
)));
}
let end = (start + block).min(old.len());
out.extend_from_slice(&old[start..end]);
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(old: &[u8], new: &[u8], block: Option<usize>) -> Delta {
let d = encode(old, new, block);
let rebuilt = apply(old, &d).expect("apply");
assert_eq!(rebuilt, new, "roundtrip mismatch");
d
}
#[test]
fn empty_old_empty_new() {
let d = roundtrip(b"", b"", None);
assert!(d.ops.is_empty());
}
#[test]
fn empty_old_some_new() {
let d = roundtrip(b"", b"hello world", None);
assert_eq!(d.ops, vec![Op::Literal(b"hello world".to_vec())]);
}
#[test]
fn some_old_empty_new() {
let d = roundtrip(b"abcdef", b"", None);
assert!(d.ops.is_empty());
}
#[test]
fn identical_is_all_copy_zero_literals() {
let old: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
let d = roundtrip(&old, &old, Some(700));
assert_eq!(d.literal_bytes(), 0, "expected zero literals");
assert!(d.copy_ops() > 0);
assert!(d.ops.iter().all(|o| matches!(o, Op::Copy(_))));
}
#[test]
fn completely_different_is_all_literal() {
let old = vec![0u8; 4096];
let new = vec![0xFFu8; 4096];
let d = roundtrip(&old, &new, Some(700));
assert_eq!(d.copy_ops(), 0, "expected no copies");
assert_eq!(d.literal_bytes(), new.len());
}
#[test]
fn new_shorter_than_old() {
let old: Vec<u8> = (0..3000u32).map(|i| (i % 97) as u8).collect();
let new = &old[..1234];
roundtrip(&old, new, Some(700));
}
#[test]
fn new_longer_than_old() {
let old: Vec<u8> = (0..3000u32).map(|i| (i % 97) as u8).collect();
let mut new = old.clone();
new.extend(std::iter::repeat_n(0xAB, 2000));
let d = roundtrip(&old, &new, Some(700));
assert!(d.copy_ops() > 0, "prefix should be copied");
}
#[test]
fn single_byte_change_middle() {
let old: Vec<u8> = (0..8000u32).map(|i| (i % 211) as u8).collect();
let mut new = old.clone();
new[4000] ^= 0xFF;
let d = roundtrip(&old, &new, Some(700));
assert!(d.copy_ops() > 0);
assert!(
d.literal_bytes() < 2 * 700,
"only the changed block should differ"
);
}
#[test]
fn insertion_at_front_shifts_everything() {
let old: Vec<u8> = (0..8000u32).map(|i| (i % 197) as u8).collect();
let mut new = vec![1u8, 2, 3, 4, 5];
new.extend_from_slice(&old);
let d = roundtrip(&old, &new, Some(700));
assert!(d.copy_ops() > 0, "shifted blocks must still be found");
assert!(d.literal_bytes() <= 5 + 700);
}
#[test]
fn block_size_clamps() {
assert_eq!(block_size_for(0), MIN_BLOCK);
assert_eq!(block_size_for(100), MIN_BLOCK);
assert_eq!(block_size_for(1_000_000), 1000);
assert_eq!(block_size_for(usize::MAX), MAX_BLOCK);
}
#[test]
fn apply_rejects_out_of_range_copy() {
let bad = Delta {
block_size: 4,
ops: vec![Op::Copy(999)],
};
assert!(apply(b"tiny", &bad).is_err());
}
#[test]
fn strong_len_is_16() {
assert_eq!(crate::checksum::STRONG_LEN, 16);
}
}