//! 资源领域对象 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, /// 该资源依赖的其他资源标识 pub dependencies: Vec, /// Addressables bundle 的 CRC32(catalog 中的 `m_Crc`)。 /// /// `None` 表示 catalog 未提供该字段;Unity 用 `0` 表示「不做 CRC 校验」, /// 因此 `Some(0)` 与 `None` 在校验时同样视为「无 CRC」。为向后兼容旧的 /// 持久化数据,反序列化时缺省为 `None`。 #[serde(default)] pub crc: Option, } /// 已下载字节与 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 { 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) -> 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 }) ); } }