feat: prepare experiment push package

This commit is contained in:
2026-06-30 00:08:36 +08:00
parent 3c00659691
commit adc1cd5b91
45 changed files with 7858 additions and 65 deletions
+68 -2
View File
@@ -44,6 +44,72 @@ pub struct ParsedAssetBundle {
pub assets: Vec<String>,
/// 原始数据(保留用于序列化)
pub raw_data: Vec<u8>,
/// UnityFS 文件头信息。
pub unityfs_header: Option<UnityFsHeader>,
/// UnityFS 压缩块信息。
pub blocks: Vec<UnityFsBlockInfo>,
/// UnityFS 目录信息。
pub directories: Vec<UnityFsDirectoryInfo>,
}
/// UnityFS 文件头。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnityFsHeader {
/// UnityFS 格式版本。
pub format_version: u32,
/// Bundle 目标版本字符串,例如 `5.x.x`。
pub target_version: String,
/// Unity 编辑器版本字符串。
pub unity_version: String,
/// 文件总大小。
pub total_size: u64,
/// 压缩后的 block info 大小。
pub compressed_blocks_info_size: u32,
/// 解压后的 block info 大小。
pub uncompressed_blocks_info_size: u32,
/// UnityFS flags 原始值。
pub flags: u32,
}
/// UnityFS 块压缩类型。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnityFsCompression {
/// 未压缩。
None,
/// LZMA 压缩。
Lzma,
/// LZ4 压缩。
Lz4,
/// LZ4HC 压缩。
Lz4Hc,
/// 当前版本未识别的压缩类型。
Unknown(u16),
}
/// UnityFS 压缩块信息。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnityFsBlockInfo {
/// 解压后大小。
pub uncompressed_size: u32,
/// 压缩后大小。
pub compressed_size: u32,
/// 块 flags 原始值。
pub flags: u16,
/// 解析出的压缩类型。
pub compression: UnityFsCompression,
}
/// UnityFS 目录条目。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnityFsDirectoryInfo {
/// 条目在数据区中的偏移。
pub offset: u64,
/// 条目大小。
pub size: u64,
/// 条目 flags 原始值。
pub flags: u32,
/// 条目路径。
pub path: String,
}
/// Unity Adapter 接口
@@ -113,10 +179,10 @@ mod tests {
fn test_raw_assetbundle() {
let bundle = RawAssetBundle {
data: vec![0x55, 0x6e, 0x69, 0x74, 0x79, 0x46, 0x53], // "UnityFS"
path: Some("test.bundle".to_string()),
path: Some("synthetic-minimal.bundle".to_string()),
};
assert_eq!(bundle.data.len(), 7);
assert_eq!(bundle.path, Some("test.bundle".to_string()));
assert_eq!(bundle.path, Some("synthetic-minimal.bundle".to_string()));
}
}
+16 -1
View File
@@ -18,6 +18,13 @@ impl UnityAdapterRegistry {
}
}
/// 创建带有默认 Unity 适配器的注册表。
pub fn with_defaults() -> Self {
let mut registry = Self::new();
registry.register(Arc::new(crate::unity::Unity2021_3Adapter::new()));
registry
}
/// 注册一个适配器
pub fn register(&mut self, adapter: Arc<dyn UnityAdapter>) {
self.adapters.push(adapter);
@@ -91,7 +98,7 @@ mod tests {
let bundle = RawAssetBundle {
data,
path: Some("test.bundle".to_string()),
path: Some("synthetic-minimal.bundle".to_string()),
};
let result = registry.select_adapter(&bundle);
@@ -111,4 +118,12 @@ mod tests {
let result = registry.select_adapter(&bundle);
assert!(result.is_err());
}
#[test]
fn test_with_defaults_registers_unity_2021_3_adapter() {
let registry = UnityAdapterRegistry::with_defaults();
assert_eq!(registry.count(), 1);
assert_eq!(registry.all_adapters()[0].name(), "Unity-2021.3");
}
}
+532
View File
@@ -0,0 +1,532 @@
//! Unity serialized file reader.
//!
//! This module is intentionally narrow: it extracts `TextAsset` payloads from
//! Unity serialized files such as `resources.assets` and
//! `globalgamemanagers.assets`.
use std::fs;
use std::path::Path;
/// One extracted Unity `TextAsset`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitySerializedTextAsset {
/// Unity path ID of the object.
pub path_id: i64,
/// Asset name stored in the serialized object.
pub name: String,
/// Raw bytes stored by the `TextAsset`.
pub bytes: Vec<u8>,
}
/// Parsed Unity serialized file summary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitySerializedFile {
/// Serialized file format version.
pub version: u32,
/// Unity editor version stored in the file.
pub unity_version: String,
/// Target platform value from the file header.
pub platform: i32,
text_assets: Vec<UnitySerializedTextAsset>,
}
impl UnitySerializedFile {
/// Parses a serialized file from raw bytes.
pub fn from_slice(data: &[u8]) -> Result<Self, String> {
let mut reader = Reader::new(data);
let _metadata_size = reader.read_u32_be("metadata_size")?;
let _file_size = reader.read_u32_be("file_size")?;
let version = reader.read_u32_be("version")?;
let _data_offset = reader.read_u32_be("data_offset")?;
let endian_flag = reader.read_u8("endian_flag")?;
reader.read_bytes(3, "reserved")?;
let (metadata_size, file_size, data_offset) = if version >= 22 {
let metadata_size = reader.read_u32_be("metadata_size_2")?;
let file_size = reader.read_u64_be("file_size_2")?;
let data_offset = reader.read_u64_be("data_offset_2")? as usize;
let _unknown = reader.read_u64_be("unknown_2")?;
(metadata_size, file_size, data_offset)
} else {
(_metadata_size, _file_size as u64, _data_offset as usize)
};
let _ = metadata_size;
let _ = file_size;
let endian = if endian_flag == 0 {
Endian::Little
} else {
Endian::Big
};
reader.set_endian(endian);
let unity_version = reader.read_c_string("unity_version")?;
let platform = reader.read_i32("platform")?;
let enable_type_tree = reader.read_u8("enable_type_tree")?;
let type_count = reader.read_i32("type_count")?;
if type_count < 0 {
return Err(format!("Invalid Unity type count: {}", type_count));
}
let mut class_ids = Vec::with_capacity(type_count as usize);
for _ in 0..type_count {
class_ids.push(read_serialized_type(
&mut reader,
version,
enable_type_tree,
)?);
}
let big_id_enabled = if version >= 11 && version < 14 {
reader.read_i32("big_id_enabled")?
} else {
0
};
let object_count = reader.read_i32("object_count")?;
if object_count < 0 {
return Err(format!("Invalid Unity object count: {}", object_count));
}
let mut text_assets = Vec::new();
for _ in 0..object_count {
if version >= 14 {
reader.align(4)?;
}
let path_id = if big_id_enabled != 0 {
reader.read_i64("path_id")?
} else if version < 14 {
reader.read_i32("path_id")? as i64
} else {
reader.read_i64("path_id")?
};
let byte_start = if version >= 22 {
reader.read_u64("byte_start")? as usize
} else {
reader.read_u32("byte_start")? as usize
};
let byte_size = reader.read_u32("byte_size")? as usize;
let type_id = reader.read_i32("type_id")?;
if version < 16 {
reader.read_u16("class_id")?;
}
if version < 11 {
reader.read_u16("is_destroyed")?;
}
if (11..17).contains(&version) {
reader.read_i16("script_type_index")?;
}
if version == 15 || version == 16 {
reader.read_u8("stripped")?;
}
let class_id = class_ids
.get(type_id as usize)
.copied()
.ok_or_else(|| format!("Invalid Unity type index: {}", type_id))?;
if class_id == 49 {
let object_start = data_offset
.checked_add(byte_start)
.ok_or_else(|| "Unity object offset overflow".to_string())?;
let object_end = object_start
.checked_add(byte_size)
.ok_or_else(|| "Unity object size overflow".to_string())?;
if object_end > data.len() {
return Err(format!(
"Unity object exceeds file size: start={}, size={}, file_size={}",
object_start,
byte_size,
data.len()
));
}
let asset = parse_text_asset(path_id, &data[object_start..object_end], endian)?;
text_assets.push(asset);
}
}
Ok(Self {
version,
unity_version,
platform,
text_assets,
})
}
/// Parses a serialized file from disk.
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, String> {
let path = path.as_ref();
let bytes = fs::read(path)
.map_err(|error| format!("Failed to read {}: {error}", path.display()))?;
Self::from_slice(&bytes)
}
/// Returns all extracted text assets.
pub fn text_assets(&self) -> &[UnitySerializedTextAsset] {
&self.text_assets
}
/// Returns one extracted text asset by name.
pub fn text_asset(&self, name: &str) -> Option<&UnitySerializedTextAsset> {
self.text_assets.iter().find(|asset| asset.name == name)
}
}
fn parse_text_asset(
path_id: i64,
data: &[u8],
endian: Endian,
) -> Result<UnitySerializedTextAsset, String> {
let mut reader = Reader::new(data);
reader.set_endian(endian);
let name = reader.read_len_prefixed_string("text_asset_name")?;
reader.align(4)?;
let bytes_len = reader.read_u32("text_asset_bytes_len")? as usize;
let bytes = reader.read_bytes(bytes_len, "text_asset_bytes")?.to_vec();
Ok(UnitySerializedTextAsset {
path_id,
name,
bytes,
})
}
fn read_serialized_type(
reader: &mut Reader<'_>,
version: u32,
enable_type_tree: u8,
) -> Result<i32, String> {
let class_id = reader.read_i32("type_class_id")?;
if version >= 16 {
reader.read_u8("type_is_stripped")?;
}
if version >= 17 {
reader.read_i16("type_script_index")?;
}
if version >= 13 {
if (version < 16 && class_id < 0) || (version >= 16 && class_id == 114) {
reader.read_bytes(16, "type_script_id")?;
}
reader.read_bytes(16, "type_hash")?;
}
if enable_type_tree != 0 {
if version >= 12 || version == 10 {
let node_count = reader.read_i32("type_tree_node_count")?;
if node_count < 0 {
return Err(format!("Invalid Unity type tree node count: {}", node_count));
}
let string_buffer_size = reader.read_i32("type_tree_string_buffer_size")?;
if string_buffer_size < 0 {
return Err(format!(
"Invalid Unity type tree string buffer size: {}",
string_buffer_size
));
}
let node_size = 2 + 1 + 1 + 4 + 4 + 4 + 4 + 4 + if version >= 19 { 8 } else { 0 };
reader.read_bytes(
node_count as usize * node_size,
"type_tree_nodes",
)?;
reader.read_bytes(string_buffer_size as usize, "type_tree_strings")?;
}
if version >= 21 {
let dependency_count = reader.read_i32("type_tree_dependency_count")?;
if dependency_count < 0 {
return Err(format!(
"Invalid Unity type tree dependency count: {}",
dependency_count
));
}
reader.read_bytes(
dependency_count as usize * 4,
"type_tree_dependencies",
)?;
}
}
Ok(class_id)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Endian {
Little,
Big,
}
struct Reader<'a> {
data: &'a [u8],
offset: usize,
endian: Endian,
}
impl<'a> Reader<'a> {
fn new(data: &'a [u8]) -> Self {
Self {
data,
offset: 0,
endian: Endian::Big,
}
}
fn set_endian(&mut self, endian: Endian) {
self.endian = endian;
}
fn read_bytes(&mut self, len: usize, field: &str) -> Result<&'a [u8], String> {
let end = self
.offset
.checked_add(len)
.ok_or_else(|| format!("{field} length overflow at offset {}", self.offset))?;
if end > self.data.len() {
return Err(format!(
"Unexpected end while reading {field} at offset {}: need {}, have {}",
self.offset,
len,
self.data.len().saturating_sub(self.offset)
));
}
let bytes = &self.data[self.offset..end];
self.offset = end;
Ok(bytes)
}
fn align(&mut self, alignment: usize) -> Result<(), String> {
if alignment == 0 {
return Ok(());
}
let remainder = self.offset % alignment;
if remainder == 0 {
return Ok(());
}
let padding = alignment - remainder;
self.read_bytes(padding, "alignment padding").map(|_| ())
}
fn read_u8(&mut self, field: &str) -> Result<u8, String> {
Ok(self.read_bytes(1, field)?[0])
}
fn read_u16(&mut self, field: &str) -> Result<u16, String> {
let bytes = self.read_bytes(2, field)?;
Ok(match self.endian {
Endian::Little => u16::from_le_bytes([bytes[0], bytes[1]]),
Endian::Big => u16::from_be_bytes([bytes[0], bytes[1]]),
})
}
fn read_i16(&mut self, field: &str) -> Result<i16, String> {
let bytes = self.read_bytes(2, field)?;
Ok(match self.endian {
Endian::Little => i16::from_le_bytes([bytes[0], bytes[1]]),
Endian::Big => i16::from_be_bytes([bytes[0], bytes[1]]),
})
}
fn read_u32(&mut self, field: &str) -> Result<u32, String> {
let bytes = self.read_bytes(4, field)?;
Ok(match self.endian {
Endian::Little => u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
Endian::Big => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
})
}
fn read_u32_be(&mut self, field: &str) -> Result<u32, String> {
let bytes = self.read_bytes(4, field)?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_i32(&mut self, field: &str) -> Result<i32, String> {
let bytes = self.read_bytes(4, field)?;
Ok(match self.endian {
Endian::Little => i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
Endian::Big => i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
})
}
fn read_u64(&mut self, field: &str) -> Result<u64, String> {
let bytes = self.read_bytes(8, field)?;
Ok(match self.endian {
Endian::Little => u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]),
Endian::Big => u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]),
})
}
fn read_u64_be(&mut self, field: &str) -> Result<u64, String> {
let bytes = self.read_bytes(8, field)?;
Ok(u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]))
}
fn read_i64(&mut self, field: &str) -> Result<i64, String> {
let bytes = self.read_bytes(8, field)?;
Ok(match self.endian {
Endian::Little => i64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]),
Endian::Big => i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]),
})
}
fn read_c_string(&mut self, field: &str) -> Result<String, String> {
let remaining = &self.data[self.offset..];
let Some(length) = remaining.iter().position(|&byte| byte == 0) else {
return Err(format!(
"Missing null terminator while reading {field} at offset {}",
self.offset
));
};
let bytes = self.read_bytes(length, field)?;
self.offset += 1;
std::str::from_utf8(bytes)
.map(ToOwned::to_owned)
.map_err(|error| format!("Invalid UTF-8 in {field}: {error}"))
}
fn read_len_prefixed_string(&mut self, field: &str) -> Result<String, String> {
let len = self.read_u32(field)? as usize;
let bytes = self.read_bytes(len, field)?;
std::str::from_utf8(bytes)
.map(ToOwned::to_owned)
.map_err(|error| format!("Invalid UTF-8 in {field}: {error}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn push_i16_le(data: &mut Vec<u8>, value: i16) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u32_le(data: &mut Vec<u8>, value: u32) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_i32_le(data: &mut Vec<u8>, value: i32) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_i64_le(data: &mut Vec<u8>, value: i64) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u64_le(data: &mut Vec<u8>, value: u64) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u32_be(data: &mut Vec<u8>, value: u32) {
data.extend_from_slice(&value.to_be_bytes());
}
fn push_u64_be(data: &mut Vec<u8>, value: u64) {
data.extend_from_slice(&value.to_be_bytes());
}
fn align(data: &mut Vec<u8>, alignment: usize) {
let remainder = data.len() % alignment;
if remainder != 0 {
data.resize(data.len() + alignment - remainder, 0);
}
}
fn synthetic_serialized_file() -> Vec<u8> {
let mut object_data = Vec::new();
push_u32_le(&mut object_data, 14);
object_data.extend_from_slice(b"GameMainConfig");
align(&mut object_data, 4);
push_u32_le(&mut object_data, 5);
object_data.extend_from_slice(b"hello");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(0);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 49);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 1);
align(&mut metadata, 4);
push_i64_le(&mut metadata, 1);
push_u64_le(&mut metadata, 0);
push_u32_le(&mut metadata, object_data.len() as u32);
push_i32_le(&mut metadata, 0);
let header_len = 48usize;
let data_offset = header_len + metadata.len();
let file_size = data_offset + object_data.len();
let mut file = Vec::new();
push_u32_be(&mut file, metadata.len() as u32);
push_u32_be(&mut file, file_size as u32);
push_u32_be(&mut file, 22);
push_u32_be(&mut file, 0);
file.push(0);
file.extend_from_slice(&[0, 0, 0]);
push_u32_be(&mut file, metadata.len() as u32);
push_u64_be(&mut file, file_size as u64);
push_u64_be(&mut file, data_offset as u64);
push_u64_be(&mut file, 0);
file.extend_from_slice(&metadata);
file.extend_from_slice(&object_data);
file
}
#[test]
fn parses_synthetic_text_asset() {
let file = synthetic_serialized_file();
let parsed = UnitySerializedFile::from_slice(&file).unwrap();
assert_eq!(parsed.version, 22);
assert_eq!(parsed.unity_version, "2021.3.56f2");
assert_eq!(parsed.platform, 19);
assert_eq!(parsed.text_assets.len(), 1);
let asset = parsed.text_asset("GameMainConfig").unwrap();
assert_eq!(asset.name, "GameMainConfig");
assert_eq!(asset.bytes, b"hello");
}
#[test]
fn reads_text_asset_from_real_resource_file_when_available() {
let path = Path::new(
"/home/wanye/D/BlueArchive/AllResources/YostarGames/BlueArchive_JP/BlueArchive_Data/resources.assets",
);
if !path.exists() {
return;
}
let parsed = UnitySerializedFile::from_path(path).unwrap();
let asset = parsed.text_asset("GameMainConfig").unwrap();
assert_eq!(asset.name, "GameMainConfig");
assert_eq!(asset.bytes.len(), 896);
assert_eq!(
&asset.bytes[..32],
&[
0x60, 0x23, 0x0a, 0x06, 0x95, 0x72, 0x83, 0x57, 0x54, 0x23, 0x49, 0x06,
0xfc, 0x72, 0xb4, 0x57, 0x57, 0x23, 0x6b, 0x06, 0x98, 0x72, 0xb5, 0x57,
0x71, 0x23, 0x1b, 0x06, 0xec, 0x72, 0xf6, 0x57,
],
);
}
}
+363 -20
View File
@@ -2,11 +2,17 @@
//!
//! 支持 Unity 2021.3.x 版本的 AssetBundle
use super::adapter::{ParsedAssetBundle, RawAssetBundle, UnityAdapter, VersionRange};
use super::adapter::{
ParsedAssetBundle, RawAssetBundle, UnityAdapter, UnityFsBlockInfo, UnityFsCompression,
UnityFsDirectoryInfo, UnityFsHeader, VersionRange,
};
use async_trait::async_trait;
use std::io::Cursor;
const PARSE_NOT_IMPLEMENTED: &str = "parse() 将在 Phase 2 实现";
const SERIALIZE_NOT_IMPLEMENTED: &str = "serialize() 将在 Phase 2 实现";
const UNITYFS_COMPRESSION_MASK: u32 = 0x3f;
const UNITYFS_BLOCK_INFO_AT_END_FLAG: u32 = 0x80;
const UNITYFS_ALIGNMENT: usize = 16;
/// Unity 2021.3 Adapter
pub struct Unity2021_3Adapter;
@@ -42,6 +48,267 @@ impl Unity2021_3Adapter {
.map(ToOwned::to_owned)
.ok()
}
fn parse_unityfs(data: &[u8]) -> Result<ParsedAssetBundle, String> {
let mut reader = UnityFsReader::new(data);
let signature = reader.read_c_string("signature")?;
if signature != "UnityFS" {
return Err(format!("Unsupported AssetBundle signature: {}", signature));
}
let format_version = reader.read_u32("format_version")?;
let target_version = reader.read_c_string("target_version")?;
let unity_version = reader.read_c_string("unity_version")?;
let total_size = reader.read_u64("total_size")?;
let compressed_blocks_info_size = reader.read_u32("compressed_blocks_info_size")?;
let uncompressed_blocks_info_size = reader.read_u32("uncompressed_blocks_info_size")?;
let flags = reader.read_u32("flags")?;
let header = UnityFsHeader {
format_version,
target_version,
unity_version,
total_size,
compressed_blocks_info_size,
uncompressed_blocks_info_size,
flags,
};
if format_version >= 7 {
reader.align(UNITYFS_ALIGNMENT)?;
}
let blocks_info_bytes = read_blocks_info_bytes(data, &mut reader, &header)?;
let block_info = decompress_blocks_info(
blocks_info_bytes,
compressed_blocks_info_size,
uncompressed_blocks_info_size,
flags,
)?;
let (blocks, directories) = Self::parse_blocks_info(&block_info)?;
Ok(ParsedAssetBundle {
unity_version: header.unity_version.clone(),
assets: directories
.iter()
.map(|directory| directory.path.clone())
.collect(),
raw_data: data.to_vec(),
unityfs_header: Some(header),
blocks,
directories,
})
}
fn parse_blocks_info(
data: &[u8],
) -> Result<(Vec<UnityFsBlockInfo>, Vec<UnityFsDirectoryInfo>), String> {
let mut reader = UnityFsReader::new(data);
let _hash = reader.read_bytes(16, "blocks_info_hash")?;
let block_count = reader.read_i32("block_count")?;
if block_count < 0 {
return Err(format!("Invalid UnityFS block count: {}", block_count));
}
let mut blocks = Vec::with_capacity(block_count as usize);
for _ in 0..block_count {
let uncompressed_size = reader.read_u32("block_uncompressed_size")?;
let compressed_size = reader.read_u32("block_compressed_size")?;
let flags = reader.read_u16("block_flags")?;
blocks.push(UnityFsBlockInfo {
uncompressed_size,
compressed_size,
flags,
compression: compression_from_flags(flags),
});
}
let directory_count = reader.read_i32("directory_count")?;
if directory_count < 0 {
return Err(format!(
"Invalid UnityFS directory count: {}",
directory_count
));
}
let mut directories = Vec::with_capacity(directory_count as usize);
for _ in 0..directory_count {
directories.push(UnityFsDirectoryInfo {
offset: reader.read_u64("directory_offset")?,
size: reader.read_u64("directory_size")?,
flags: reader.read_u32("directory_flags")?,
path: reader.read_c_string("directory_path")?,
});
}
Ok((blocks, directories))
}
}
fn read_blocks_info_bytes<'a>(
data: &'a [u8],
reader: &mut UnityFsReader<'a>,
header: &UnityFsHeader,
) -> Result<&'a [u8], String> {
let len = header.compressed_blocks_info_size as usize;
if blocks_info_at_end(header.flags) {
let start = data.len().checked_sub(len).ok_or_else(|| {
format!(
"UnityFS block info at end underflow: compressed size {}, file size {}",
len,
data.len()
)
})?;
return Ok(&data[start..]);
}
reader.read_bytes(len, "blocks_info")
}
fn blocks_info_at_end(flags: u32) -> bool {
flags & UNITYFS_BLOCK_INFO_AT_END_FLAG != 0
}
fn decompress_blocks_info(
data: &[u8],
compressed_size: u32,
uncompressed_size: u32,
flags: u32,
) -> Result<Vec<u8>, String> {
if data.len() != compressed_size as usize {
return Err(format!(
"UnityFS block info size mismatch: header says {}, read {}",
compressed_size,
data.len()
));
}
let compression = compression_from_flags((flags & UNITYFS_COMPRESSION_MASK) as u16);
match compression {
UnityFsCompression::None => {
if compressed_size != uncompressed_size {
return Err(format!(
"Uncompressed UnityFS block info size mismatch: compressed {} != uncompressed {}",
compressed_size, uncompressed_size
));
}
Ok(data.to_vec())
}
UnityFsCompression::Lz4 | UnityFsCompression::Lz4Hc => {
lz4::block::decompress(data, Some(uncompressed_size as i32))
.map_err(|error| format!("Failed to decompress UnityFS LZ4 block info: {}", error))
}
UnityFsCompression::Lzma => {
let mut output = Vec::with_capacity(uncompressed_size as usize);
lzma_rs::lzma_decompress(&mut Cursor::new(data), &mut output).map_err(|error| {
format!("Failed to decompress UnityFS LZMA block info: {}", error)
})?;
if output.len() != uncompressed_size as usize {
return Err(format!(
"UnityFS LZMA block info size mismatch: expected {}, got {}",
uncompressed_size,
output.len()
));
}
Ok(output)
}
UnityFsCompression::Unknown(value) => Err(format!(
"Unsupported UnityFS block info compression flag: {}",
value
)),
}
}
fn compression_from_flags(flags: u16) -> UnityFsCompression {
match flags & UNITYFS_COMPRESSION_MASK as u16 {
0 => UnityFsCompression::None,
1 => UnityFsCompression::Lzma,
2 => UnityFsCompression::Lz4,
3 | 4 => UnityFsCompression::Lz4Hc,
value => UnityFsCompression::Unknown(value),
}
}
struct UnityFsReader<'a> {
data: &'a [u8],
offset: usize,
}
impl<'a> UnityFsReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, offset: 0 }
}
fn read_bytes(&mut self, len: usize, field: &str) -> Result<&'a [u8], String> {
let end = self
.offset
.checked_add(len)
.ok_or_else(|| format!("{} length overflow at offset {}", field, self.offset))?;
if end > self.data.len() {
return Err(format!(
"Unexpected end while reading {} at offset {}: need {}, have {}",
field,
self.offset,
len,
self.data.len().saturating_sub(self.offset)
));
}
let bytes = &self.data[self.offset..end];
self.offset = end;
Ok(bytes)
}
fn align(&mut self, alignment: usize) -> Result<(), String> {
if alignment == 0 {
return Ok(());
}
let remainder = self.offset % alignment;
if remainder == 0 {
return Ok(());
}
let padding = alignment - remainder;
self.read_bytes(padding, "alignment padding").map(|_| ())
}
fn read_u16(&mut self, field: &str) -> Result<u16, String> {
let bytes = self.read_bytes(2, field)?;
Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
}
fn read_u32(&mut self, field: &str) -> Result<u32, String> {
let bytes = self.read_bytes(4, field)?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_i32(&mut self, field: &str) -> Result<i32, String> {
let bytes = self.read_bytes(4, field)?;
Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_u64(&mut self, field: &str) -> Result<u64, String> {
let bytes = self.read_bytes(8, field)?;
Ok(u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]))
}
fn read_c_string(&mut self, field: &str) -> Result<String, String> {
let remaining = &self.data[self.offset..];
let Some(length) = remaining.iter().position(|&byte| byte == 0) else {
return Err(format!(
"Missing null terminator while reading {} at offset {}",
field, self.offset
));
};
let bytes = self.read_bytes(length, field)?;
self.offset += 1;
std::str::from_utf8(bytes)
.map(ToOwned::to_owned)
.map_err(|error| format!("Invalid UTF-8 in {}: {}", field, error))
}
}
impl Default for Unity2021_3Adapter {
@@ -69,14 +336,16 @@ impl UnityAdapter for Unity2021_3Adapter {
}
}
async fn parse(&self, _bundle: &RawAssetBundle) -> Result<ParsedAssetBundle, String> {
// TODO: Phase 2 实现
// 需要:
// 1. 解析 UnityFS 文件头
// 2. 解压缩数据块
// 3. 解析 TypeTree
// 4. 提取 Asset 对象
Err(PARSE_NOT_IMPLEMENTED.to_string())
async fn parse(&self, bundle: &RawAssetBundle) -> Result<ParsedAssetBundle, String> {
let parsed = Self::parse_unityfs(&bundle.data)?;
if !self.supported_versions().contains(&parsed.unity_version) {
return Err(format!(
"Unsupported Unity version for {}: {}",
self.name(),
parsed.unity_version
));
}
Ok(parsed)
}
async fn serialize(&self, _parsed: &ParsedAssetBundle) -> Result<Vec<u8>, String> {
@@ -94,6 +363,66 @@ impl UnityAdapter for Unity2021_3Adapter {
mod tests {
use super::*;
fn push_c_string(data: &mut Vec<u8>, value: &str) {
data.extend_from_slice(value.as_bytes());
data.push(0);
}
fn push_u16(data: &mut Vec<u8>, value: u16) {
data.extend_from_slice(&value.to_be_bytes());
}
fn push_u32(data: &mut Vec<u8>, value: u32) {
data.extend_from_slice(&value.to_be_bytes());
}
fn push_i32(data: &mut Vec<u8>, value: i32) {
data.extend_from_slice(&value.to_be_bytes());
}
fn push_u64(data: &mut Vec<u8>, value: u64) {
data.extend_from_slice(&value.to_be_bytes());
}
fn align(data: &mut Vec<u8>, alignment: usize) {
let remainder = data.len() % alignment;
if remainder != 0 {
data.resize(data.len() + alignment - remainder, 0);
}
}
fn synthetic_minimal_unityfs_bundle() -> Vec<u8> {
let mut blocks_info = Vec::new();
blocks_info.extend_from_slice(&[0; 16]);
push_i32(&mut blocks_info, 1);
push_u32(&mut blocks_info, 4);
push_u32(&mut blocks_info, 4);
push_u16(&mut blocks_info, 0);
push_i32(&mut blocks_info, 1);
push_u64(&mut blocks_info, 0);
push_u64(&mut blocks_info, 4);
push_u32(&mut blocks_info, 0);
push_c_string(&mut blocks_info, "CAB-test");
let mut data = Vec::new();
push_c_string(&mut data, "UnityFS");
push_u32(&mut data, 8);
push_c_string(&mut data, "5.x.x");
push_c_string(&mut data, "2021.3.56f2");
push_u64(&mut data, 0);
push_u32(&mut data, blocks_info.len() as u32);
push_u32(&mut data, blocks_info.len() as u32);
push_u32(&mut data, 0);
align(&mut data, UNITYFS_ALIGNMENT);
data.extend_from_slice(&blocks_info);
data.extend_from_slice(b"data");
let total_size = data.len() as u64;
let total_size_offset = b"UnityFS\0".len() + 4 + b"5.x.x\0".len() + b"2021.3.56f2\0".len();
data[total_size_offset..total_size_offset + 8].copy_from_slice(&total_size.to_be_bytes());
data
}
#[test]
fn test_adapter_name() {
let adapter = Unity2021_3Adapter::new();
@@ -114,15 +443,9 @@ mod tests {
fn test_can_handle_valid_bundle() {
let adapter = Unity2021_3Adapter::new();
// 模拟 UnityFS 文件头
let mut data = Vec::new();
data.extend_from_slice(b"UnityFS\0");
data.extend_from_slice(&[0, 0, 0, 8]); // format version
data.extend_from_slice(b"5.x.x\x002021.3.56f2\0");
let bundle = RawAssetBundle {
data,
path: Some("test.bundle".to_string()),
data: synthetic_minimal_unityfs_bundle(),
path: Some("synthetic-minimal.bundle".to_string()),
};
assert!(adapter.can_handle(&bundle));
@@ -141,7 +464,27 @@ mod tests {
}
#[tokio::test]
async fn test_parse_not_implemented() {
async fn test_parse_unityfs_metadata() {
let adapter = Unity2021_3Adapter::new();
let bundle = RawAssetBundle {
data: synthetic_minimal_unityfs_bundle(),
path: Some("synthetic-minimal.bundle".to_string()),
};
let parsed = adapter.parse(&bundle).await.unwrap();
let header = parsed.unityfs_header.unwrap();
assert_eq!(parsed.unity_version, "2021.3.56f2");
assert_eq!(header.format_version, 8);
assert_eq!(parsed.blocks.len(), 1);
assert_eq!(parsed.blocks[0].compression, UnityFsCompression::None);
assert_eq!(parsed.directories.len(), 1);
assert_eq!(parsed.directories[0].path, "CAB-test");
assert_eq!(parsed.assets, vec!["CAB-test".to_string()]);
}
#[tokio::test]
async fn test_parse_rejects_invalid_bundle() {
let adapter = Unity2021_3Adapter::new();
let bundle = RawAssetBundle {
@@ -151,6 +494,6 @@ mod tests {
let result = adapter.parse(&bundle).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Phase 2"));
assert!(result.unwrap_err().contains("signature"));
}
}