fix(unityfs): 加固基础容器解析校验
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

补充 UnityFS 总大小、block/directory 计数、目录路径、重复项和边界校验,覆盖 LZMA 数据块与损坏输入回归。新增隔离真实 UnityPy bundle 测试入口,并用 /tmp 中的 char_118_yuki.ab 完成实际回归。复杂对象解析与发布级重打包继续保留在 G-005 后续范围。

Fixes #3
This commit is contained in:
2026-08-20 12:13:04 +08:00
parent 9d4f8d903c
commit 90083302a2
6 changed files with 181 additions and 21 deletions
+2 -2
View File
@@ -104,7 +104,7 @@ job/unit/provider run 状态。当前下载实现使用默认 8 个独立 worker
待完成:
- `crates/bat-assetbundle` 已具备 UnityFS 容器、对象表、TypeTree 元数据、基础字段读取、TextAsset 和 TextUnit 提取;UnityFS TextAsset patch 发布前置链路已可用,真实复杂版本差异、重打包和通用 Patch 仍未实现。
- `crates/bat-assetbundle` 已具备 UnityFS 容器、对象表、TypeTree 元数据、基础字段读取、TextAsset 和 TextUnit 提取;UnityFS 容器已补充总大小、计数、路径、重复 directory、LZMA 和边界校验,并通过 UnityPy 真实 bundle 隔离回归;UnityFS TextAsset patch 发布前置链路已可用,真实复杂版本差异、重打包和通用 Patch 仍未实现。
- Addressables parser 已覆盖当前真实形态 fixture/golden 与 `m_Crc`,但仍需继续覆盖二进制/压缩字段组合和更细失败诊断。
- 客户端发现、备份、应用补丁流程尚未连接真实实现。
@@ -294,7 +294,7 @@ GitHub issue 状态:#1 已关闭;#17 的历史决定不代表当前下载实
1. Issue #1P0,主体已实现):`bat.sock` Unix socket JSON-RPC 已扩展为面向 Go 服务层的 Rust Resource Backend API。统一 envelope`ok``status``error``data``request_id`)与 `BAT-ERR` 错误码模型已落地;`daemon.*`status/logs/stop/restart/reload/refresh/doctor)、`resource.*`state/sync/verify/repair/manifest/list/index)、`schedule.*`list/add/update/remove/run)、`parse.*`status/text_units/errors)、`translation.*`tasks/handoff/task.update/proofread)、`localized.*`status)、`catalog.*`status/refresh/diff/versions)、`task.*`status/list/cancel/logs)、文件级 `patch.apply``unityfs.patch_text_asset` / `unityfs.patch_string_field` / `unityfs.patch_field` 已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:发布级 `patch build` / `patch rollback`、复杂 `unityfs.*` 语义编辑、`task.create`(按设计由语义方法创建)、`daemon.clean-stable`(由 CLI 侧按进程生命周期显式执行,live RPC 内不做在线清理)、Redis 任务后端(`.env` 已预留配置键,接入时机另议)。Go 层通过 RPC 调用 Rust backend,不走 FFIFFI 降级说明见 `docs/architecture/official-resource-backend.md` §7)。
2. Go 侧:进度见 `docs/reports/GO_STATUS.md`。G-008 已关闭;`bat-api` 资源 bootstrap/分发 MVP 已落地,已含 `/v1/bootstrap``/v1/launcher/bootstrap`、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、动态 JSON no-store、OpenAPI、管理控制白名单、CDN Range/缓存头、RPC 周期刷新和 USERGUIDE 基础章节;仓库内 Rust/Go snapshot contract fixture 已落地,剩余为远程服务器全量 release 联调、refresh mtime/size 增量缓存和可选持久化。
3. 文本提取 / 翻译队列 / Patch 输入:`official-textunit-index.json``official-textunit-tasks.json``crowdin-textunit-queue.json` 已生成;TextUnit 明细、解析错误和离线翻译任务状态/失败原因已可查询,`translation.task.update` 已提供 Rust CLI、Go typed helper 和 bat-api dashboard 状态回写入口,`translation.proofread` 已提供人工校对状态标记入口。剩余为真实 Crowdin/provider worker、翻译记忆和 Patch 构建。
4. Issue #3P1):AssetBundle UnityFS 基础解析校验已具备离线和隔离真实样本覆盖;对象解析继续跟踪 G-005。
4. Issue #3已完成当前目标):AssetBundle UnityFS 基础解析校验已覆盖 header、block、directory、LZ4/LZMA、alignment、总大小、计数、路径安全、重复 directory、边界和隔离 UnityPy 真实样本;复杂对象解析与发布级重打包继续跟踪 G-005。
5. Issue #2(已完成当前目标):Addressables JSON/compact catalog 已提取 bundle provider、name、hash/size/CRC、资源类型和依赖关系,并贯通 ResourceEntry、SQLite 与 resource.index 输出;独立二进制格式仍明确拒绝。
6. 通用 Binary/JSON/Text Patch 基础已落地;复杂 AssetBundle 重打包和真实翻译系统仍应后置,UnityFS TextAsset patch 发布前置已具备回归测试。
+2 -2
View File
@@ -211,7 +211,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
1. **解析缓存闭环**:官方同步发布后生成 `official-parse-cache.json`,覆盖 manifest 全部条目、直接 bundle、zip 内 bundle、非候选资源和解析失败诊断;未变化文件按 URL、相对路径、size 和 BLAKE3 复用解析结果。
2. **Addressables 完整化**:覆盖 Windows/Android JSON、compact JSON 和后续二进制 catalog 入口,解析 provider、internal id、primary key、dependency、bundle name、hash、size、CRC 和资源类型。
3. **UnityFS 容器层**继续完善 header、block info、directory、data block、压缩、alignment、边界错误、directory 文件提取和真实样本回归
3. **UnityFS 容器层**基础目标已完成 header、block info、directory、data block、LZ4/LZMA、alignment、总大小/计数/路径/边界错误、directory 文件提取和 UnityPy 真实样本回归;复杂版本差异和发布级重打包另行推进
4. **Serialized file 层**:稳定 Unity serialized file header、type table、TypeTree node、object table、path id、class id 和 raw object bytes 表示。
5. **字段级解析层**:实现 TypeTree 字段 reader,支持 bool、integer、float、string、bytes、array、vector/staticvector 嵌套 `Array``List<T>` / `HashSet<T>` 集合 alias、map、PPtr、enum `value__` backing field、`LayerMask` / `BitField``m_Bits` backing field、常见固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、unknown fixed-size raw bytes 保留和同长度替换、TypeTree-covered managed reference / `SerializedReference` alias 和 TypeTree-covered managed reference registry 记录;managed-reference full typename 可拆为 assembly/namespace/class,常见 `m_ManagedReferences` / `RefIds` / verbose type 字段命名、`managedReference*` / `serializedReference*` metadata 和 `data` / `value` / `payload` / `object` / `managedReferencePayload` / `referencePayload` / `serializedReferencePayload` / `managedReferenceValue` / `referenceValue` / `serializedReferenceValue` / `managedReferenceObject` / `referenceObject` / `serializedReferenceObject` / `managedReferenceData` / `referenceData` / `serializedData` / `serializedReferenceData` payload 命名已有回归覆盖,TextUnit 只提取 payload 字符串并按结构化 record、`RefIds[n]` 等记录前缀或子字段保留类型上下文;array/vector/List/HashSet/map 元素与 registry payload 字段保留独立 field path、offset 和 byte size,可支撑字符串元素、managed-reference registry payload 字段、基础语义字段 patch、enum/bit_field 语义 patch、固定值类型 patch、unknown fixed-size bytes patch、object 字段组合和 TypeTree schema 支撑的 array/vector/List/HashSet/map 整体变长替换,`first/second``key/value` map entry schema 已有回归覆盖;解析模块当前处于维护冻结,未见样本驱动的完整 managed reference registry / map entry 变体和 unknown 字段结构语义暂不继续扩展,除非属于冻结规则允许的稳定性修复。
6. **文本对象入口**:实现 TextAsset、MonoBehaviour、ScriptableObject 的可扩展提取入口,输出可追溯到 bundle、serialized file、path id 和 field path 的文本定位。
@@ -403,7 +403,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
1. issue #17 已关闭(顺序下载 + 指数退避)。
2. G-008 已决策关闭:同步/运维命令行 = 近乎全自动的 Rust `bat`
3. G-009 / issue #19`bat-api` 资源分发 MVP 已落地;优先服务器联调;拉取仍在 Rust `bat`
4. 继续 Addressablesissue #2与 UnityFSissue #3 / G-005)。
4. 继续 Addressables 结构变体与 UnityFS 复杂对象能力issue #3 / G-005 的后续阶段)。
5.`crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建。
6. 继续扩展 G-011 剩余查询面:真实 provider worker 集成与状态落库验证、`doctor cas` 诊断入口和面向大索引的查询优化。
+139 -4
View File
@@ -6,6 +6,7 @@ use crate::types::{
ParsedAssetBundle, RawAssetBundle, UnityFsBlockInfo, UnityFsBundle, UnityFsCompression,
UnityFsDirectoryInfo, UnityFsFile, UnityFsHeader, UnitySerializedParseError,
};
use std::collections::HashSet;
use std::io::Cursor;
const UNITYFS_COMPRESSION_MASK: u32 = 0x3f;
@@ -113,9 +114,9 @@ fn parse_unityfs(data: &[u8]) -> Result<UnityFsBundle> {
flags: reader.read_u32("flags")?,
};
if header.total_size > data.len() as u64 {
if header.total_size != data.len() as u64 {
return Err(AssetBundleError::Parse(format!(
"UnityFS total_size {} exceeds file size {}",
"UnityFS total_size {} does not match file size {}",
header.total_size,
data.len()
)));
@@ -446,8 +447,14 @@ fn parse_blocks_info(
"invalid UnityFS block count: {block_count}"
)));
}
let block_count = checked_record_count(
block_count,
data.len().saturating_sub(reader.offset()),
10,
"block_count",
)?;
let mut blocks = Vec::with_capacity(block_count as usize);
let mut blocks = Vec::with_capacity(block_count);
for _ in 0..block_count {
let uncompressed_size = reader.read_u32("block_uncompressed_size")?;
let compressed_size = reader.read_u32("block_compressed_size")?;
@@ -466,8 +473,14 @@ fn parse_blocks_info(
"invalid UnityFS directory count: {directory_count}"
)));
}
let directory_count = checked_record_count(
directory_count,
data.len().saturating_sub(reader.offset()),
21,
"directory_count",
)?;
let mut directories = Vec::with_capacity(directory_count as usize);
let mut directories = Vec::with_capacity(directory_count);
for _ in 0..directory_count {
directories.push(UnityFsDirectoryInfo {
offset: reader.read_u64("directory_offset")?,
@@ -546,7 +559,15 @@ fn validate_directory_bounds(
data_region_size: u64,
directories: &[UnityFsDirectoryInfo],
) -> Result<()> {
let mut paths = HashSet::with_capacity(directories.len());
for (index, directory) in directories.iter().enumerate() {
validate_directory_path(index, &directory.path)?;
if !paths.insert(directory.path.as_str()) {
return Err(AssetBundleError::Parse(format!(
"UnityFS directory {} (index {index}) is duplicated",
directory.path
)));
}
let end = directory
.offset
.checked_add(directory.size)
@@ -567,6 +588,45 @@ fn validate_directory_bounds(
Ok(())
}
fn checked_record_count(
count: i32,
remaining_bytes: usize,
minimum_record_size: usize,
field: &str,
) -> Result<usize> {
let count = usize::try_from(count)
.map_err(|_| AssetBundleError::Parse(format!("invalid UnityFS {field}: {count}")))?;
let required_bytes = count.checked_mul(minimum_record_size).ok_or_else(|| {
AssetBundleError::Parse(format!(
"UnityFS {field} record size overflows usize: count {count}, minimum {minimum_record_size}"
))
})?;
if required_bytes > remaining_bytes {
return Err(AssetBundleError::Parse(format!(
"UnityFS {field} count {count} requires at least {required_bytes} bytes, only {remaining_bytes} remain"
)));
}
Ok(count)
}
fn validate_directory_path(index: usize, path: &str) -> Result<()> {
let is_windows_absolute = path.as_bytes().get(1) == Some(&b':');
if path.is_empty()
|| path.starts_with('/')
|| path.starts_with('\\')
|| is_windows_absolute
|| path
.replace('\\', "/")
.split('/')
.any(|component| component == "..")
{
return Err(AssetBundleError::Parse(format!(
"UnityFS directory path is unsafe at index {index}: {path:?}"
)));
}
Ok(())
}
struct UnityFsReader<'a> {
data: &'a [u8],
offset: usize,
@@ -972,6 +1032,21 @@ mod tests {
assert_eq!(parsed.files[0].data, payload);
}
#[test]
fn extracts_lzma_compressed_data_block() {
let parser = UnityFsParser::new();
let payload = b"localized-lzma-payload";
let mut compressed = Vec::new();
lzma_rs::lzma_compress(&mut Cursor::new(payload), &mut compressed).unwrap();
let data = synthetic_unityfs_bundle_with_payload("CAB-lzma", payload, &compressed, 1);
let parsed = parser.parse_bytes(&data).unwrap();
assert_eq!(parsed.files.len(), 1);
assert_eq!(parsed.files[0].path, "CAB-lzma");
assert_eq!(parsed.files[0].data, payload);
}
#[test]
fn extracts_data_block_after_block_info_alignment_padding() {
let parser = UnityFsParser::new();
@@ -1002,6 +1077,62 @@ mod tests {
assert!(error.contains("uncompressed data region size 4"), "{error}");
}
#[test]
fn rejects_unsafe_directory_path() {
let parser = UnityFsParser::new();
let data = synthetic_unityfs_bundle_with_payload("../outside", b"data", b"data", 0);
let error = parser.parse_bytes(&data).unwrap_err().to_string();
assert!(error.contains("unsafe"), "{error}");
assert!(error.contains("../outside"), "{error}");
}
#[test]
fn rejects_duplicate_directory_path() {
let parser = UnityFsParser::new();
let mut blocks_info = blocks_info(4);
push_i32_at(&mut blocks_info, 16 + 4 + 10, 2);
push_u64(&mut blocks_info, 0);
push_u64(&mut blocks_info, 0);
push_u32(&mut blocks_info, 0);
push_c_string(&mut blocks_info, "CAB-test");
let data = synthetic_unityfs_bundle(&blocks_info, 0, false);
let error = parser.parse_bytes(&data).unwrap_err().to_string();
assert!(error.contains("duplicated"), "{error}");
}
#[test]
fn rejects_declared_total_size_mismatch() {
let parser = UnityFsParser::new();
let mut data = synthetic_unityfs_bundle(&blocks_info(4), 0, false);
let total_size_offset = b"UnityFS\0".len() + 4 + b"5.x.x\0".len() + b"2021.3.56f2\0".len();
let declared_size = (data.len() as u64) - 1;
data[total_size_offset..total_size_offset + 8]
.copy_from_slice(&declared_size.to_be_bytes());
let error = parser.parse_bytes(&data).unwrap_err().to_string();
assert!(error.contains("does not match file size"), "{error}");
}
#[test]
fn rejects_block_count_that_cannot_fit_in_block_info() {
let parser = UnityFsParser::new();
let mut data = synthetic_unityfs_bundle(&blocks_info(4), 0, false);
// The fixed test header is aligned to offset 64; block_count follows
// the 16-byte blocks-info hash.
let block_count_offset = 64 + 16;
data[block_count_offset..block_count_offset + 4].copy_from_slice(&i32::MAX.to_be_bytes());
let error = parser.parse_bytes(&data).unwrap_err().to_string();
assert!(error.contains("block_count"), "{error}");
assert!(error.contains("requires at least"), "{error}");
}
#[test]
fn rejects_truncated_header_with_field_context() {
let parser = UnityFsParser::new();
@@ -1023,4 +1154,8 @@ mod tests {
"{error}"
);
}
fn push_i32_at(data: &mut [u8], offset: usize, value: i32) {
data[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
}
}
@@ -0,0 +1,24 @@
use bat_assetbundle::UnityFsParser;
use std::path::PathBuf;
#[test]
#[ignore = "requires BAT_REAL_UNITYFS_BUNDLE pointing at an isolated real UnityFS bundle"]
fn parses_isolated_real_unityfs_bundle() {
let path = PathBuf::from(
std::env::var("BAT_REAL_UNITYFS_BUNDLE").expect("BAT_REAL_UNITYFS_BUNDLE must be set"),
);
let data = std::fs::read(&path).expect("read isolated real UnityFS bundle");
let parsed = UnityFsParser::new()
.parse_bytes(&data)
.expect("parse isolated real UnityFS bundle");
assert_eq!(parsed.header.total_size, data.len() as u64);
assert!(!parsed.header.unity_version.is_empty());
assert!(!parsed.blocks.is_empty());
assert!(!parsed.directories.is_empty());
assert_eq!(parsed.files.len(), parsed.directories.len());
assert_eq!(
parsed.uncompressed_data_size,
parsed.files.iter().map(|file| file.size).sum::<u64>()
);
}
+10 -9
View File
@@ -29,7 +29,7 @@
| --- | --- | --- | --- |
| 官方 seed manifest | `TableCatalog.bytes``BundlePackingInfo.bytes``MediaCatalog.bytes` | 完整下载 URL、相对路径、hash 校验边界 | 已用于下载计划,仍需沉淀更多结构化字段 |
| Addressables catalog | `catalog_*.zip` 内 JSON/bin catalog、`catalog_*.hash` | asset path、provider、dependencies、size、CRC、bundle name | JSON/compact 当前目标字段已覆盖;未知结构返回明确错误 |
| UnityFS container | `.bundle`、zip 内 bundle | header、block、directory、解压文件、基础摘要 | 已支持基础解包、LZ4/LZMA、边界校验 |
| UnityFS container | `.bundle`、zip 内 bundle | header、block、directory、解压文件、基础摘要 | 已支持基础解包、LZ4/LZMA、alignment、大小/计数/路径/边界校验 |
| Serialized file | UnityFS directory 文件 | header、type table、TypeTree node、object table、TextAsset bytes | 已支持基础表结构和 TextAsset bytes |
| Unity 对象字段 | TextAsset、MonoBehaviour、ScriptableObject | 可翻译文本单元、上下文、资源定位 | TypeTree 基础字段读取、`SerializedReference` / prefixed managed-reference metadata alias、payload 提取和字符串提取已落地,真实结构覆盖继续扩大 |
| Patch 发布前解析 | 已翻译文本、中间格式、原版资源 | 可验证 patch manifest、汉化 release 目录 | UnityFS TextAsset 前置已落地,通用 Binary/JSON/Text Patch 与文件级 patch / UnityFS 写入入口可用,发布级 build/rollback 未开放 |
@@ -40,16 +40,17 @@
`crates/bat-assetbundle` 已经承担解析核心:
1. `UnityFsParser` 解析 UnityFS header、block info、directory。
1. `UnityFsParser` 解析 UnityFS header、block info、directory,并校验声明总大小与实际文件大小
2. 支持 LZ4/LZMA block info 和数据 block 解压。
3. 支持 block info at end 和官方样本中出现的 block data alignment。
4. 能从 UnityFS directory 提取文件 bytes。
5. `serialized` 模块能读取 Unity serialized file header、type table、TypeTree node 元数据、object table
6. 能提取 TextAsset 的 name 和原始 bytes
7. TypeTree field reader 已支持基础标量、string、bytes、array、vector/staticvector 嵌套 `Array` 形态、`List<T>` / `HashSet<T>` 集合 alias、map、PPtr、enum `value__` backing field、`LayerMask` / `BitField``m_Bits` backing field、嵌套对象、常见固定 Unity float/int/hash 值类型的 leaf 和 direct child TypeTree 形态、unknown fixed-size raw bytes 保留和同长度替换、TypeTree-covered managed reference、TypeTree-covered managed reference registry 记录、`m_ManagedReferences` / `RefIds` / `m_RefIds` / verbose type 字段等 registry 命名变体、`id` / `typeInfo` 等 metadata 命名变体、`data` / `value` / `payload` / `object` / `managedReferencePayload` / `referencePayload` / `serializedReferencePayload` / `managedReferenceValue` / `referenceValue` / `serializedReferenceValue` / `managedReferenceObject` / `referenceObject` / `serializedReferenceObject` / `managedReferenceData` / `referenceData` / `serializedData` 等 payload 命名变体、managed-reference full typename 拆解和字段 offset/size 诊断
8. `TextUnitExtractor` 已把 JSON/CSV/TSV/plain TextAsset、TypeTree 字符串字段和 TypeTree-covered managed reference payload 字符串输出为可序列化 TextUnit/JSONLzip 场景保留 archive entryTextUnit 明细包含 serialized file、path id、class id、field path、字段 offset/byte size、format、asset name 和上下文。managed-reference 类型元数据保留为 payload context,不进入翻译文本队列;即使 registry 暂时只能走 fallback 字段遍历,`RefIds``className``namespaceName``asmName` 等元数据别名也会被跳过,payload/value/object 家族和 `managedReferenceData` / `referenceData` / `serializedData` 仍按 payload 处理,并按 `RefIds[n]` 等记录前缀或子字段推导 metadata,避免多条 fallback record 混用 managed-reference context
9. `ResourceImportService` 能把 AssetBundle 摘要、TextAsset/Table/Media 分类和 TextUnit 摘要写入导入报告
10. 官方同步后 `OfficialParseCacheService` 能从 `official-download-manifest.json` 遍历所有资源,解析直接 bundle 和 zip 内条目,非候选资源记录为 unsupported,并缓存 TextUnit 数量/格式/诊断摘要,同时写出 `official-textunit-index.json``parse.text_units` / `parse.errors` 查询
4. 能从 UnityFS directory 提取文件 bytes,并拒绝越界、重复或不安全路径
5. 对 block/directory 计数先按剩余字节做有界检查,避免损坏输入触发超大内存分配
6. `serialized` 模块能读取 Unity serialized file header、type table、TypeTree node 元数据、object table
7. 能提取 TextAsset 的 name 和原始 bytes
8. TypeTree field reader 已支持基础标量、string、bytes、array、vector/staticvector 嵌套 `Array` 形态、`List<T>` / `HashSet<T>` 集合 alias、map、PPtr、enum `value__` backing field、`LayerMask` / `BitField``m_Bits` backing field、嵌套对象、常见固定 Unity float/int/hash 值类型的 leaf 和 direct child TypeTree 形态、unknown fixed-size raw bytes 保留和同长度替换、TypeTree-covered managed reference、TypeTree-covered managed reference registry 记录、`m_ManagedReferences` / `RefIds` / `m_RefIds` / verbose type 字段等 registry 命名变体、`id` / `typeInfo` 等 metadata 命名变体、`data` / `value` / `payload` / `object` / `managedReferencePayload` / `referencePayload` / `serializedReferencePayload` / `managedReferenceValue` / `referenceValue` / `serializedReferenceValue` / `managedReferenceObject` / `referenceObject` / `serializedReferenceObject` / `managedReferenceData` / `referenceData` / `serializedData` payload 命名变体、managed-reference full typename 拆解和字段 offset/size 诊断
9. `TextUnitExtractor` 已把 JSON/CSV/TSV/plain TextAsset、TypeTree 字符串字段和 TypeTree-covered managed reference payload 字符串输出为可序列化 TextUnit/JSONLzip 场景保留 archive entryTextUnit 明细包含 serialized file、path id、class id、field path、字段 offset/byte size、format、asset name 和上下文。managed-reference 类型元数据保留为 payload context,不进入翻译文本队列;即使 registry 暂时只能走 fallback 字段遍历,`RefIds``className``namespaceName``asmName` 等元数据别名也会被跳过,payload/value/object 家族和 `managedReferenceData` / `referenceData` / `serializedData` 仍按 payload 处理,并按 `RefIds[n]` 等记录前缀或子字段推导 metadata,避免多条 fallback record 混用 managed-reference context
10. `ResourceImportService` 能把 AssetBundle 摘要、TextAsset/Table/Media 分类和 TextUnit 摘要写入导入报告
11. 官方同步后 `OfficialParseCacheService` 能从 `official-download-manifest.json` 遍历所有资源,解析直接 bundle 和 zip 内条目,非候选资源记录为 unsupported,并缓存 TextUnit 数量/格式/诊断摘要,同时写出 `official-textunit-index.json``parse.text_units` / `parse.errors` 查询。
当前还不能宣称完整:
+4 -4
View File
@@ -111,13 +111,13 @@
### G-005AssetBundle 引擎解析器仍未完成
状态:**部分完成**
状态:**UnityFS 基础容器校验目标完成,复杂对象能力仍部分完成**
冻结状态:自 2026-07-30 起,G-005 不再作为默认推进项。解析层只接受维护冻结规则允许的稳定性修复、诊断修复、真实回归修复和文档校正;新增 TypeTree 语义类型、扩大解析覆盖和新增写入型解析入口全部暂停。冻结细则见 `docs/reports/PARSER_FREEZE.md`
现象:
- `crates/bat-assetbundle` 已接管 UnityFS 解析,提供 `UnityFsParser``UnityFsBundle`、header、block info、directory、压缩模式、block info at end、LZ4/LZMA block info 解压、数据 block 解压、directory 文件提取和边界诊断
- `crates/bat-assetbundle` 已接管 UnityFS 解析,提供 `UnityFsParser``UnityFsBundle`、header、block info、directory、压缩模式、block info at end、LZ4/LZMA block info 解压、数据 block 解压、alignment、总大小/计数/路径/目录边界诊断、directory 文件提取。
- `crates/bat-assetbundle::serialized` 已提供 Unity serialized file header、type table、TypeTree node 元数据、object table、TextAsset bytes 和基础 TypeTree field reader。
- `adapters/src/unity/unity_2021_3.rs` 已降为 Unity 版本选择薄层,复用 `bat-assetbundle`,不再维护第二套 UnityFS parser。
- `ResourceImportService` 的 UnityFS 摘要已经能暴露解包文件数、serialized file 数、TextAsset 名称、TextUnit 数量/格式和字段诊断。
@@ -132,8 +132,8 @@ issue 43 例外说明:新增的 `parse repack` 只编排已有 TextAsset、Typ
当前验收证据:
- `crates/bat-assetbundle` 能解析结构化测试样本和隔离真实样本。
- 支持 UnityFS header、blocks、directory、metadata 摘要、directory 文件提取。
- `crates/bat-assetbundle` 能解析结构化测试样本和隔离真实样本;新增 UnityPy `char_118_yuki.ab` 真实 bundle 回归入口,样本只下载到 `/tmp`,不入库
- 支持 UnityFS header、blocks、directory、metadata 摘要、directory 文件提取,并拒绝声明总大小不符、计数越界、重复/不安全路径和目录数据越界
- 支持 Unity serialized file object table、TypeTree node 元数据和 TextAsset 提取的合成 fixture。
- 错误包含偏移和字段上下文。