Files
BlueArchiveToolkit/crates/bat-patch/src/binary.rs
T
nyaKazuha f4880a71bd feat(patch): 补齐通用补丁引擎基础
实现 Binary、JSON、Text patch 与通用 manifest 校验基础。

验证:cargo test -p bat-patch --locked;cargo clippy -p bat-patch --all-targets --locked -- -D warnings。
2026-07-31 00:21:50 +08:00

171 lines
5.6 KiB
Rust

//! 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<BinaryPatchHunk>,
}
/// 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<u8>,
},
}
/// 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<Vec<u8>> {
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<Vec<u8>> {
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(_)));
}
}