mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
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
This commit is contained in:
@@ -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-32;0 表示不做 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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
+4
@@ -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<_>>(),
|
||||
|
||||
Reference in New Issue
Block a user