const MIN_MATCH_LENGTH_LARGE: usize = 16;
const MIN_MATCH_LENGTH_SMALL: usize = 8;
const MAX_MATCH_CANDIDATES: usize = 1024;
const MATCH_CHUNK_SIZE: usize = 32;
const MAX_COPY_LENGTH: usize = 0xFF_FFFF;
const INDEX_BLOCK_SIZE: usize = 16;
const MAX_INDEX_BYTES: usize = 4 * 1024 * 1024;
const DENSE_INDEX_BELOW: usize = 1024;
#[derive(Clone, Copy, Debug)]
struct IndexEntry {
key: u32,
offset: u32,
}
#[derive(Debug)]
pub struct DeltaIndex {
entries: Vec<IndexEntry>,
}
#[derive(Debug)]
pub struct DeltaEncoder;
impl DeltaEncoder {
pub fn new() -> Self {
Self
}
pub fn encode(base: &[u8], target: &[u8]) -> Vec<u8> {
if base.is_empty() {
return Self::encode_insert(target);
}
let index = Self::build_index(base);
Self::encode_with_index(&index, base, target)
}
pub fn encode_with_index(index: &DeltaIndex, base: &[u8], target: &[u8]) -> Vec<u8> {
if base.is_empty() {
return Self::encode_insert(target);
}
let min_match = Self::min_match_for(target.len());
let mut delta = Vec::new();
let mut pos = 0;
let mut key = Self::target_key(target, pos);
while pos < target.len() {
if let Some((offset, length)) =
Self::find_best_match(index, base, target, pos, key, min_match)
{
Self::emit_copy(&mut delta, offset, length);
pos += length;
key = Self::target_key(target, pos);
} else {
let start = pos;
while pos < target.len() && pos - start < 127 {
pos += 1;
key = Self::roll_target_key(key, target, pos);
if Self::find_best_match(index, base, target, pos, key, min_match).is_some() {
break;
}
}
let len = pos - start;
delta.push(len as u8 - 1);
delta.extend_from_slice(&target[start..pos]);
}
}
delta
}
pub fn estimate_delta_size(base: &[u8], target: &[u8]) -> usize {
if base.is_empty() {
return target.len() + target.len().div_ceil(128);
}
let index = Self::build_index(base);
Self::estimate_delta_size_with_index(&index, base, target)
}
pub fn estimate_delta_size_with_index(index: &DeltaIndex, base: &[u8], target: &[u8]) -> usize {
if base.is_empty() {
return target.len() + target.len().div_ceil(128);
}
let min_match = Self::min_match_for(target.len());
let mut size = 0usize;
let mut pos = 0;
let mut key = Self::target_key(target, pos);
while pos < target.len() {
if let Some((offset, length)) =
Self::find_best_match(index, base, target, pos, key, min_match)
{
size += Self::copy_instruction_size(offset, length);
pos += length;
key = Self::target_key(target, pos);
} else {
let start = pos;
while pos < target.len() && pos - start < 127 {
pos += 1;
key = Self::roll_target_key(key, target, pos);
if Self::find_best_match(index, base, target, pos, key, min_match).is_some() {
break;
}
}
size += 1 + (pos - start);
}
}
size
}
pub fn build_index(base: &[u8]) -> DeltaIndex {
if base.len() < 4 {
return DeltaIndex {
entries: Vec::new(),
};
}
let last_offset = (base.len() - 4).min(u32::MAX as usize);
let max_entries = MAX_INDEX_BYTES / size_of::<IndexEntry>();
let stride = if base.len() < DENSE_INDEX_BELOW {
1
} else {
(last_offset + 1)
.div_ceil(max_entries)
.max(INDEX_BLOCK_SIZE)
.next_multiple_of(INDEX_BLOCK_SIZE)
};
let mut entries = Vec::with_capacity(last_offset / stride + 1);
for offset in (0..=last_offset).step_by(stride) {
let key = u32::from_be_bytes([
base[offset],
base[offset + 1],
base[offset + 2],
base[offset + 3],
]);
entries.push(IndexEntry {
key,
offset: offset as u32,
});
}
entries.sort_unstable_by_key(|entry| (entry.key, entry.offset));
DeltaIndex { entries }
}
fn emit_copy(delta: &mut Vec<u8>, offset: usize, length: usize) {
let mut remaining = length;
let mut offset = offset;
while remaining > 0 {
let chunk = remaining.min(MAX_COPY_LENGTH);
Self::emit_copy_instruction(delta, offset, chunk);
offset += chunk;
remaining -= chunk;
}
}
fn emit_copy_instruction(delta: &mut Vec<u8>, offset: usize, length: usize) {
let mut cmd: u8 = 0x80;
let offset = offset as u32;
let length = length as u32;
cmd |= 0x01; if offset & 0xFF00 != 0 {
cmd |= 0x02;
}
if offset & 0xFF_0000 != 0 {
cmd |= 0x04;
}
if offset & 0xFF00_0000 != 0 {
cmd |= 0x08;
}
if length != 0x10000 {
if length & 0xFF != 0 {
cmd |= 0x10;
}
if length & 0xFF00 != 0 {
cmd |= 0x20;
}
if length & 0xFF_0000 != 0 {
cmd |= 0x40;
}
}
delta.push(cmd);
delta.push(offset as u8); if offset & 0xFF00 != 0 {
delta.push((offset >> 8) as u8);
}
if offset & 0xFF_0000 != 0 {
delta.push((offset >> 16) as u8);
}
if offset & 0xFF00_0000 != 0 {
delta.push((offset >> 24) as u8);
}
if length != 0x10000 {
if length & 0xFF != 0 {
delta.push(length as u8);
}
if length & 0xFF00 != 0 {
delta.push((length >> 8) as u8);
}
if length & 0xFF_0000 != 0 {
delta.push((length >> 16) as u8);
}
}
}
fn copy_instruction_size(offset: usize, length: usize) -> usize {
let mut remaining = length;
let mut offset = offset;
let mut size = 0;
while remaining > 0 {
let chunk = remaining.min(MAX_COPY_LENGTH);
size += Self::copy_instruction_size_one(offset, chunk);
offset += chunk;
remaining -= chunk;
}
size
}
fn copy_instruction_size_one(offset: usize, length: usize) -> usize {
let offset = offset as u32;
let length = length as u32;
let mut n = 1 + 1;
if offset & 0xFF00 != 0 {
n += 1;
}
if offset & 0xFF_0000 != 0 {
n += 1;
}
if offset & 0xFF00_0000 != 0 {
n += 1;
}
if length != 0x10000 {
if length & 0xFF != 0 {
n += 1;
}
if length & 0xFF00 != 0 {
n += 1;
}
if length & 0xFF_0000 != 0 {
n += 1;
}
}
n
}
fn min_match_for(target_len: usize) -> usize {
if target_len < 1024 {
MIN_MATCH_LENGTH_SMALL
} else {
MIN_MATCH_LENGTH_LARGE
}
}
fn encode_insert(data: &[u8]) -> Vec<u8> {
let mut delta = Vec::new();
for chunk in data.chunks(128) {
delta.push((chunk.len() - 1) as u8);
delta.extend_from_slice(chunk);
}
delta
}
fn find_best_match(
index: &DeltaIndex,
base: &[u8],
target: &[u8],
pos: usize,
key: Option<u32>,
min_match: usize,
) -> Option<(usize, usize)> {
let key = key?;
let found = index
.entries
.binary_search_by_key(&key, |entry| entry.key)
.ok()?;
let first = index.entries[..found].partition_point(|entry| entry.key < key);
let last = found + index.entries[found..].partition_point(|entry| entry.key == key);
let offsets = &index.entries[first..last];
let mut best_offset = 0;
let mut best_length = 0;
let target_remaining = target.len() - pos;
let recent_start = offsets.len().saturating_sub(MAX_MATCH_CANDIDATES);
let mut examined = 0usize;
if recent_start > 0 {
let offset = offsets[0].offset as usize;
let length = Self::match_length(base, offset, target, pos);
if length > best_length {
best_length = length;
best_offset = offset;
}
if length == target_remaining {
return Some((best_offset, best_length));
}
examined += 1;
}
let remaining_budget = MAX_MATCH_CANDIDATES - examined;
let start = offsets.len().saturating_sub(remaining_budget);
for entry in &offsets[start..] {
let offset = entry.offset as usize;
let length = Self::match_length(base, offset, target, pos);
if length > best_length {
best_length = length;
best_offset = offset;
}
if length == target_remaining {
break;
}
}
if best_length >= min_match {
Some((best_offset, best_length))
} else {
None
}
}
fn target_key(target: &[u8], pos: usize) -> Option<u32> {
let bytes = target.get(pos..pos.checked_add(4)?)?;
Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn roll_target_key(key: Option<u32>, target: &[u8], pos: usize) -> Option<u32> {
let next_byte = *target.get(pos.checked_add(3)?)?;
Some((key? << 8) | u32::from(next_byte))
}
fn match_length(base: &[u8], base_pos: usize, target: &[u8], target_pos: usize) -> usize {
let max_len = (base.len() - base_pos).min(target.len() - target_pos).min(
(u32::MAX as usize)
.saturating_sub(base_pos)
.saturating_add(1),
);
let mut len = 0;
while len + MATCH_CHUNK_SIZE <= max_len
&& base[base_pos + len..base_pos + len + MATCH_CHUNK_SIZE]
== target[target_pos + len..target_pos + len + MATCH_CHUNK_SIZE]
{
len += MATCH_CHUNK_SIZE;
}
while len < max_len && base[base_pos + len] == target[target_pos + len] {
len += 1;
}
len
}
}
impl Default for DeltaEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{DeltaEncoder, IndexEntry, MAX_INDEX_BYTES};
#[test]
fn index_memory_is_bounded() {
let base = vec![0u8; 16 * 1024 * 1024];
let index = DeltaEncoder::build_index(&base);
assert!(index.entries.capacity() * size_of::<IndexEntry>() <= MAX_INDEX_BYTES);
}
}