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`,但仍需继续覆盖二进制/压缩字段组合和更细失败诊断。 - 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)。 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 增量缓存和可选持久化。 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 构建。 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 输出;独立二进制格式仍明确拒绝。 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 发布前置已具备回归测试。 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 复用解析结果。 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 和资源类型。 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 表示。 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 字段结构语义暂不继续扩展,除非属于冻结规则允许的稳定性修复。 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 的文本定位。 6. **文本对象入口**:实现 TextAsset、MonoBehaviour、ScriptableObject 的可扩展提取入口,输出可追溯到 bundle、serialized file、path id 和 field path 的文本定位。
@@ -403,7 +403,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
1. issue #17 已关闭(顺序下载 + 指数退避)。 1. issue #17 已关闭(顺序下载 + 指数退避)。
2. G-008 已决策关闭:同步/运维命令行 = 近乎全自动的 Rust `bat` 2. G-008 已决策关闭:同步/运维命令行 = 近乎全自动的 Rust `bat`
3. G-009 / issue #19`bat-api` 资源分发 MVP 已落地;优先服务器联调;拉取仍在 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 构建。 5.`crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建。
6. 继续扩展 G-011 剩余查询面:真实 provider worker 集成与状态落库验证、`doctor cas` 诊断入口和面向大索引的查询优化。 6. 继续扩展 G-011 剩余查询面:真实 provider worker 集成与状态落库验证、`doctor cas` 诊断入口和面向大索引的查询优化。
+139 -4
View File
@@ -6,6 +6,7 @@ use crate::types::{
ParsedAssetBundle, RawAssetBundle, UnityFsBlockInfo, UnityFsBundle, UnityFsCompression, ParsedAssetBundle, RawAssetBundle, UnityFsBlockInfo, UnityFsBundle, UnityFsCompression,
UnityFsDirectoryInfo, UnityFsFile, UnityFsHeader, UnitySerializedParseError, UnityFsDirectoryInfo, UnityFsFile, UnityFsHeader, UnitySerializedParseError,
}; };
use std::collections::HashSet;
use std::io::Cursor; use std::io::Cursor;
const UNITYFS_COMPRESSION_MASK: u32 = 0x3f; const UNITYFS_COMPRESSION_MASK: u32 = 0x3f;
@@ -113,9 +114,9 @@ fn parse_unityfs(data: &[u8]) -> Result<UnityFsBundle> {
flags: reader.read_u32("flags")?, 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!( return Err(AssetBundleError::Parse(format!(
"UnityFS total_size {} exceeds file size {}", "UnityFS total_size {} does not match file size {}",
header.total_size, header.total_size,
data.len() data.len()
))); )));
@@ -446,8 +447,14 @@ fn parse_blocks_info(
"invalid UnityFS block count: {block_count}" "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 { for _ in 0..block_count {
let uncompressed_size = reader.read_u32("block_uncompressed_size")?; let uncompressed_size = reader.read_u32("block_uncompressed_size")?;
let compressed_size = reader.read_u32("block_compressed_size")?; let compressed_size = reader.read_u32("block_compressed_size")?;
@@ -466,8 +473,14 @@ fn parse_blocks_info(
"invalid UnityFS directory count: {directory_count}" "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 { for _ in 0..directory_count {
directories.push(UnityFsDirectoryInfo { directories.push(UnityFsDirectoryInfo {
offset: reader.read_u64("directory_offset")?, offset: reader.read_u64("directory_offset")?,
@@ -546,7 +559,15 @@ fn validate_directory_bounds(
data_region_size: u64, data_region_size: u64,
directories: &[UnityFsDirectoryInfo], directories: &[UnityFsDirectoryInfo],
) -> Result<()> { ) -> Result<()> {
let mut paths = HashSet::with_capacity(directories.len());
for (index, directory) in directories.iter().enumerate() { 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 let end = directory
.offset .offset
.checked_add(directory.size) .checked_add(directory.size)
@@ -567,6 +588,45 @@ fn validate_directory_bounds(
Ok(()) 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> { struct UnityFsReader<'a> {
data: &'a [u8], data: &'a [u8],
offset: usize, offset: usize,
@@ -972,6 +1032,21 @@ mod tests {
assert_eq!(parsed.files[0].data, payload); 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] #[test]
fn extracts_data_block_after_block_info_alignment_padding() { fn extracts_data_block_after_block_info_alignment_padding() {
let parser = UnityFsParser::new(); let parser = UnityFsParser::new();
@@ -1002,6 +1077,62 @@ mod tests {
assert!(error.contains("uncompressed data region size 4"), "{error}"); 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] #[test]
fn rejects_truncated_header_with_field_context() { fn rejects_truncated_header_with_field_context() {
let parser = UnityFsParser::new(); let parser = UnityFsParser::new();
@@ -1023,4 +1154,8 @@ mod tests {
"{error}" "{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 校验边界 | 已用于下载计划,仍需沉淀更多结构化字段 | | 官方 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 当前目标字段已覆盖;未知结构返回明确错误 | | 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 | | 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 提取和字符串提取已落地,真实结构覆盖继续扩大 | | 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 未开放 | | Patch 发布前解析 | 已翻译文本、中间格式、原版资源 | 可验证 patch manifest、汉化 release 目录 | UnityFS TextAsset 前置已落地,通用 Binary/JSON/Text Patch 与文件级 patch / UnityFS 写入入口可用,发布级 build/rollback 未开放 |
@@ -40,16 +40,17 @@
`crates/bat-assetbundle` 已经承担解析核心: `crates/bat-assetbundle` 已经承担解析核心:
1. `UnityFsParser` 解析 UnityFS header、block info、directory。 1. `UnityFsParser` 解析 UnityFS header、block info、directory,并校验声明总大小与实际文件大小
2. 支持 LZ4/LZMA block info 和数据 block 解压。 2. 支持 LZ4/LZMA block info 和数据 block 解压。
3. 支持 block info at end 和官方样本中出现的 block data alignment。 3. 支持 block info at end 和官方样本中出现的 block data alignment。
4. 能从 UnityFS directory 提取文件 bytes。 4. 能从 UnityFS directory 提取文件 bytes,并拒绝越界、重复或不安全路径
5. `serialized` 模块能读取 Unity serialized file header、type table、TypeTree node 元数据、object table 5. 对 block/directory 计数先按剩余字节做有界检查,避免损坏输入触发超大内存分配
6. 能提取 TextAsset 的 name 和原始 bytes 6. `serialized` 模块能读取 Unity serialized file header、type table、TypeTree node 元数据、object table
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 诊断 7. 能提取 TextAsset 的 name 和原始 bytes
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 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. `ResourceImportService` 能把 AssetBundle 摘要、TextAsset/Table/Media 分类和 TextUnit 摘要写入导入报告 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. 官方同步后 `OfficialParseCacheService` 能从 `official-download-manifest.json` 遍历所有资源,解析直接 bundle 和 zip 内条目,非候选资源记录为 unsupported,并缓存 TextUnit 数量/格式/诊断摘要,同时写出 `official-textunit-index.json``parse.text_units` / `parse.errors` 查询 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 引擎解析器仍未完成 ### G-005AssetBundle 引擎解析器仍未完成
状态:**部分完成** 状态:**UnityFS 基础容器校验目标完成,复杂对象能力仍部分完成**
冻结状态:自 2026-07-30 起,G-005 不再作为默认推进项。解析层只接受维护冻结规则允许的稳定性修复、诊断修复、真实回归修复和文档校正;新增 TypeTree 语义类型、扩大解析覆盖和新增写入型解析入口全部暂停。冻结细则见 `docs/reports/PARSER_FREEZE.md` 冻结状态:自 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。 - `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。 - `adapters/src/unity/unity_2021_3.rs` 已降为 Unity 版本选择薄层,复用 `bat-assetbundle`,不再维护第二套 UnityFS parser。
- `ResourceImportService` 的 UnityFS 摘要已经能暴露解包文件数、serialized file 数、TextAsset 名称、TextUnit 数量/格式和字段诊断。 - `ResourceImportService` 的 UnityFS 摘要已经能暴露解包文件数、serialized file 数、TextAsset 名称、TextUnit 数量/格式和字段诊断。
@@ -132,8 +132,8 @@ issue 43 例外说明:新增的 `parse repack` 只编排已有 TextAsset、Typ
当前验收证据: 当前验收证据:
- `crates/bat-assetbundle` 能解析结构化测试样本和隔离真实样本。 - `crates/bat-assetbundle` 能解析结构化测试样本和隔离真实样本;新增 UnityPy `char_118_yuki.ab` 真实 bundle 回归入口,样本只下载到 `/tmp`,不入库
- 支持 UnityFS header、blocks、directory、metadata 摘要、directory 文件提取。 - 支持 UnityFS header、blocks、directory、metadata 摘要、directory 文件提取,并拒绝声明总大小不符、计数越界、重复/不安全路径和目录数据越界
- 支持 Unity serialized file object table、TypeTree node 元数据和 TextAsset 提取的合成 fixture。 - 支持 Unity serialized file object table、TypeTree node 元数据和 TextAsset 提取的合成 fixture。
- 错误包含偏移和字段上下文。 - 错误包含偏移和字段上下文。