mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:54:55 +08:00
feat(patch): 补齐通用补丁引擎基础
实现 Binary、JSON、Text patch 与通用 manifest 校验基础。 验证:cargo test -p bat-patch --locked;cargo clippy -p bat-patch --all-targets --locked -- -D warnings。
This commit is contained in:
+158
-10
@@ -1,13 +1,145 @@
|
||||
//! Binary Patch 模块占位
|
||||
//! Deterministic binary hunk patch.
|
||||
|
||||
/// Binary 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.
|
||||
///
|
||||
/// 返回 [`crate::PatchError::ApplyFailed`] 而非空结果,避免调用方把未实现的
|
||||
/// 占位当成一次成功的补丁应用。
|
||||
pub fn apply_patch(_old: &[u8], _patch: &[u8]) -> crate::Result<Vec<u8>> {
|
||||
Err(crate::PatchError::ApplyFailed(
|
||||
"binary patch 尚未实现".to_string(),
|
||||
))
|
||||
/// 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)]
|
||||
@@ -15,8 +147,24 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apply_patch_reports_not_implemented() {
|
||||
let error = apply_patch(b"old", b"patch").unwrap_err();
|
||||
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(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user