use std::collections::HashMap;
use serde::Deserialize;
use serde::Serialize;
use crate::CodePushError;
const BLOCK: usize = 512;
const MAX_CANDIDATES: usize = 16;
pub const MAX_DELTA_OUTPUT: u64 = 512 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeltaOp {
Copy {
offset: u64,
len: u64,
},
Insert {
#[serde(with = "base64_bytes")]
bytes: Vec<u8>,
},
}
mod base64_bytes {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serializer;
pub(super) fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&STANDARD.encode(bytes))
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<u8>, D::Error> {
let encoded = String::deserialize(deserializer)?;
STANDARD.decode(encoded).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Delta {
pub base_hash: String,
pub target_hash: String,
pub ops: Vec<DeltaOp>,
}
pub fn create_delta(base: &[u8], target: &[u8]) -> Delta {
let mut index: HashMap<u64, Vec<usize>> = HashMap::new();
let mut offset = 0;
while offset + BLOCK <= base.len() {
let hash = quick_hash(&base[offset..offset + BLOCK]);
let offsets = index.entry(hash).or_default();
if offsets.len() < MAX_CANDIDATES {
offsets.push(offset);
}
offset += BLOCK;
}
let mut ops: Vec<DeltaOp> = Vec::new();
let mut literal: Vec<u8> = Vec::new();
let mut position = 0;
while position < target.len() {
let mut matched = None;
if position + BLOCK <= target.len() {
let hash = quick_hash(&target[position..position + BLOCK]);
if let Some(candidates) = index.get(&hash) {
matched = candidates
.iter()
.map(|&base_offset| {
let length = extend_match(base, base_offset, target, position);
(base_offset, length)
})
.max_by_key(|&(_, length)| length)
.filter(|&(_, length)| length >= BLOCK as u64);
}
}
if let Some((base_offset, forward)) = matched {
let mut start = base_offset;
while start > 0 && literal.last() == Some(&base[start - 1]) {
literal.pop();
start -= 1;
}
flush_literal(&mut ops, &mut literal);
ops.push(DeltaOp::Copy {
offset: start as u64,
len: (base_offset - start) as u64 + forward,
});
position += forward as usize;
} else {
literal.push(target[position]);
position += 1;
}
}
flush_literal(&mut ops, &mut literal);
Delta {
base_hash: crate::manifest::hash_bytes(base),
target_hash: crate::manifest::hash_bytes(target),
ops,
}
}
pub fn apply_delta(base: &[u8], delta: &Delta) -> Result<Vec<u8>, CodePushError> {
apply_delta_within(base, delta, MAX_DELTA_OUTPUT)
}
pub fn apply_delta_within(
base: &[u8],
delta: &Delta,
max_output: u64,
) -> Result<Vec<u8>, CodePushError> {
let actual = crate::manifest::hash_bytes(base);
if actual != delta.base_hash {
return Err(CodePushError::DeltaBaseMismatch {
expected: delta.base_hash.clone(),
actual,
});
}
let mut total: u64 = 0;
for op in &delta.ops {
let len = match op {
DeltaOp::Copy { offset, len } => {
let in_bounds = offset
.checked_add(*len)
.is_some_and(|end| end <= base.len() as u64);
if !in_bounds {
return Err(CodePushError::Malformed(format!(
"delta copy out of bounds: {offset} + {len} over {}",
base.len()
)));
}
*len
}
DeltaOp::Insert { bytes } => bytes.len() as u64,
};
total = total
.checked_add(len)
.filter(|&total| total <= max_output)
.ok_or_else(|| {
CodePushError::Malformed(format!(
"delta output exceeds the {max_output} byte limit"
))
})?;
}
let mut out = Vec::with_capacity(total as usize);
for op in &delta.ops {
match op {
DeltaOp::Copy { offset, len } => {
let start = *offset as usize;
out.extend_from_slice(&base[start..start + *len as usize]);
}
DeltaOp::Insert { bytes } => out.extend_from_slice(bytes),
}
}
let produced = crate::manifest::hash_bytes(&out);
if produced != delta.target_hash {
return Err(CodePushError::HashMismatch {
path: "<delta result>".to_owned(),
expected: delta.target_hash.clone(),
actual: produced,
});
}
Ok(out)
}
fn flush_literal(ops: &mut Vec<DeltaOp>, literal: &mut Vec<u8>) {
if !literal.is_empty() {
ops.push(DeltaOp::Insert {
bytes: std::mem::take(literal),
});
}
}
fn quick_hash(block: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let quarter = block.len() / 4;
for &byte in &block[..quarter] {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100_0000_01b3);
}
for &byte in &block[block.len() - quarter..] {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100_0000_01b3);
}
hash
}
fn extend_match(base: &[u8], base_offset: usize, target: &[u8], target_offset: usize) -> u64 {
let mut length = 0usize;
while base_offset + length < base.len()
&& target_offset + length < target.len()
&& base[base_offset + length] == target[target_offset + length]
{
length += 1;
}
length as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_rejects_output_over_the_limit_before_allocating() {
let base = vec![0u8; 1024 * 1024];
let copies = MAX_DELTA_OUTPUT / base.len() as u64 + 1;
let delta = Delta {
base_hash: crate::manifest::hash_bytes(&base),
target_hash: String::new(),
ops: (0..copies)
.map(|_| {
DeltaOp::Copy {
offset: 0,
len: base.len() as u64,
}
})
.collect(),
};
let result = apply_delta(&base, &delta);
assert!(
matches!(&result, Err(CodePushError::Malformed(message)) if message.contains("limit")),
"got {result:?}"
);
}
#[test]
fn inserted_bytes_travel_as_base64() {
let delta = Delta {
base_hash: "base".to_owned(),
target_hash: "target".to_owned(),
ops: vec![
DeltaOp::Copy { offset: 0, len: 3 },
DeltaOp::Insert {
bytes: vec![0, 1, 2, 255],
},
],
};
let json = serde_json::to_string(&delta).expect("serialize");
assert_eq!(
json,
r#"{"base_hash":"base","target_hash":"target","ops":[{"Copy":{"offset":0,"len":3}},{"Insert":{"bytes":"AAEC/w=="}}]}"#
);
assert_eq!(
serde_json::from_str::<Delta>(&json).expect("deserialize"),
delta
);
assert!(serde_json::from_str::<Delta>(&json.replace("AAEC/w==", "not base64!")).is_err());
}
#[test]
fn matches_extend_back_over_bytes_the_base_shares() {
let mut state = 0x2545_f491_4f6c_dd1d_u64;
let base: Vec<u8> = (0..8 * BLOCK)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state as u8
})
.collect();
let mut target = base.clone();
target[BLOCK + 10] ^= 0xff;
let delta = create_delta(&base, &target);
assert_eq!(apply_delta(&base, &delta).expect("apply"), target);
let inserted: usize = delta
.ops
.iter()
.map(|op| {
match op {
DeltaOp::Insert { bytes } => bytes.len(),
DeltaOp::Copy { .. } => 0,
}
})
.sum();
assert!(
inserted < 16,
"{inserted} bytes inserted for one changed byte"
);
}
#[test]
fn a_base_of_one_repeated_block_encodes_in_linear_time() {
let base = vec![0u8; 4 * 1024 * 1024];
let mut target = base.clone();
target[2 * 1024 * 1024] = 1;
let delta = create_delta(&base, &target);
assert_eq!(apply_delta(&base, &delta).expect("apply"), target);
assert!(delta.ops.len() <= 4, "{:?}", delta.ops);
}
#[test]
fn apply_rejects_copies_whose_end_overflows() {
let base = vec![0u8; 16];
let delta = Delta {
base_hash: crate::manifest::hash_bytes(&base),
target_hash: String::new(),
ops: vec![DeltaOp::Copy {
offset: 8,
len: u64::MAX,
}],
};
let result = apply_delta(&base, &delta);
assert!(
matches!(result, Err(CodePushError::Malformed(_))),
"got {result:?}"
);
}
}