mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:06:44 +08:00
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>
209 lines
6.3 KiB
Rust
209 lines
6.3 KiB
Rust
//! 资源领域对象
|
||
|
||
use std::path::PathBuf;
|
||
|
||
/// 资源类型
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||
pub enum ResourceType {
|
||
/// AssetBundle
|
||
AssetBundle,
|
||
/// Manifest
|
||
Manifest,
|
||
/// TableBundle
|
||
TableBundle,
|
||
/// TextAsset
|
||
TextAsset,
|
||
/// Media resource
|
||
Media,
|
||
/// 其他
|
||
Other,
|
||
}
|
||
|
||
/// 资源条目
|
||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||
pub struct ResourceEntry {
|
||
/// 资源路径
|
||
pub path: String,
|
||
/// 资源 Hash
|
||
pub hash: String,
|
||
/// 资源大小
|
||
pub size: u64,
|
||
/// 资源类型
|
||
pub resource_type: ResourceType,
|
||
/// 资源在 Manifest 中的逻辑地址
|
||
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
|
||
}
|
||
|
||
/// 资源
|
||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||
pub struct Resource {
|
||
/// 资源 ID
|
||
pub id: String,
|
||
/// 本地路径
|
||
pub local_path: PathBuf,
|
||
/// 资源条目
|
||
pub entry: ResourceEntry,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn entry_with(size: u64, crc: Option<u32>) -> ResourceEntry {
|
||
ResourceEntry {
|
||
path: "test.bundle".to_string(),
|
||
hash: "abc123".to_string(),
|
||
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
|
||
})
|
||
);
|
||
}
|
||
}
|