mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 05:34:55 +08:00
fix(unityfs): 加固基础容器解析校验
补充 UnityFS 总大小、block/directory 计数、目录路径、重复项和边界校验,覆盖 LZMA 数据块与损坏输入回归。新增隔离真实 UnityPy bundle 测试入口,并用 /tmp 中的 char_118_yuki.ab 完成实际回归。复杂对象解析与发布级重打包继续保留在 G-005 后续范围。 Fixes #3
This commit is contained in:
+2
-2
@@ -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 循环互斥;任务历史持久化于 `<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,不走 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 发布前置已具备回归测试。
|
||||
|
||||
|
||||
+2
-2
@@ -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. 继续 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` 诊断入口和面向大索引的查询优化。
|
||||
|
||||
|
||||
@@ -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>()
|
||||
);
|
||||
}
|
||||
@@ -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/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<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/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` 查询。
|
||||
|
||||
当前还不能宣称完整:
|
||||
|
||||
|
||||
@@ -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。
|
||||
- 错误包含偏移和字段上下文。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user