//! Deterministic binary hunk patch. use crate::PatchError; use serde::{Deserialize, Serialize}; /// Current binary patch schema version. pub const BINARY_PATCH_VERSION: u32 = 1; /// Binary patch made of deterministic copy/insert hunks. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BinaryPatch { /// Patch schema version. pub version: u32, /// Expected BLAKE3 hash of the source bytes. pub source_blake3: String, /// Expected BLAKE3 hash of the target bytes. pub target_blake3: String, /// Source byte length. pub source_size: u64, /// Target byte length. pub target_size: u64, /// Ordered hunks. pub hunks: Vec, } /// One binary patch hunk. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum BinaryPatchHunk { /// Copy a byte range from the source. Copy { /// Source offset. offset: u64, /// Number of bytes to copy. length: u64, }, /// Insert literal bytes. Insert { /// Literal bytes. bytes: Vec, }, } /// Creates a deterministic hunk patch. /// /// The first implementation optimizes for correctness and stable output. It /// emits copy hunks for equal runs and insert hunks for changed runs; more /// compact suffix/prefix matching can be added later without changing the /// manifest/integrity contract. pub fn diff(old: &[u8], new: &[u8]) -> BinaryPatch { let mut hunks = Vec::new(); let mut index = 0usize; while index < new.len() { if index < old.len() && old[index] == new[index] { let start = index; while index < new.len() && index < old.len() && old[index] == new[index] { index += 1; } hunks.push(BinaryPatchHunk::Copy { offset: start as u64, length: (index - start) as u64, }); continue; } let start = index; while index < new.len() && (index >= old.len() || old[index] != new[index]) { index += 1; } hunks.push(BinaryPatchHunk::Insert { bytes: new[start..index].to_vec(), }); } BinaryPatch { version: BINARY_PATCH_VERSION, source_blake3: blake3_hex(old), target_blake3: blake3_hex(new), source_size: old.len() as u64, target_size: new.len() as u64, hunks, } } /// Applies a structured binary patch. pub fn apply_binary_patch(old: &[u8], patch: &BinaryPatch) -> crate::Result> { if patch.version != BINARY_PATCH_VERSION { return Err(PatchError::ApplyFailed(format!( "unsupported binary patch version {}", patch.version ))); } if patch.source_size != old.len() as u64 || patch.source_blake3 != blake3_hex(old) { return Err(PatchError::ApplyFailed( "binary patch source integrity mismatch".to_string(), )); } let target_capacity = usize::try_from(patch.target_size) .map_err(|_| PatchError::ApplyFailed("binary patch target too large".to_string()))?; let mut output = Vec::with_capacity(target_capacity); for hunk in &patch.hunks { match hunk { BinaryPatchHunk::Copy { offset, length } => { let start = usize::try_from(*offset).map_err(|_| { PatchError::ApplyFailed("binary patch copy offset overflow".to_string()) })?; let length = usize::try_from(*length).map_err(|_| { PatchError::ApplyFailed("binary patch copy length overflow".to_string()) })?; let end = start.checked_add(length).ok_or_else(|| { PatchError::ApplyFailed("binary patch copy range overflow".to_string()) })?; let bytes = old.get(start..end).ok_or_else(|| { PatchError::ApplyFailed(format!( "binary patch copy range {start}..{end} exceeds source {}", old.len() )) })?; output.extend_from_slice(bytes); } BinaryPatchHunk::Insert { bytes } => output.extend_from_slice(bytes), } } if output.len() as u64 != patch.target_size || blake3_hex(&output) != patch.target_blake3 { return Err(PatchError::ApplyFailed( "binary patch target integrity mismatch".to_string(), )); } Ok(output) } /// Serializes and applies a binary patch. pub fn apply_patch(old: &[u8], patch: &[u8]) -> crate::Result> { let patch: BinaryPatch = serde_json::from_slice(patch) .map_err(|error| PatchError::ApplyFailed(format!("invalid binary patch JSON: {error}")))?; apply_binary_patch(old, &patch) } fn blake3_hex(bytes: &[u8]) -> String { blake3::hash(bytes).to_hex().to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn binary_patch_round_trips_changed_bytes() { let old = b"abcdef012345"; let new = b"abcXYZ012345!"; let patch = diff(old, new); let patch_json = serde_json::to_vec(&patch).unwrap(); assert_eq!(apply_binary_patch(old, &patch).unwrap(), new); assert_eq!(apply_patch(old, &patch_json).unwrap(), new); assert!(patch .hunks .iter() .any(|hunk| matches!(hunk, BinaryPatchHunk::Insert { .. }))); } #[test] fn binary_patch_rejects_wrong_source() { let patch = diff(b"old", b"new"); let error = apply_binary_patch(b"bad", &patch).unwrap_err(); assert!(matches!(error, crate::PatchError::ApplyFailed(_))); } }