mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
实现 Binary、JSON、Text patch 与通用 manifest 校验基础。 验证:cargo test -p bat-patch --locked;cargo clippy -p bat-patch --all-targets --locked -- -D warnings。
171 lines
5.6 KiB
Rust
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(_)));
|
|
}
|
|
}
|