Files
BlueArchiveToolkit/core/src/domain/resource.rs
T
nyaKazuhaandClaude Fable 5 8efd8f36b4 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>
2026-07-18 02:41:41 -07:00

209 lines
6.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 资源领域对象
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 的 CRC32catalog 中的 `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
})
);
}
}