From 90083302a2c1e5efea7fac47fcbd6d97c817ef1f Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Thu, 20 Aug 2026 12:13:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(unityfs):=20=E5=8A=A0=E5=9B=BA=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E5=AE=B9=E5=99=A8=E8=A7=A3=E6=9E=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充 UnityFS 总大小、block/directory 计数、目录路径、重复项和边界校验,覆盖 LZMA 数据块与损坏输入回归。新增隔离真实 UnityPy bundle 测试入口,并用 /tmp 中的 char_118_yuki.ab 完成实际回归。复杂对象解析与发布级重打包继续保留在 G-005 后续范围。 Fixes #3 --- CURRENT_STATUS.md | 4 +- PROJECT_PLAN.md | 4 +- crates/bat-assetbundle/src/parser.rs | 143 +++++++++++++++++- .../tests/real_unityfs_reference.rs | 24 +++ docs/architecture/assetbundle.md | 19 +-- docs/reports/CURRENT_GAPS.md | 8 +- 6 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 crates/bat-assetbundle/tests/real_unityfs_reference.rs diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md index 6b598d1..ab4ccac 100644 --- a/CURRENT_STATUS.md +++ b/CURRENT_STATUS.md @@ -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 #1(P0,主体已实现):`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 循环互斥;任务历史持久化于 `/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,不走 FFI(FFI 降级说明见 `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 #3(P1):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 发布前置已具备回归测试。 diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index 8213df5..94f00a4 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -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` / `HashSet` 集合 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. 继续 Addressables(issue #2)与 UnityFS(issue #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` 诊断入口和面向大索引的查询优化。 diff --git a/crates/bat-assetbundle/src/parser.rs b/crates/bat-assetbundle/src/parser.rs index 6798c93..2c27588 100644 --- a/crates/bat-assetbundle/src/parser.rs +++ b/crates/bat-assetbundle/src/parser.rs @@ -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 { 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 { + 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()); + } } diff --git a/crates/bat-assetbundle/tests/real_unityfs_reference.rs b/crates/bat-assetbundle/tests/real_unityfs_reference.rs new file mode 100644 index 0000000..58036ac --- /dev/null +++ b/crates/bat-assetbundle/tests/real_unityfs_reference.rs @@ -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::() + ); +} diff --git a/docs/architecture/assetbundle.md b/docs/architecture/assetbundle.md index fcea0dd..79eef27 100644 --- a/docs/architecture/assetbundle.md +++ b/docs/architecture/assetbundle.md @@ -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` / `HashSet` 集合 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/JSONL;zip 场景保留 archive entry,TextUnit 明细包含 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` / `HashSet` 集合 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/JSONL;zip 场景保留 archive entry,TextUnit 明细包含 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` 查询。 当前还不能宣称完整: diff --git a/docs/reports/CURRENT_GAPS.md b/docs/reports/CURRENT_GAPS.md index 2f32837..20b9cc5 100644 --- a/docs/reports/CURRENT_GAPS.md +++ b/docs/reports/CURRENT_GAPS.md @@ -111,13 +111,13 @@ ### G-005:AssetBundle 引擎解析器仍未完成 -状态:**部分完成** +状态:**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。 - 错误包含偏移和字段上下文。