feat(addressables): 补全 catalog 可校验字段
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

补充 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
This commit is contained in:
2026-08-19 23:10:16 +08:00
parent 72c1a879a3
commit 9d4f8d903c
18 changed files with 311 additions and 55 deletions
+166 -37
View File
@@ -256,6 +256,22 @@ impl AddressablesCatalogDriver {
}
let address = Self::string_field(value, &["address", "Address", "m_Address", "key", "Key"]);
let provider_id = Self::catalog_string_field(
value,
&[
"provider_id",
"providerId",
"ProviderId",
"provider",
"Provider",
"m_ProviderId",
"m_Provider",
],
);
let bundle_name = Self::catalog_string_field(
value,
&["bundle_name", "bundleName", "BundleName", "m_BundleName"],
);
let hash = value
.get("hash")
.or_else(|| value.get("Hash"))
@@ -264,28 +280,32 @@ impl AddressablesCatalogDriver {
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("addressable_{}", index));
let size = value
.get("size")
.or_else(|| value.get("Size"))
.or_else(|| value.get("m_Size"))
.and_then(|value| value.as_u64())
.unwrap_or_default();
let size = Self::u64_field(value, &["size", "Size", "m_Size"]).unwrap_or_default();
let dependencies = Self::dependencies_from_entry(value);
let crc = value
.get("crc")
.or_else(|| value.get("Crc"))
.or_else(|| value.get("m_Crc"))
.and_then(|value| value.as_u64())
.and_then(|value| u32::try_from(value).ok());
let crc = Self::u32_field(value, &["crc", "Crc", "m_Crc"]);
let resource_type_name = Self::type_name_field(
value,
&[
"resource_type",
"resourceType",
"ResourceType",
"m_ResourceType",
],
);
Some(ResourceEntry {
path: path.to_string(),
hash,
size,
resource_type: Self::resource_type_for_path(path),
resource_type: Self::resource_type_for_compact_entry(
path,
resource_type_name.as_deref(),
),
address,
dependencies,
provider_id,
bundle_name,
crc,
})
}
@@ -299,6 +319,42 @@ impl AddressablesCatalogDriver {
})
}
fn catalog_string_field(value: &Value, fields: &[&str]) -> Option<String> {
Self::string_field(value, fields).or_else(|| {
["extra_data", "ExtraData", "m_ExtraData", "data", "Data"]
.iter()
.find_map(|field| value.get(*field))
.and_then(|extra| Self::string_field(extra, fields))
})
}
fn type_name_field(value: &Value, fields: &[&str]) -> Option<String> {
fields.iter().find_map(|field| {
let value = value.get(*field)?;
value.as_str().map(ToOwned::to_owned).or_else(|| {
value
.get("m_ClassName")
.or_else(|| value.get("ClassName"))
.or_else(|| value.get("class_name"))
.and_then(|name| name.as_str())
.map(ToOwned::to_owned)
})
})
}
fn u64_field(value: &Value, fields: &[&str]) -> Option<u64> {
fields.iter().find_map(|field| {
let value = value.get(*field)?;
value
.as_u64()
.or_else(|| value.as_str()?.parse::<u64>().ok())
})
}
fn u32_field(value: &Value, fields: &[&str]) -> Option<u32> {
Self::u64_field(value, fields).and_then(|value| u32::try_from(value).ok())
}
fn string_array_field(value: &Value, fields: &[&str]) -> Vec<String> {
for field in fields {
if let Some(array) = value.get(field).and_then(|value| value.as_array()) {
@@ -383,6 +439,8 @@ impl AddressablesCatalogDriver {
resource_type: Self::resource_type_for_path(path),
address: None,
dependencies: Vec::new(),
provider_id: None,
bundle_name: None,
crc: None,
})
})
@@ -421,6 +479,8 @@ impl AddressablesCatalogDriver {
resource_type,
address: None,
dependencies: Vec::new(),
provider_id: None,
bundle_name: None,
crc: None,
});
}
@@ -479,8 +539,9 @@ impl AddressablesCatalogDriver {
internal_ids.len()
)
})?;
provider_ids
let provider_id = provider_ids
.get(record.provider_index as usize)
.cloned()
.ok_or_else(|| {
format!(
"compact entry {index} provider_index {} out of range {}",
@@ -499,13 +560,12 @@ impl AddressablesCatalogDriver {
)
})?;
let path = Self::normalize_internal_id(&internal_id_prefixes, internal_id);
let extra = Self::extra_data_at(&extra_data, record.data_index);
let resource_type_name = Self::resource_type_name(json, record.resource_type_index);
let extra = Self::extra_data_at(&extra_data, record.data_index)?;
let resource_type_name = Self::resource_type_name(json, record.resource_type_index)?;
let dependencies = Self::compact_dependencies(record, &entry_records, &buckets, &keys);
let hash = extra
.hash
.filter(|value| !value.is_empty())
.or_else(|| extra.bundle_name.filter(|value| !value.is_empty()))
.unwrap_or_else(|| format!("addressable_{}", index));
resources.push(ResourceEntry {
@@ -522,6 +582,8 @@ impl AddressablesCatalogDriver {
Some(primary_key)
},
dependencies,
provider_id: Some(provider_id),
bundle_name: extra.bundle_name,
crc: extra.crc,
});
}
@@ -632,25 +694,34 @@ impl AddressablesCatalogDriver {
.collect()
}
fn extra_data_at(extra_data: &[u8], data_index: i32) -> AddressablesExtraData {
fn extra_data_at(extra_data: &[u8], data_index: i32) -> Result<AddressablesExtraData, String> {
if data_index < 0 {
return AddressablesExtraData::default();
return Ok(AddressablesExtraData::default());
}
let Some((object, _)) = Self::read_serialized_object(extra_data, data_index as usize)
else {
return AddressablesExtraData::default();
return Err(format!(
"compact entry extra data index {} is not decodable",
data_index
));
};
let AddressablesObject::JsonObject { json, .. } = object else {
return AddressablesExtraData::default();
return Err(format!(
"compact entry extra data index {} is not a JSON object",
data_index
));
};
let Some(json) = json else {
return AddressablesExtraData::default();
return Err(format!(
"compact entry extra data index {} contains invalid JSON",
data_index
));
};
AddressablesExtraData {
Ok(AddressablesExtraData {
hash: json
.get("m_Hash")
.and_then(|value| value.as_str())
@@ -659,26 +730,31 @@ impl AddressablesCatalogDriver {
.get("m_BundleName")
.and_then(|value| value.as_str())
.map(ToOwned::to_owned),
bundle_size: json.get("m_BundleSize").and_then(|value| value.as_u64()),
bundle_size: Self::u64_field(&json, &["m_BundleSize", "bundle_size", "size"]),
// m_Crc 是 bundle 的 IEEE CRC-320 表示不做 CRC 校验,忠实保留原值。
crc: json
.get("m_Crc")
.and_then(|value| value.as_u64())
.and_then(|value| u32::try_from(value).ok()),
}
crc: Self::u32_field(&json, &["m_Crc", "crc", "Crc"]),
})
}
fn resource_type_name(json: &Value, index: i32) -> Option<String> {
fn resource_type_name(json: &Value, index: i32) -> Result<Option<String>, String> {
if index < 0 {
return None;
return Ok(None);
}
json.get("m_resourceTypes")?
.as_array()?
.get(index as usize)?
.get("m_ClassName")?
.as_str()
.map(ToOwned::to_owned)
let resource_types = json
.get("m_resourceTypes")
.and_then(|value| value.as_array())
.ok_or_else(|| "compact catalog missing m_resourceTypes array".to_string())?;
let value = resource_types.get(index as usize).ok_or_else(|| {
format!(
"compact resource_type_index {} out of range {}",
index,
resource_types.len()
)
})?;
Self::type_name_field(value, &["m_ClassName", "ClassName", "class_name"])
.ok_or_else(|| format!("compact resource type {} has no class name", index))
.map(Some)
}
fn normalize_internal_id(prefixes: &[String], internal_id: &str) -> String {
@@ -869,6 +945,25 @@ impl AddressablesCatalogDriver {
fallback_hash_count.to_string(),
);
}
let provider_count = resources
.iter()
.filter(|entry| entry.provider_id.is_some())
.count();
if provider_count > 0 {
extra.insert("provider_id_count".to_string(), provider_count.to_string());
}
let bundle_name_count = resources
.iter()
.filter(|entry| entry.bundle_name.is_some())
.count();
if bundle_name_count > 0 {
extra.insert(
"bundle_name_count".to_string(),
bundle_name_count.to_string(),
);
}
}
}
@@ -1146,6 +1241,8 @@ mod tests {
"hash": "synthetic-entry-hash",
"size": 119,
"crc": 3735928559,
"provider_id": "synthetic-provider",
"bundle_name": "synthetic-bundle",
"address": "Character_001",
"dependencies": ["synthetic/shared.bundle"]
},
@@ -1163,6 +1260,14 @@ mod tests {
assert_eq!(manifest.resources[0].path, "synthetic/minimal.bundle");
assert_eq!(manifest.resources[0].hash, "synthetic-entry-hash");
assert_eq!(manifest.resources[0].size, 119);
assert_eq!(
manifest.resources[0].provider_id.as_deref(),
Some("synthetic-provider")
);
assert_eq!(
manifest.resources[0].bundle_name.as_deref(),
Some("synthetic-bundle")
);
// m_Crc(此处 0xDEADBEEF)应被提取;缺该字段的条目为 None。
assert_eq!(manifest.resources[0].crc, Some(0xDEAD_BEEF));
assert_eq!(manifest.resources[1].crc, None);
@@ -1207,6 +1312,14 @@ mod tests {
manifest.metadata.extra.get("dependency_count"),
Some(&"1".to_string())
);
assert_eq!(
manifest.metadata.extra.get("provider_id_count"),
Some(&"1".to_string())
);
assert_eq!(
manifest.metadata.extra.get("bundle_name_count"),
Some(&"1".to_string())
);
}
#[tokio::test]
@@ -1243,6 +1356,14 @@ mod tests {
assert_eq!(resource.hash, "hash-compact");
assert_eq!(resource.size, 42);
assert_eq!(resource.crc, Some(0x1234_5678));
assert_eq!(
resource.provider_id.as_deref(),
Some("UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider")
);
assert_eq!(
resource.bundle_name.as_deref(),
Some("synthetic-bundle-name")
);
assert_eq!(resource.resource_type, ResourceType::AssetBundle);
assert_eq!(resource.address.as_deref(), Some("synthetic.bundle"));
assert_eq!(
@@ -1261,6 +1382,14 @@ mod tests {
manifest.metadata.extra.get("declared_crc_count"),
Some(&"1".to_string())
);
assert_eq!(
manifest.metadata.extra.get("provider_id_count"),
Some(&"1".to_string())
);
assert_eq!(
manifest.metadata.extra.get("bundle_name_count"),
Some(&"1".to_string())
);
}
#[tokio::test]
+25
View File
@@ -21,6 +21,19 @@ async fn parses_current_catalog_fixture_with_resource_categories() {
manifest.resources[3].dependencies,
vec!["shared_dependencies.bundle".to_string()]
);
assert_eq!(
manifest.resources[0].provider_id.as_deref(),
Some("provider-table")
);
assert_eq!(
manifest.resources[0].bundle_name.as_deref(),
Some("table-bundle")
);
assert_eq!(manifest.resources[0].crc, Some(0x1234_5678));
assert_eq!(
manifest.resources[3].provider_id.as_deref(),
Some("provider-bundle")
);
assert_eq!(
manifest.metadata.cdn_prefixes,
vec!["https://fixture.invalid/current/".to_string()]
@@ -63,6 +76,18 @@ async fn parses_catalog_structure_change_with_alias_fields() {
manifest.resources[0].dependencies,
vec!["shared_assets_current.bundle".to_string()]
);
assert_eq!(
manifest.resources[0].provider_id.as_deref(),
Some("provider-android")
);
assert_eq!(
manifest.resources[0].bundle_name.as_deref(),
Some("title-android-bundle")
);
assert_eq!(manifest.resources[1].resource_type, ResourceType::TextAsset);
assert_eq!(manifest.resources[1].address.as_deref(), Some("lesson"));
assert_eq!(
manifest.resources[1].provider_id.as_deref(),
Some("provider-text")
);
}
@@ -8,6 +8,9 @@
"internal_id": "TableBundles/ExcelDB.db",
"hash": "current-table-hash",
"size": 4096,
"provider_id": "provider-table",
"bundle_name": "table-bundle",
"crc": "305419896",
"address": "ExcelDB",
"dependencies": []
},
@@ -15,6 +18,8 @@
"internal_id": "MediaResources-Windows/voice/title.acb",
"hash": "current-media-hash",
"size": 2048,
"m_ProviderId": "provider-media",
"m_BundleName": "media-bundle",
"address": "title",
"dependencies": []
},
@@ -22,6 +27,8 @@
"internal_id": "TextAssets/dialogue.csv",
"hash": "current-text-hash",
"size": 128,
"Provider": "provider-text",
"BundleName": "text-bundle",
"address": "dialogue",
"dependencies": []
},
@@ -29,6 +36,8 @@
"internal_id": "shared_assets_current.bundle",
"hash": "current-bundle-hash",
"size": 8192,
"provider": "provider-bundle",
"bundleName": "shared-bundle",
"address": "shared_assets_current",
"dependencies": [
"shared_dependencies.bundle"
@@ -9,6 +9,8 @@
"InternalId": "MediaResources-Android/voice/title.awb",
"Hash": "changed-media-hash",
"Size": 65536,
"ProviderId": "provider-android",
"BundleName": "title-android-bundle",
"Address": "title-android",
"m_Dependencies": [
"shared_assets_current.bundle"
@@ -18,6 +20,8 @@
"Path": "TextAssets/lesson.json",
"Hash": "changed-text-hash",
"Size": 512,
"provider_id": "provider-text",
"bundle_name": "lesson-bundle",
"Key": "lesson"
}
]
@@ -13,6 +13,8 @@
"dependencies": [
"shared_assets_all_123.bundle"
],
"provider_id": "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider",
"bundle_name": "bundle-main",
"crc": 0
},
{
@@ -22,6 +24,8 @@
"resource_type": "AssetBundle",
"address": "academy-_mxload-prefabs-2025-08-26_assets_all_1581352935.bundle",
"dependencies": [],
"provider_id": "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider",
"bundle_name": "bundle-second",
"crc": 0
},
{
@@ -31,16 +35,20 @@
"resource_type": "AssetBundle",
"address": "shared_assets_all_123.bundle",
"dependencies": [],
"provider_id": "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider",
"bundle_name": "bundle-shared",
"crc": 0
}
],
"metadata": {
"asset_bundle_count": "3",
"declared_size_count": "3",
"bundle_name_count": "3",
"dependency_count": "1",
"internal_id_count": "3",
"resource_count": "3",
"resource_type_count": "1",
"provider_id_count": "3",
"key_object_count": "4",
"bucket_record_count": "4",
"entry_record_count": "3",
@@ -22,6 +22,8 @@ async fn parses_real_shape_addressables_catalog_against_golden() {
"resource_type": format!("{:?}", resource.resource_type),
"address": resource.address,
"dependencies": resource.dependencies,
"provider_id": resource.provider_id,
"bundle_name": resource.bundle_name,
"crc": resource.crc,
})
}).collect::<Vec<_>>(),