feat(assetbundle): 完善官方资源解析与双目录发布

This commit is contained in:
2026-07-25 20:51:01 +08:00
parent 102b49b666
commit 3e9bb20d79
37 changed files with 4663 additions and 1333 deletions
+156
View File
@@ -87,6 +87,16 @@ pub struct UnityFsImportSummary {
pub directory_count: usize,
/// UnityFS directory 路径。
pub directories: Vec<String>,
/// UnityFS directory 解出的文件数量。
pub file_count: usize,
/// 成功解析出的 Unity serialized file 数量。
pub serialized_file_count: usize,
/// 成功解析出的 TextAsset 数量。
pub text_asset_count: usize,
/// TextAsset 名称列表。
pub text_assets: Vec<String>,
/// 非致命 serialized-file 解析诊断数量。
pub serialized_parse_error_count: usize,
}
/// Manifest 导入报告。
@@ -266,6 +276,15 @@ impl<'a> ResourceImportService<'a> {
.into_iter()
.map(|directory| directory.path)
.collect(),
file_count: parsed.files.len(),
serialized_file_count: parsed.serialized_files.len(),
text_asset_count: parsed.text_assets.len(),
text_assets: parsed
.text_assets
.into_iter()
.map(|asset| asset.name)
.collect(),
serialized_parse_error_count: parsed.serialized_parse_errors.len(),
})
}
}
@@ -375,6 +394,26 @@ mod tests {
data.extend_from_slice(&value.to_be_bytes());
}
fn push_i16_le(data: &mut Vec<u8>, value: i16) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u32_le(data: &mut Vec<u8>, value: u32) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_i32_le(data: &mut Vec<u8>, value: i32) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_i64_le(data: &mut Vec<u8>, value: i64) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u64_le(data: &mut Vec<u8>, value: u64) {
data.extend_from_slice(&value.to_le_bytes());
}
fn align(data: &mut Vec<u8>, alignment: usize) {
let remainder = data.len() % alignment;
if remainder != 0 {
@@ -414,6 +453,83 @@ mod tests {
data
}
fn synthetic_text_asset_unityfs_bundle() -> Vec<u8> {
let serialized_file = synthetic_serialized_text_asset();
let mut blocks_info = Vec::new();
blocks_info.extend_from_slice(&[1; 16]);
push_i32(&mut blocks_info, 1);
push_u32(&mut blocks_info, serialized_file.len() as u32);
push_u32(&mut blocks_info, serialized_file.len() as u32);
push_u16(&mut blocks_info, 0);
push_i32(&mut blocks_info, 1);
push_u64(&mut blocks_info, 0);
push_u64(&mut blocks_info, serialized_file.len() as u64);
push_u32(&mut blocks_info, 0);
push_c_string(&mut blocks_info, "CAB-scenario");
let mut data = Vec::new();
push_c_string(&mut data, "UnityFS");
push_u32(&mut data, 8);
push_c_string(&mut data, "5.x.x");
push_c_string(&mut data, "2021.3.56f2");
push_u64(&mut data, 0);
push_u32(&mut data, blocks_info.len() as u32);
push_u32(&mut data, blocks_info.len() as u32);
push_u32(&mut data, 0);
align(&mut data, 16);
data.extend_from_slice(&blocks_info);
data.extend_from_slice(&serialized_file);
let total_size = data.len() as u64;
let total_size_offset = b"UnityFS\0".len() + 4 + b"5.x.x\0".len() + b"2021.3.56f2\0".len();
data[total_size_offset..total_size_offset + 8].copy_from_slice(&total_size.to_be_bytes());
data
}
fn synthetic_serialized_text_asset() -> Vec<u8> {
let mut object_data = Vec::new();
push_u32_le(&mut object_data, 8);
object_data.extend_from_slice(b"Scenario");
align(&mut object_data, 4);
push_u32_le(&mut object_data, 15);
object_data.extend_from_slice("こんにちは".as_bytes());
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(0);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 49);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 1);
align(&mut metadata, 4);
push_i64_le(&mut metadata, 1);
push_u64_le(&mut metadata, 0);
push_u32_le(&mut metadata, object_data.len() as u32);
push_i32_le(&mut metadata, 0);
let header_len = 48usize;
let data_offset = header_len + metadata.len();
let file_size = data_offset + object_data.len();
let mut file = Vec::new();
push_u32(&mut file, metadata.len() as u32);
push_u32(&mut file, file_size as u32);
push_u32(&mut file, 22);
push_u32(&mut file, 0);
file.push(0);
file.extend_from_slice(&[0, 0, 0]);
push_u32(&mut file, metadata.len() as u32);
push_u64(&mut file, file_size as u64);
push_u64(&mut file, data_offset as u64);
push_u64(&mut file, 0);
file.extend_from_slice(&metadata);
file.extend_from_slice(&object_data);
file
}
fn synthetic_manifest() -> GenericManifest {
GenericManifest {
format: ManifestFormat::AddressablesCatalog,
@@ -594,6 +710,11 @@ mod tests {
assert_eq!(unityfs.block_count, 1);
assert_eq!(unityfs.directory_count, 1);
assert_eq!(unityfs.directories, vec!["SYNTHETIC-CAB".to_string()]);
assert_eq!(unityfs.file_count, 1);
assert_eq!(unityfs.serialized_file_count, 0);
assert_eq!(unityfs.text_asset_count, 0);
assert!(unityfs.text_assets.is_empty());
assert_eq!(unityfs.serialized_parse_error_count, 0);
assert_eq!(
report.imported[1].category,
ResourceImportCategory::TextAsset
@@ -645,6 +766,41 @@ mod tests {
);
}
#[tokio::test]
async fn import_summary_reports_text_assets_inside_assetbundle() {
let temp_dir = TempDir::new().unwrap();
let cas = FileSystemCasRepository::new(temp_dir.path().join("cas"));
let resources = InMemoryResourceRepository::new();
let service = ResourceImportService::new(&cas, &resources);
let manifest = manifest_with(vec![ResourceEntry {
path: "synthetic/scenario.bundle".to_string(),
hash: "synthetic-scenario-hash".to_string(),
size: 1,
resource_type: ResourceType::AssetBundle,
address: None,
dependencies: Vec::new(),
crc: None,
}]);
let report = service
.import_manifest_bundles(
&manifest,
&[BundleSource::new(
"scenario.bundle",
synthetic_text_asset_unityfs_bundle(),
)],
)
.await
.unwrap();
let unityfs = report.imported[0].unityfs.as_ref().unwrap();
assert_eq!(unityfs.file_count, 1);
assert_eq!(unityfs.serialized_file_count, 1);
assert_eq!(unityfs.text_asset_count, 1);
assert_eq!(unityfs.text_assets, vec!["Scenario".to_string()]);
assert_eq!(unityfs.serialized_parse_error_count, 0);
}
#[tokio::test]
async fn returns_error_when_bundle_data_is_missing() {
let temp_dir = TempDir::new().unwrap();