mod extend_match;
mod tables;
use std::{collections::HashSet, sync::Arc};
use extend_match::extend_match;
use tables::{
DATA_INS_LEN, HASH_LIMIT, MAX_OP_SIZE, MIN_DELTA_RATE, RABIN_SHIFT, RABIN_WINDOW, T, U,
};
#[cfg(feature = "delta-stats")]
#[derive(Default)]
pub struct DeltaStats {
pub extension_calls: u64,
pub extension_bytes_compared: u64,
pub bucket_scans: u64,
pub bucket_entries_scanned: u64,
pub bucket_sizes: Vec<u32>,
pub candidates_entered_extension: u64,
pub matches_accepted: u64,
}
#[cfg(feature = "delta-stats")]
impl DeltaStats {
#[inline(always)]
fn sink(self) {}
}
#[cfg(not(feature = "delta-stats"))]
#[derive(Default)]
pub struct DeltaStats {
pub extension_calls: u64,
pub extension_bytes_compared: u64,
pub bucket_scans: u64,
pub bucket_entries_scanned: u64,
pub bucket_sizes: Vec<u32>,
pub candidates_entered_extension: u64,
pub matches_accepted: u64,
}
#[cfg(not(feature = "delta-stats"))]
impl DeltaStats {
#[inline(always)]
fn sink(self) {
let _ = self.extension_calls;
let _ = self.extension_bytes_compared;
let _ = self.bucket_scans;
let _ = self.bucket_entries_scanned;
let _ = self.bucket_sizes;
let _ = self.candidates_entered_extension;
let _ = self.matches_accepted;
}
}
#[derive(Debug, Clone, Copy)]
pub struct IndexEntry {
pub offset: u32,
pub hash: u32,
}
pub struct RabinDeltaIndex {
pub source: Arc<[u8]>,
pub entries: Box<[IndexEntry]>,
pub buckets: Box<[u32]>,
pub hash_mask: u32,
}
pub fn create_delta_index(source: &[u8]) -> Option<RabinDeltaIndex> {
create_delta_index_arc(Arc::from(source))
}
pub fn create_delta_index_arc(source: Arc<[u8]>) -> Option<RabinDeltaIndex> {
let src_len = source.len();
if src_len == 0 {
return None;
}
let entries_count = (src_len - 1) / RABIN_WINDOW;
if entries_count == 0 {
return None;
}
let hsize_entries = entries_count / 4;
let mut hsize: usize = 16;
while hsize < hsize_entries {
hsize <<= 1;
}
let hmask = hsize - 1;
let mut bucket_lists: Vec<Vec<IndexEntry>> = vec![Vec::new(); hsize];
let mut prev_val: u32 = !0u32;
let mut pos = entries_count * RABIN_WINDOW - RABIN_WINDOW;
loop {
let window_start = pos + 1;
let window_end = window_start + RABIN_WINDOW;
if window_end > src_len {
if pos == 0 {
break;
}
pos = pos.saturating_sub(RABIN_WINDOW);
continue;
}
let mut val: u32 = 0;
for &b in &source[window_start..window_end] {
val = ((val << 8) | b as u32) ^ T[(val >> RABIN_SHIFT) as usize];
}
if val == prev_val {
let h = val as usize & hmask;
if let Some(last) = bucket_lists[h].last_mut() {
last.offset = (pos + RABIN_WINDOW) as u32;
}
} else {
prev_val = val;
let h = val as usize & hmask;
bucket_lists[h].push(IndexEntry {
offset: (pos + RABIN_WINDOW) as u32,
hash: val,
});
}
if pos == 0 {
break;
}
pos = pos.saturating_sub(RABIN_WINDOW);
}
let mut total_entries = 0usize;
for bucket in bucket_lists.iter_mut() {
if bucket.len() > HASH_LIMIT {
cull_bucket(bucket);
}
total_entries += bucket.len();
}
let mut entries_vec: Vec<IndexEntry> = Vec::with_capacity(total_entries);
let mut buckets: Vec<u32> = Vec::with_capacity(hsize + 1);
for bucket in bucket_lists.iter() {
buckets.push(entries_vec.len() as u32);
entries_vec.extend_from_slice(bucket);
}
buckets.push(entries_vec.len() as u32);
Some(RabinDeltaIndex {
source,
entries: entries_vec.into_boxed_slice(),
buckets: buckets.into_boxed_slice(),
hash_mask: hmask as u32,
})
}
fn cull_bucket(bucket: &mut Vec<IndexEntry>) {
let total = bucket.len();
let target = HASH_LIMIT;
let mut kept = Vec::with_capacity(target);
for (idx, entry) in bucket.drain(..).enumerate() {
if kept.len() < target && idx * target / total == kept.len() {
kept.push(entry);
}
}
*bucket = kept;
}
fn write_varint(n: usize, out: &mut Vec<u8>) {
let mut v = n;
while v >= 0x80 {
out.push((v as u8) | 0x80);
v >>= 7;
}
out.push(v as u8);
}
fn push_copy_op(out: &mut Vec<u8>, offset: u32, size: u32) {
let mut cmd: u8 = 0x80;
let mut extra: Vec<u8> = Vec::with_capacity(7);
let mut off = offset;
for i in 0..4u8 {
let b = (off & 0xff) as u8;
off >>= 8;
if b != 0 {
cmd |= 1 << i;
extra.push(b);
}
}
let mut sz = size;
for i in 0..3u8 {
let b = (sz & 0xff) as u8;
sz >>= 8;
if b != 0 {
cmd |= 1 << (4 + i);
extra.push(b);
}
}
out.push(cmd);
out.extend_from_slice(&extra);
}
fn create_delta(
index: &RabinDeltaIndex,
target: &[u8],
max_size: Option<usize>,
) -> Option<Vec<u8>> {
create_delta_inner(index, target, max_size).map(|(delta, stats)| {
stats.sink();
delta
})
}
fn create_delta_inner(
index: &RabinDeltaIndex,
target: &[u8],
max_size: Option<usize>,
) -> Option<(Vec<u8>, DeltaStats)> {
let src_data = &index.source;
let src_len = src_data.len();
let trg_len = target.len();
let cap = 32 + trg_len / 2;
let mut out: Vec<u8> = Vec::with_capacity(cap);
write_varint(src_len, &mut out);
write_varint(trg_len, &mut out);
if trg_len == 0 {
return Some((out, DeltaStats::default()));
}
let mut stats = DeltaStats::default();
let hmask = index.hash_mask as usize;
let buckets = &index.buckets;
let entries = &index.entries;
let init_count = RABIN_WINDOW.min(trg_len);
let mut data_len_pos = out.len();
out.push(0u8);
for &b in &target[..init_count] {
out.push(b);
}
let mut val: u32 = 0;
for &b in &target[..init_count] {
val = ((val << 8) | b as u32) ^ T[(val >> RABIN_SHIFT) as usize];
}
let mut data_pos = init_count; let mut inscnt = init_count; let mut moff: u32 = 0; let mut msize: u32 = 0;
while data_pos < trg_len {
if data_pos >= RABIN_WINDOW {
let oldest = target[data_pos - RABIN_WINDOW];
val ^= U[oldest as usize];
}
let new_byte = target[data_pos];
val = ((val << 8) | new_byte as u32) ^ T[(val >> RABIN_SHIFT) as usize];
if msize < 4096 {
let bi = val as usize & hmask;
let bucket_start = buckets[bi] as usize;
let bucket_end = buckets[bi + 1] as usize;
let bucket_entries = &entries[bucket_start..bucket_end];
stats.bucket_scans += 1;
stats.bucket_entries_scanned += bucket_entries.len() as u64;
stats.bucket_sizes.push(bucket_entries.len() as u32);
for &entry in bucket_entries {
if entry.hash != val {
continue;
}
let ref_start = entry.offset as usize;
let src_remain = src_len - ref_start;
let trg_remain = trg_len - data_pos;
let max_match = src_remain.min(trg_remain);
if max_match <= msize as usize {
break;
}
stats.candidates_entered_extension += 1;
let match_len = extend_match(src_data, target, ref_start, data_pos, max_match);
stats.extension_calls += 1;
stats.extension_bytes_compared += match_len as u64;
if match_len > msize as usize {
msize = match_len as u32;
moff = ref_start as u32;
stats.matches_accepted += 1;
if msize >= 4096 {
break; }
}
}
}
if msize < 4 {
if inscnt == 0 {
data_len_pos = out.len();
out.push(0u8); }
out.push(new_byte);
inscnt += 1;
data_pos += 1;
if inscnt == DATA_INS_LEN {
out[data_len_pos] = DATA_INS_LEN as u8;
inscnt = 0;
}
msize = 0;
} else {
let mut match_off = moff as usize;
let mut match_len = msize as usize;
let back_extend: usize = {
let max_back = inscnt.min(match_off);
let mut cnt = 0usize;
while cnt < max_back && src_data[match_off - 1 - cnt] == target[data_pos - 1 - cnt]
{
cnt += 1;
}
cnt
};
if back_extend > 0 {
match_off -= back_extend;
match_len += back_extend;
data_pos -= back_extend;
inscnt -= back_extend;
let new_out_len = if inscnt == 0 {
data_len_pos
} else {
out.len() - back_extend
};
out.truncate(new_out_len);
}
if inscnt > 0 {
out[data_len_pos] = inscnt as u8;
inscnt = 0;
}
let mut remaining = match_len as u32;
let mut remaining_off = match_off as u32;
let max_copy: u32 = 0x10000;
let left: u32 = remaining.saturating_sub(max_copy);
remaining -= left;
push_copy_op(&mut out, remaining_off, remaining);
data_pos += remaining as usize;
remaining_off += remaining;
moff = remaining_off;
msize = left;
if msize < 4096 && data_pos >= RABIN_WINDOW {
val = 0;
for &b in &target[data_pos - RABIN_WINDOW..data_pos] {
val = ((val << 8) | b as u32) ^ T[(val >> RABIN_SHIFT) as usize];
}
}
}
if let Some(max) = max_size
&& out.len() > max
{
return None;
}
if out.len() + MAX_OP_SIZE > out.capacity() {
out.reserve(MAX_OP_SIZE * 2);
}
}
if inscnt > 0 {
out[data_len_pos] = inscnt as u8;
}
if let Some(max) = max_size
&& out.len() > max
{
return None;
}
Some((out, stats))
}
pub fn encode_rabin(old_data: &[u8], new_data: &[u8]) -> Vec<u8> {
if new_data.is_empty() {
let mut out = Vec::with_capacity(4);
write_varint(old_data.len(), &mut out);
write_varint(0, &mut out);
return out;
}
if old_data.is_empty() {
return encode_literal_only(0, new_data);
}
if let Some(index) = create_delta_index(old_data) {
create_delta(&index, new_data, None)
.expect("delta should always succeed when max_size is None")
} else {
encode_literal_only(old_data.len(), new_data)
}
}
pub fn encode_rabin_with_index(index: &RabinDeltaIndex, target: &[u8]) -> Vec<u8> {
create_delta(index, target, None).expect("delta should always succeed when max_size is None")
}
pub fn encode_rabin_with_index_and_max_size(
index: &RabinDeltaIndex,
target: &[u8],
max_size: usize,
) -> Option<Vec<u8>> {
if target.is_empty() {
let mut out = Vec::with_capacity(4);
write_varint(index.source.len(), &mut out);
write_varint(0, &mut out);
return Some(out);
}
create_delta(index, target, Some(max_size))
}
pub fn rabin_encode_rate(old_data: &[u8], new_data: &[u8]) -> f64 {
if new_data.is_empty() && old_data.is_empty() {
return 1.0;
}
if old_data.is_empty() || new_data.is_empty() {
return 0.0;
}
if let Some(index) = create_delta_index(old_data) {
let delta = create_delta(&index, new_data, None)
.expect("delta should always succeed when max_size is None");
let new_len = new_data.len();
let mut shared: usize = 0;
let mut pos = 0usize;
while pos < delta.len() && (delta[pos] & 0x80) != 0 {
pos += 1;
}
pos += 1;
while pos < delta.len() && (delta[pos] & 0x80) != 0 {
pos += 1;
}
pos += 1;
while pos < delta.len() {
let cmd = delta[pos];
pos += 1;
if cmd & 0x80 == 0 {
let len = cmd as usize;
pos += len; } else {
let mut sz: usize = 0;
for i in 0..4 {
if cmd & (1 << i) != 0 {
pos += 1; }
}
for i in 0..3 {
if cmd & (1 << (4 + i)) != 0 {
sz |= (delta[pos] as usize) << (8 * i);
pos += 1;
}
}
if sz == 0 {
sz = 0x10000; }
shared += sz.min(new_len);
}
}
if new_len > 0 {
shared.min(new_len) as f64 / new_len as f64
} else {
0.0
}
} else {
0.0
}
}
pub fn heuristic_encode_rate_rabin(old_data: &[u8], new_data: &[u8]) -> f64 {
let old_len = old_data.len();
let new_len = new_data.len();
if old_len == 0 && new_len == 0 {
return 1.0;
}
if old_len == 0 || new_len == 0 {
return 0.0;
}
let step = if old_len > 1_000_000 {
256
} else {
RABIN_WINDOW
};
let old_samples = if old_len >= RABIN_WINDOW {
(old_len - RABIN_WINDOW) / step + 1
} else {
0
};
if old_samples == 0 {
return 0.0;
}
let new_samples = if new_len >= RABIN_WINDOW {
(new_len - RABIN_WINDOW) / step + 1
} else {
0
};
if new_samples == 0 {
return 0.0;
}
let mut old_hashes: HashSet<u32> = HashSet::with_capacity(old_samples.min(8192));
let mut pos = 0usize;
while pos + RABIN_WINDOW <= old_len {
let mut val: u32 = 0;
for &b in &old_data[pos..pos + RABIN_WINDOW] {
val = ((val << 8) | b as u32) ^ T[(val >> RABIN_SHIFT) as usize];
}
old_hashes.insert(val);
pos += step;
}
let total_new_samples = new_samples;
let mut matches = 0usize;
let mut pos = 0usize;
let mut samples_done = 0usize;
while pos + RABIN_WINDOW <= new_len {
let mut val: u32 = 0;
for &b in &new_data[pos..pos + RABIN_WINDOW] {
val = ((val << 8) | b as u32) ^ T[(val >> RABIN_SHIFT) as usize];
}
if old_hashes.contains(&val) {
matches += 1;
}
samples_done += 1;
pos += step;
let remaining = total_new_samples - samples_done;
let max_possible = (matches + remaining) as f64 / total_new_samples as f64;
if max_possible < MIN_DELTA_RATE {
return 0.0;
}
}
if samples_done == 0 {
return 0.0;
}
matches as f64 / samples_done as f64
}
fn encode_literal_only(source_len: usize, new_data: &[u8]) -> Vec<u8> {
let new_len = new_data.len();
let mut out: Vec<u8> = Vec::with_capacity(4 + new_len);
write_varint(source_len, &mut out);
write_varint(new_len, &mut out);
let mut pos = 0usize;
while pos < new_len {
let chunk = DATA_INS_LEN.min(new_len - pos);
out.push(chunk as u8);
out.extend_from_slice(&new_data[pos..pos + chunk]);
pos += chunk;
}
out
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
use crate::delta::decode::delta_decode;
#[test]
fn test_cull_bucket_keeps_hash_limit_entries() {
let mut bucket = (0..128)
.map(|offset| IndexEntry { hash: 0, offset })
.collect();
cull_bucket(&mut bucket);
assert_eq!(bucket.len(), HASH_LIMIT);
assert_eq!(bucket.first().map(|entry| entry.offset), Some(0));
assert_eq!(bucket.last().map(|entry| entry.offset), Some(126));
assert!(
bucket
.windows(2)
.all(|pair| pair[0].offset < pair[1].offset)
);
}
#[test]
fn test_rabin_round_trip_identical() {
let old = b"hello world, this is a test for rabin delta";
let new = b"hello world, this is a test for rabin delta";
let delta = encode_rabin(old, new);
let decoded = delta_decode(&mut Cursor::new(&delta), old).unwrap();
assert_eq!(decoded, new, "round-trip should reconstruct exact data");
}
#[test]
fn test_rabin_round_trip_edit() {
let old = b"hello world, this is a test";
let new = b"hello rust, this is a test";
let delta = encode_rabin(old, new);
let decoded = delta_decode(&mut Cursor::new(&delta), old).unwrap();
assert_eq!(decoded, new);
}
#[test]
fn test_rabin_round_trip_expand() {
let old = b"small";
let new = b"this is a much larger buffer that includes small at the end";
let delta = encode_rabin(old, new);
let decoded = delta_decode(&mut Cursor::new(&delta), old).unwrap();
assert_eq!(decoded, new);
}
#[test]
fn test_rabin_round_trip_different() {
let old = b"abcdefghijklmnop";
let new = b"1234567890123456";
let delta = encode_rabin(old, new);
let decoded = delta_decode(&mut Cursor::new(&delta), old).unwrap();
assert_eq!(decoded, new);
}
#[test]
fn test_rabin_empty() {
let delta = encode_rabin(b"", b"");
let decoded = delta_decode(&mut Cursor::new(&delta), b"").unwrap();
assert_eq!(decoded, b"");
let delta = encode_rabin(b"", b"hello");
let decoded = delta_decode(&mut Cursor::new(&delta), b"").unwrap();
assert_eq!(decoded, b"hello");
let delta = encode_rabin(b"hello", b"");
let decoded = delta_decode(&mut Cursor::new(&delta), b"hello").unwrap();
assert_eq!(decoded, b"");
}
#[test]
fn test_rabin_single_byte() {
let delta = encode_rabin(b"a", b"b");
let decoded = delta_decode(&mut Cursor::new(&delta), b"a").unwrap();
assert_eq!(decoded, b"b");
}
#[test]
fn test_rabin_short_data() {
let old = b"abc";
let new = b"abd";
let delta = encode_rabin(old, new);
let decoded = delta_decode(&mut Cursor::new(&delta), old).unwrap();
assert_eq!(decoded, new);
let old2 = b"hello world";
let new2 = b"hello wOrld";
let delta2 = encode_rabin(old2, new2);
let decoded2 = delta_decode(&mut Cursor::new(&delta2), old2).unwrap();
assert_eq!(decoded2, new2);
}
#[test]
fn test_rabin_large_buffer_compresses() {
let old = vec![0xABu8; 100_000];
let mut new = old.clone();
new[500] = 0xCD;
new[50_000] = 0xEF;
new[99_000] = 0x12;
let delta = encode_rabin(&old, &new);
assert!(
delta.len() < new.len() / 10,
"delta should compress well: delta={}, new={}",
delta.len(),
new.len()
);
let decoded = delta_decode(&mut Cursor::new(&delta), &old).unwrap();
assert_eq!(decoded, new);
}
#[test]
fn test_heuristic_identical() {
let data = b"hello world, this is a test for rabin heuristic";
let rate = heuristic_encode_rate_rabin(data, data);
assert!(
(rate - 1.0).abs() < 1e-6,
"identical data should give rate 1.0"
);
}
#[test]
fn test_heuristic_different() {
let old = vec![0xABu8; 1000];
let new = vec![0xCDu8; 1000];
let rate = heuristic_encode_rate_rabin(&old, &new);
assert!(
rate < 0.2,
"different data should give low rate, got {rate}"
);
}
#[test]
fn test_rabin_encode_rate_identical() {
let data = b"test data for rabin encode rate";
let rate = rabin_encode_rate(data, data);
assert!((rate - 1.0).abs() < 1e-6);
}
#[test]
fn test_rabin_with_zlib_fixtures() {
use std::{
env,
fs::File,
io::{BufReader, Read},
path::PathBuf,
};
use flate2::bufread::ZlibDecoder;
fn read_zlib_data(path: &std::path::Path) -> Result<Vec<u8>, std::io::Error> {
let file = File::open(path)?;
let buf_reader = BufReader::new(file);
let mut deflate = ZlibDecoder::new(buf_reader);
let mut result = Vec::new();
deflate.read_to_end(&mut result)?;
Ok(result)
}
let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
source.push("tests/diff/16ecdcc8f663777896bd39ca025a041b7f005e");
let old_data = read_zlib_data(&source).unwrap();
let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
source.push("tests/diff/bee0d45f981adf7c2926a0dc04deb7f006bcc3");
let new_data = read_zlib_data(&source).unwrap();
let delta = encode_rabin(&old_data, &new_data);
let decoded = delta_decode(&mut Cursor::new(&delta), &old_data).unwrap();
assert_eq!(decoded, new_data, "rabin round-trip with zlib fixtures");
}
#[test]
fn test_rabin_backward_extension() {
let old = b"AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD";
let mut new = vec![b'A'; 32];
new[8] = b'X'; let delta = encode_rabin(old, &new);
let decoded = delta_decode(&mut Cursor::new(&delta), old).unwrap();
assert_eq!(decoded, new);
}
#[test]
fn test_rabin_repetitive_data() {
let old = vec![b'X'; 10_000];
let new = vec![b'X'; 10_000];
let delta = encode_rabin(&old, &new);
let decoded = delta_decode(&mut Cursor::new(&delta), &old).unwrap();
assert_eq!(decoded, new);
assert!(
delta.len() < 100,
"repetitive data should be very compact: got {}",
delta.len()
);
}
}