chore: establish development baseline

This commit is contained in:
2026-06-28 01:27:09 +08:00
commit dd53e3054e
131 changed files with 19327 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
//! Unity Addressables Catalog Driver
//!
//! 解析 Unity Addressables 的 catalog_Remote.json 文件
use super::driver::{GenericManifest, ManifestDriver, ManifestFormat, ManifestMetadata};
use async_trait::async_trait;
use bat_core::domain::{ResourceEntry, ResourceType};
use serde_json::Value;
use std::collections::HashMap;
/// Addressables Catalog Driver
pub struct AddressablesCatalogDriver;
impl AddressablesCatalogDriver {
/// 创建新的 Driver 实例
pub fn new() -> Self {
Self
}
}
impl Default for AddressablesCatalogDriver {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ManifestDriver for AddressablesCatalogDriver {
fn name(&self) -> &str {
"Unity Addressables Catalog"
}
fn format_version(&self) -> &str {
"1.0"
}
fn can_parse(&self, raw_data: &[u8]) -> bool {
// 检测是否是 Addressables Catalog
// 1. 必须是有效的 JSON
// 2. 必须包含 m_LocatorId 字段
// 3. m_LocatorId 值应该是 "AddressablesMainContentCatalog"
let text = match std::str::from_utf8(raw_data) {
Ok(t) => t,
Err(_) => return false,
};
let json: Value = match serde_json::from_str(text) {
Ok(j) => j,
Err(_) => return false,
};
let locator_id = match json.get("m_LocatorId") {
Some(id) => id,
None => return false,
};
let id_str = match locator_id.as_str() {
Some(s) => s,
None => return false,
};
id_str.contains("AddressablesMainContentCatalog") || id_str.contains("Addressables")
}
async fn parse(&self, raw_data: &[u8]) -> Result<GenericManifest, String> {
// 解析 JSON
let text = std::str::from_utf8(raw_data)
.map_err(|e| format!("Invalid UTF-8: {}", e))?;
let json: Value = serde_json::from_str(text)
.map_err(|e| format!("Invalid JSON: {}", e))?;
// 提取元数据
let locator_id = json.get("m_LocatorId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// 提取 CDN 前缀
let mut cdn_prefixes = Vec::new();
if let Some(prefixes) = json.get("m_InternalIdPrefixes").and_then(|v| v.as_array()) {
for prefix in prefixes {
if let Some(s) = prefix.as_str() {
cdn_prefixes.push(s.to_string());
}
}
}
// 提取资源列表(基础版本 - 仅从 m_InternalIds 提取)
let mut resources = Vec::new();
if let Some(internal_ids) = json.get("m_InternalIds").and_then(|v| v.as_array()) {
for (index, id) in internal_ids.iter().enumerate() {
if let Some(path) = id.as_str() {
// 跳过空路径
if path.is_empty() {
continue;
}
// 判断资源类型
let resource_type = if path.ends_with(".bundle") {
ResourceType::AssetBundle
} else if path.contains("catalog") {
ResourceType::Manifest
} else {
ResourceType::Other
};
resources.push(ResourceEntry {
path: path.to_string(),
hash: format!("addressable_{}", index), // 临时 Hash
size: 0, // 大小未知
resource_type,
});
}
}
}
// TODO: 解析 m_KeyDataString、m_EntryDataString 等压缩字段
// 这些字段使用了自定义压缩格式,需要实现解压缩算法
// 参考:Unity Addressables 源代码
let metadata = ManifestMetadata {
locator_id,
cdn_prefixes,
extra: HashMap::new(),
};
Ok(GenericManifest {
format: ManifestFormat::AddressablesCatalog,
resources,
metadata,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_can_parse_valid_catalog() {
let driver = AddressablesCatalogDriver::new();
let valid_json = r#"{
"m_LocatorId": "AddressablesMainContentCatalog",
"m_InternalIds": []
}"#;
assert!(driver.can_parse(valid_json.as_bytes()));
}
#[test]
fn test_can_parse_invalid() {
let driver = AddressablesCatalogDriver::new();
let invalid_json = r#"{"some": "other"}"#;
assert!(!driver.can_parse(invalid_json.as_bytes()));
let not_json = b"not json at all";
assert!(!driver.can_parse(not_json));
}
#[tokio::test]
async fn test_parse_simple_catalog() {
let driver = AddressablesCatalogDriver::new();
let catalog_json = r#"{
"m_LocatorId": "AddressablesMainContentCatalog",
"m_InternalIdPrefixes": [],
"m_InternalIds": [
"academy.bundle",
"character.bundle",
"catalog.json"
]
}"#;
let result = driver.parse(catalog_json.as_bytes()).await;
assert!(result.is_ok());
let manifest = result.unwrap();
assert_eq!(manifest.format, ManifestFormat::AddressablesCatalog);
assert_eq!(manifest.resources.len(), 3);
assert_eq!(manifest.resources[0].resource_type, ResourceType::AssetBundle);
}
}