Files
BlueArchiveToolkit/core/src/domain/resource.rs
T
nyaKazuha 9d4f8d903c
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
feat(addressables): 补全 catalog 可校验字段
补充 JSON/compact catalog 的 provider ID、bundle name、资源类型、hash、size、CRC 和依赖字段,并贯通 ResourceEntry、SQLite 资源索引和回归 golden。对 compact extra data 与 resource type index 的损坏返回明确错误,不再把 bundle name 当作 hash fallback。\n\n验证:cargo fmt --check;cargo test -p bat-core --locked;cargo test -p bat-adapters --locked;cargo test -p bat-infrastructure --locked;git diff --check。\n\nClippy 仍受既有 core/src/domain/game_client.rs:141 的 needless-question-mark 和 items-after-test-module 基线问题影响,未混入本 issue 修复。\n\nFixes #2
2026-08-19 23:10:16 +08:00

276 lines
9.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 provider ID。
///
/// 旧的 manifest 和资源索引没有该字段,缺省时保持 `None`。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_id: Option<String>,
/// Addressables bundle name。
///
/// 该值是定位/诊断字段,不作为资源 hash 的替代值。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bundle_name: Option<String>,
/// Addressables bundle 的 CRC32catalog 中的 `m_Crc`)。
///
/// `None` 表示 catalog 未提供该字段;Unity 用 `0` 表示「不做 CRC 校验」,
/// 因此 `Some(0)` 与 `None` 在校验时同样视为「无 CRC」。为向后兼容旧的
/// 持久化数据,反序列化时缺省为 `None`。
#[serde(default)]
pub crc: Option<u32>,
}
/// 资源解析与发布侧元数据。
///
/// 该结构默认全空,保证旧索引和只保存基础 manifest 信息的资源仍可反序列化。
/// 官方资源导入会按 release manifest 和 parse cache 填充这些字段,供
/// `resource.index` 等只读接口暴露版本、平台、bundle、TextAsset 和 TextUnit 摘要。
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ResourceMetadata {
/// 资源所属的官方 release ID。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub official_release_id: Option<String>,
/// 从官方相对路径推断的平台标签,例如 `windows` 或 `android`。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
/// 资源本身或所在 bundle 的官方相对路径。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bundle_path: Option<String>,
/// ZIP 内被解析到的 bundle entry;直接 bundle 为空。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub archive_entries: Vec<String>,
/// parse cache 中出现过的解析状态标签。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parse_statuses: Vec<String>,
/// 解析到的 Unity 版本集合。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unity_versions: Vec<String>,
/// UnityFS directory file 总数。
#[serde(default, skip_serializing_if = "is_zero")]
pub unityfs_file_count: u64,
/// Unity serialized file 总数。
#[serde(default, skip_serializing_if = "is_zero")]
pub serialized_file_count: u64,
/// TextAsset 对象总数。
#[serde(default, skip_serializing_if = "is_zero")]
pub text_asset_count: u64,
/// TextAsset 名称集合。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub text_assets: Vec<String>,
/// TextUnit 总数。
#[serde(default, skip_serializing_if = "is_zero")]
pub text_unit_count: u64,
/// TextUnit 格式标签集合,例如 `json`、`csv`、`tsv`、`plain`。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub text_unit_formats: Vec<String>,
/// TextUnit 提取阶段的非致命诊断数量。
#[serde(default, skip_serializing_if = "is_zero")]
pub text_unit_error_count: u64,
}
fn is_zero(value: &u64) -> bool {
*value == 0
}
/// 已下载字节与 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,
/// 解析、发布和索引侧扩展元数据。
#[serde(default)]
pub metadata: ResourceMetadata,
}
#[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(),
provider_id: None,
bundle_name: None,
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
})
);
}
}