mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 01:46:44 +08:00
feat(addressables): 提取 m_Crc 并提供 size/CRC 校验 API(issue #2)
Addressables catalog 里的 m_Crc(bundle IEEE CRC-32)此前从未解析;
声明的 size 也只作元数据、无校验能力。本次:
- ResourceEntry 新增 crc: Option<u32>(serde default 向后兼容),
compact 与 expanded 两种 catalog 形态均解析 m_Crc/m_Crc→crc
- core 新增 crc32_ieee(IEEE CRC-32,等价 zlib/Unity m_Crc)与
ResourceEntry::{declared_crc, verify_downloaded_bytes}:按声明的
size/crc 校验字节,0 视为「无 CRC」跳过
- SqliteResourceRepository 持久化 crc 列,旧库经幂等 ensure_column
迁移补列(pragma_table_info 判断后 ALTER)
- golden 投影与 fixture 补 crc 字段,验证真实形态 catalog 提取贯通
校验 API 暂不接入 import 覆盖路径(该路径按 CAS id 重写 hash/size 是
既定语义,且合成测试的声明值不匹配实际字节);接入下载/导入校验留
待 G-011。
验证:core crc32 标准向量 + verify 分支单测、expanded 形态非零 crc
提取单测、golden 端到端;core/adapters/infrastructure 全测试 + fmt +
clippy --all-targets -D warnings 全绿。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ pub mod translation;
|
||||
|
||||
pub use game_client::{ClientStatus, GameClient, GameRegion};
|
||||
pub use game_version::{GameVersion, UnityVersion};
|
||||
pub use resource::{Resource, ResourceEntry, ResourceType};
|
||||
pub use resource::{crc32_ieee, IntegrityMismatch, Resource, ResourceEntry, ResourceType};
|
||||
pub use translation::{
|
||||
ExtractedText, SourceText, TextContext, TextMetadata, TextSource, TranslatedText,
|
||||
TranslationStatus,
|
||||
|
||||
+145
-5
@@ -34,6 +34,94 @@ pub struct ResourceEntry {
|
||||
pub address: Option<String>,
|
||||
/// 该资源依赖的其他资源标识
|
||||
pub dependencies: Vec<String>,
|
||||
/// Addressables bundle 的 CRC32(catalog 中的 `m_Crc`)。
|
||||
///
|
||||
/// `None` 表示 catalog 未提供该字段;Unity 用 `0` 表示「不做 CRC 校验」,
|
||||
/// 因此 `Some(0)` 与 `None` 在校验时同样视为「无 CRC」。为向后兼容旧的
|
||||
/// 持久化数据,反序列化时缺省为 `None`。
|
||||
#[serde(default)]
|
||||
pub crc: Option<u32>,
|
||||
}
|
||||
|
||||
/// 已下载字节与 catalog 声明的可校验字段不一致。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IntegrityMismatch {
|
||||
/// 实际字节数与声明的 `size` 不符。
|
||||
Size {
|
||||
/// catalog 声明的大小。
|
||||
expected: u64,
|
||||
/// 实际字节数。
|
||||
actual: u64,
|
||||
},
|
||||
/// 实际 CRC32 与声明的 `crc` 不符。
|
||||
Crc {
|
||||
/// catalog 声明的 CRC32。
|
||||
expected: u32,
|
||||
/// 实际计算出的 CRC32。
|
||||
actual: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IntegrityMismatch {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Size { expected, actual } => {
|
||||
write!(formatter, "大小不符:声明 {expected},实际 {actual}")
|
||||
}
|
||||
Self::Crc { expected, actual } => write!(
|
||||
formatter,
|
||||
"CRC32 不符:声明 {expected:#010x},实际 {actual:#010x}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IntegrityMismatch {}
|
||||
|
||||
impl ResourceEntry {
|
||||
/// catalog 声明的 CRC32(`m_Crc`),`0` 归一化为「无 CRC」(返回 `None`)。
|
||||
pub fn declared_crc(&self) -> Option<u32> {
|
||||
self.crc.filter(|value| *value != 0)
|
||||
}
|
||||
|
||||
/// 用 catalog 声明的可校验字段(`size`、`crc`)校验已下载/已解出的字节。
|
||||
///
|
||||
/// - `size`:声明值为 `0` 视为未提供,跳过;否则要求与 `data.len()` 相等。
|
||||
/// - `crc`:无声明(`None`/`Some(0)`)时跳过;否则按 IEEE CRC-32 计算 `data`
|
||||
/// 的 CRC 并比对。Unity AssetBundle 的 `m_Crc` 即标准 IEEE CRC-32(与
|
||||
/// zlib `crc32` 一致,UnityPy/AssetStudio 等生态一致采用)。
|
||||
///
|
||||
/// 校验通过返回 `Ok(())`;不一致返回首个失败项(先 size 后 crc)。
|
||||
pub fn verify_downloaded_bytes(&self, data: &[u8]) -> Result<(), IntegrityMismatch> {
|
||||
if self.size != 0 && self.size != data.len() as u64 {
|
||||
return Err(IntegrityMismatch::Size {
|
||||
expected: self.size,
|
||||
actual: data.len() as u64,
|
||||
});
|
||||
}
|
||||
if let Some(expected) = self.declared_crc() {
|
||||
let actual = crc32_ieee(data);
|
||||
if actual != expected {
|
||||
return Err(IntegrityMismatch::Crc { expected, actual });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算 IEEE CRC-32(多项式 `0xEDB88320`,反射,初值/终值 `0xFFFFFFFF`)。
|
||||
///
|
||||
/// 与 zlib `crc32` 及 Unity AssetBundle `m_Crc` 使用的算法一致。
|
||||
pub fn crc32_ieee(data: &[u8]) -> u32 {
|
||||
let mut crc: u32 = 0xFFFF_FFFF;
|
||||
for &byte in data {
|
||||
crc ^= u32::from(byte);
|
||||
for _ in 0..8 {
|
||||
let mask = (crc & 1).wrapping_neg();
|
||||
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
|
||||
}
|
||||
}
|
||||
!crc
|
||||
}
|
||||
|
||||
/// 资源
|
||||
@@ -51,18 +139,70 @@ pub struct Resource {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resource_entry() {
|
||||
let entry = ResourceEntry {
|
||||
fn entry_with(size: u64, crc: Option<u32>) -> ResourceEntry {
|
||||
ResourceEntry {
|
||||
path: "test.bundle".to_string(),
|
||||
hash: "abc123".to_string(),
|
||||
size: 1024,
|
||||
size,
|
||||
resource_type: ResourceType::AssetBundle,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
};
|
||||
crc,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_entry() {
|
||||
let entry = entry_with(1024, None);
|
||||
assert_eq!(entry.path, "test.bundle");
|
||||
assert_eq!(entry.size, 1024);
|
||||
assert_eq!(entry.crc, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crc32_matches_known_vector() {
|
||||
// 标准 IEEE CRC-32 测试向量:crc32("123456789") == 0xCBF43926。
|
||||
assert_eq!(crc32_ieee(b"123456789"), 0xCBF4_3926);
|
||||
assert_eq!(crc32_ieee(b""), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_crc_treats_zero_as_absent() {
|
||||
assert_eq!(entry_with(0, None).declared_crc(), None);
|
||||
assert_eq!(entry_with(0, Some(0)).declared_crc(), None);
|
||||
assert_eq!(entry_with(0, Some(42)).declared_crc(), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_downloaded_bytes_checks_size_and_crc() {
|
||||
let data = b"123456789";
|
||||
let crc = crc32_ieee(data);
|
||||
|
||||
// size + crc 均匹配。
|
||||
assert!(entry_with(data.len() as u64, Some(crc))
|
||||
.verify_downloaded_bytes(data)
|
||||
.is_ok());
|
||||
|
||||
// size=0 与 crc=0/None 视为未声明,跳过校验。
|
||||
assert!(entry_with(0, None).verify_downloaded_bytes(data).is_ok());
|
||||
assert!(entry_with(0, Some(0)).verify_downloaded_bytes(data).is_ok());
|
||||
|
||||
// size 不符。
|
||||
assert_eq!(
|
||||
entry_with(3, None).verify_downloaded_bytes(data),
|
||||
Err(IntegrityMismatch::Size {
|
||||
expected: 3,
|
||||
actual: 9
|
||||
})
|
||||
);
|
||||
|
||||
// size 通过、crc 不符。
|
||||
assert_eq!(
|
||||
entry_with(data.len() as u64, Some(0xDEAD_BEEF)).verify_downloaded_bytes(data),
|
||||
Err(IntegrityMismatch::Crc {
|
||||
expected: 0xDEAD_BEEF,
|
||||
actual: crc
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user