mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 01:15:14 +08:00
feat(addressables): 提取 m_Crc 并提供 size/CRC 校验 API(issue #2)
Addressables catalog 里的 m_Crc(bundle IEEE CRC-32)此前从未解析;
声明的 size 也只作元数据、无校验能力。本次:
- ResourceEntry 新增 crc: Option<u32>(serde default 向后兼容),
compact 与 expanded 两种 catalog 形态均解析 m_Crc/m_Crc→crc
- core 新增 crc32_ieee(IEEE CRC-32,等价 zlib/Unity m_Crc)与
ResourceEntry::{declared_crc, verify_downloaded_bytes}:按声明的
size/crc 校验字节,0 视为「无 CRC」跳过
- SqliteResourceRepository 持久化 crc 列,旧库经幂等 ensure_column
迁移补列(pragma_table_info 判断后 ALTER)
- golden 投影与 fixture 补 crc 字段,验证真实形态 catalog 提取贯通
校验 API 暂不接入 import 覆盖路径(该路径按 CAS id 重写 hash/size 是
既定语义,且合成测试的声明值不匹配实际字节);接入下载/导入校验留
待 G-011。
验证:core crc32 标准向量 + verify 分支单测、expanded 形态非零 crc
提取单测、golden 端到端;core/adapters/infrastructure 全测试 + fmt +
clippy --all-targets -D warnings 全绿。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -425,6 +425,7 @@ mod tests {
|
||||
resource_type: ResourceType::AssetBundle,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
ResourceEntry {
|
||||
path: "synthetic/catalog.json".to_string(),
|
||||
@@ -433,6 +434,7 @@ mod tests {
|
||||
resource_type: ResourceType::Manifest,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
ResourceEntry {
|
||||
path: "TextAssets/dialogue.csv".to_string(),
|
||||
@@ -441,6 +443,7 @@ mod tests {
|
||||
resource_type: ResourceType::TextAsset,
|
||||
address: Some("dialogue".to_string()),
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
ResourceEntry {
|
||||
path: "TableBundles/ExcelDB.db".to_string(),
|
||||
@@ -449,6 +452,7 @@ mod tests {
|
||||
resource_type: ResourceType::TableBundle,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
ResourceEntry {
|
||||
path: "MediaResources-Windows/voice/title.acb".to_string(),
|
||||
@@ -457,6 +461,7 @@ mod tests {
|
||||
resource_type: ResourceType::Media,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
],
|
||||
metadata: ManifestMetadata {
|
||||
@@ -487,6 +492,7 @@ mod tests {
|
||||
resource_type: ResourceType::TextAsset,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,6 +513,7 @@ mod tests {
|
||||
resource_type: ResourceType::AssetBundle,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -129,13 +129,18 @@ impl SqliteResourceRepository {
|
||||
resource_type TEXT NOT NULL,
|
||||
local_path TEXT NOT NULL,
|
||||
address TEXT,
|
||||
dependencies_json TEXT NOT NULL DEFAULT '[]'
|
||||
dependencies_json TEXT NOT NULL DEFAULT '[]',
|
||||
crc INTEGER
|
||||
)
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 向后兼容:早于 crc 列的旧库缺少该列,按需补加(新建库已含该列,
|
||||
// pragma 检查后不会重复 ALTER)。
|
||||
Self::ensure_column(&self.pool, "resources", "crc", "INTEGER").await?;
|
||||
|
||||
Self::execute_query(
|
||||
&self.pool,
|
||||
sqlx::query(
|
||||
@@ -182,6 +187,33 @@ impl SqliteResourceRepository {
|
||||
.map_err(|error| bat_core::Error::Other(error.into()))
|
||||
}
|
||||
|
||||
/// 幂等地为 `table` 补加 `column`(若尚不存在)。用于向后兼容的 schema 迁移。
|
||||
async fn ensure_column(
|
||||
pool: &SqlitePool,
|
||||
table: &str,
|
||||
column: &str,
|
||||
column_type: &str,
|
||||
) -> bat_core::Result<()> {
|
||||
let exists: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM pragma_table_info(?1) WHERE name = ?2")
|
||||
.bind(table)
|
||||
.bind(column)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
||||
if exists == 0 {
|
||||
// 表名/列名/类型均为内部常量,非用户输入,可安全内插。
|
||||
Self::execute_query(
|
||||
pool,
|
||||
sqlx::query(&format!(
|
||||
"ALTER TABLE {table} ADD COLUMN {column} {column_type}"
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resource_type_to_str(resource_type: ResourceType) -> &'static str {
|
||||
match resource_type {
|
||||
ResourceType::AssetBundle => "AssetBundle",
|
||||
@@ -219,7 +251,8 @@ impl SqliteResourceRepository {
|
||||
}
|
||||
|
||||
fn resource_from_row(row: ResourceRow) -> bat_core::Result<Resource> {
|
||||
let (id, path, hash, size, resource_type, local_path, address, dependencies_json) = row;
|
||||
let (id, path, hash, size, resource_type, local_path, address, dependencies_json, crc) =
|
||||
row;
|
||||
Ok(Resource {
|
||||
id,
|
||||
local_path: PathBuf::from(local_path),
|
||||
@@ -230,6 +263,7 @@ impl SqliteResourceRepository {
|
||||
resource_type: Self::resource_type_from_str(&resource_type)?,
|
||||
address,
|
||||
dependencies: Self::dependencies_from_json(&dependencies_json)?,
|
||||
crc: crc.and_then(|value| u32::try_from(value).ok()),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -269,7 +303,7 @@ impl SqliteResourceRepository {
|
||||
limit: Option<usize>,
|
||||
) -> bat_core::Result<Vec<Resource>> {
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(
|
||||
"SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json FROM resources",
|
||||
"SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc FROM resources",
|
||||
);
|
||||
Self::apply_filters(&mut builder, query)?;
|
||||
builder.push(" ORDER BY id");
|
||||
@@ -308,9 +342,9 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO resources (
|
||||
id, path, hash, size, resource_type, local_path, address, dependencies_json
|
||||
id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
path = excluded.path,
|
||||
hash = excluded.hash,
|
||||
@@ -318,7 +352,8 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
resource_type = excluded.resource_type,
|
||||
local_path = excluded.local_path,
|
||||
address = excluded.address,
|
||||
dependencies_json = excluded.dependencies_json
|
||||
dependencies_json = excluded.dependencies_json,
|
||||
crc = excluded.crc
|
||||
"#,
|
||||
)
|
||||
.bind(resource.id.clone())
|
||||
@@ -328,7 +363,8 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
.bind(Self::resource_type_to_str(resource.entry.resource_type))
|
||||
.bind(resource.local_path.to_string_lossy().to_string())
|
||||
.bind(resource.entry.address.clone())
|
||||
.bind(dependencies),
|
||||
.bind(dependencies)
|
||||
.bind(resource.entry.crc.map(i64::from)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -338,7 +374,7 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
async fn find_by_id(&self, id: &str) -> bat_core::Result<Resource> {
|
||||
let row: Option<ResourceRow> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
||||
FROM resources
|
||||
WHERE id = ?1
|
||||
"#,
|
||||
@@ -356,7 +392,7 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
async fn find_by_hash(&self, hash: &str) -> bat_core::Result<Resource> {
|
||||
let row: Option<ResourceRow> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
||||
FROM resources
|
||||
WHERE hash = ?1
|
||||
ORDER BY id
|
||||
@@ -418,6 +454,7 @@ type ResourceRow = (
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
Option<i64>,
|
||||
);
|
||||
|
||||
fn glob_to_like(pattern: &str) -> String {
|
||||
@@ -505,6 +542,7 @@ mod tests {
|
||||
resource_type,
|
||||
address: None,
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user