feat(assetbundle): 完善官方资源解析与双目录发布

This commit is contained in:
2026-07-25 20:51:01 +08:00
parent 102b49b666
commit 3e9bb20d79
37 changed files with 4663 additions and 1333 deletions
+2 -3
View File
@@ -11,9 +11,8 @@ thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
# 注意:byteorder、lz4、lzma-rs 等 UnityFS 解析/解压依赖待解析器真正实现时
# 再按需引入,避免占位阶段白增编译负担。
lz4 = "1.28"
lzma-rs = "0.3"
[dev-dependencies]
hex = "0.4"
+33 -7
View File
@@ -1,26 +1,52 @@
//! AssetBundle 错误类型定义
//! AssetBundle error types.
use thiserror::Error;
/// AssetBundle 错误类型
/// AssetBundle parser error.
#[derive(Error, Debug)]
pub enum AssetBundleError {
/// IO 错误
/// I/O error.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// 解析错误
/// Parser reached malformed data while reading a named field.
#[error("Parse error at offset {offset} while reading {field}: {message}")]
ParseField {
/// Field or structure name being read.
field: String,
/// Byte offset where parsing failed.
offset: usize,
/// Human-readable diagnostic.
message: String,
},
/// General parser error.
#[error("Parse error: {0}")]
Parse(String),
/// 不支持的格式
/// Unsupported format or compression mode.
#[error("Unsupported format: {0}")]
UnsupportedFormat(String),
/// 其他错误
/// Other error.
#[error(transparent)]
Other(#[from] anyhow::Error),
}
/// AssetBundle Result 类型
impl AssetBundleError {
/// Creates a field-scoped parser error with byte offset context.
pub fn parse_field(
field: impl Into<String>,
offset: usize,
message: impl Into<String>,
) -> Self {
Self::ParseField {
field: field.into(),
offset,
message: message.into(),
}
}
}
/// AssetBundle result type.
pub type Result<T> = std::result::Result<T, AssetBundleError>;
+11
View File
@@ -9,9 +9,20 @@
pub mod error;
pub mod parser;
pub mod serialized;
pub mod types;
pub use error::{AssetBundleError, Result};
pub use parser::{compression_from_flags, Parser, UnityFsParser};
pub use serialized::{
UnitySerializedFile, UnitySerializedObject, UnitySerializedTextAsset, UnitySerializedType,
UnityTypeTreeNode,
};
pub use types::{
AssetType, ParsedAssetBundle, RawAssetBundle, UnityFsBlockInfo, UnityFsBundle,
UnityFsCompression, UnityFsDirectoryInfo, UnityFsFile, UnityFsHeader,
UnitySerializedParseError,
};
/// AssetBundle 解析器版本号
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
File diff suppressed because it is too large Load Diff
+878
View File
@@ -0,0 +1,878 @@
//! Unity serialized file parser.
//!
//! The parser is intentionally data-oriented: it reads the serialized file
//! header, type metadata, object table and `TextAsset` payloads. Field-level
//! deserialization for `MonoBehaviour` and `ScriptableObject` builds on the
//! type tree structures exposed here.
use crate::error::{AssetBundleError, Result};
use std::fs;
use std::path::Path;
/// One extracted Unity `TextAsset`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitySerializedTextAsset {
/// UnityFS directory path or standalone serialized file path.
pub source_path: Option<String>,
/// 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 {
/// UnityFS directory path or standalone path, when known.
pub source_path: Option<String>,
/// 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,
/// Type metadata entries declared by the file.
pub types: Vec<UnitySerializedType>,
/// Object table entries declared by the file.
pub objects: Vec<UnitySerializedObject>,
text_assets: Vec<UnitySerializedTextAsset>,
}
impl UnitySerializedFile {
/// Parses a serialized file from raw bytes.
pub fn from_slice(data: &[u8]) -> Result<Self> {
Self::from_named_slice(None::<String>, data)
}
/// Parses a serialized file from raw bytes with a source path.
pub fn from_named_slice(path: impl Into<Option<String>>, data: &[u8]) -> Result<Self> {
let source_path = path.into();
let mut reader = Reader::new(data);
let metadata_size_legacy = reader.read_u32_be("metadata_size")?;
let file_size_legacy = reader.read_u32_be("file_size")?;
let version = reader.read_u32_be("version")?;
let data_offset_legacy = 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")?;
let _unknown = reader.read_u64_be("unknown_2")?;
(u64::from(metadata_size), file_size, data_offset)
} else {
(
u64::from(metadata_size_legacy),
u64::from(file_size_legacy),
u64::from(data_offset_legacy),
)
};
if file_size > data.len() as u64 {
return Err(AssetBundleError::Parse(format!(
"Unity serialized file_size {} exceeds available bytes {}",
file_size,
data.len()
)));
}
if data_offset > data.len() as u64 {
return Err(AssetBundleError::Parse(format!(
"Unity serialized data_offset {} exceeds available bytes {}",
data_offset,
data.len()
)));
}
let metadata_end = reader
.offset()
.checked_add(metadata_size as usize)
.ok_or_else(|| {
AssetBundleError::Parse("Unity serialized metadata end overflow".to_string())
})?;
if metadata_end > data.len() {
return Err(AssetBundleError::Parse(format!(
"Unity serialized metadata exceeds file: end {}, file {}",
metadata_end,
data.len()
)));
}
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(AssetBundleError::Parse(format!(
"Invalid Unity type count: {type_count}"
)));
}
let mut types = Vec::with_capacity(type_count as usize);
for index in 0..type_count {
types.push(read_serialized_type(
&mut reader,
version,
enable_type_tree,
index as usize,
)?);
}
let big_id_enabled = if (11..14).contains(&version) {
reader.read_i32("big_id_enabled")?
} else {
0
};
let object_count = reader.read_i32("object_count")?;
if object_count < 0 {
return Err(AssetBundleError::Parse(format!(
"Invalid Unity object count: {object_count}"
)));
}
let mut objects = Vec::with_capacity(object_count as usize);
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 {
i64::from(reader.read_i32("path_id")?)
} else {
reader.read_i64("path_id")?
};
let byte_start = if version >= 22 {
reader.read_u64("byte_start")?
} else {
u64::from(reader.read_u32("byte_start")?)
};
let byte_size = reader.read_u32("byte_size")?;
let type_index_raw = reader.read_i32("type_id")?;
if type_index_raw < 0 {
return Err(AssetBundleError::Parse(format!(
"Invalid Unity type index: {type_index_raw}"
)));
}
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 type_index = type_index_raw as usize;
let class_id = types.get(type_index).map(|ty| ty.class_id).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Invalid Unity type index: {type_index_raw}, type_count {}",
types.len()
))
})?;
let object_start = data_offset.checked_add(byte_start).ok_or_else(|| {
AssetBundleError::Parse("Unity object offset overflow".to_string())
})?;
let object_end = object_start
.checked_add(u64::from(byte_size))
.ok_or_else(|| AssetBundleError::Parse("Unity object size overflow".to_string()))?;
if object_end > data.len() as u64 {
return Err(AssetBundleError::Parse(format!(
"Unity object exceeds file size: start={}, size={}, file_size={}",
object_start,
byte_size,
data.len()
)));
}
objects.push(UnitySerializedObject {
path_id,
byte_start,
byte_size,
type_index,
class_id,
});
if class_id == 49 {
let mut asset = parse_text_asset(
path_id,
&data[object_start as usize..object_end as usize],
endian,
)?;
asset.source_path.clone_from(&source_path);
text_assets.push(asset);
}
}
Ok(Self {
source_path,
version,
unity_version,
platform,
types,
objects,
text_assets,
})
}
/// Parses a serialized file from disk.
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let bytes = fs::read(path).map_err(AssetBundleError::Io)?;
Self::from_named_slice(Some(path.display().to_string()), &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)
}
}
/// Type metadata entry from a Unity serialized file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitySerializedType {
/// Index in the file type table.
pub index: usize,
/// Unity class ID, for example `49` for `TextAsset`.
pub class_id: i32,
/// Whether this type is stripped.
pub is_stripped_type: bool,
/// Script type index, when available.
pub script_type_index: Option<i16>,
/// Script hash bytes for MonoBehaviour-like types.
pub script_id: Option<[u8; 16]>,
/// Type hash bytes.
pub old_type_hash: Option<[u8; 16]>,
/// TypeTree nodes, if the file embeds a type tree.
pub type_tree: Vec<UnityTypeTreeNode>,
}
/// One TypeTree node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnityTypeTreeNode {
/// TypeTree format version for the node.
pub version: u16,
/// Nesting depth.
pub level: u8,
/// Whether this field is an array.
pub is_array: bool,
/// Field type name.
pub type_name: String,
/// Field name.
pub name: String,
/// Declared byte size.
pub byte_size: i32,
/// Node index.
pub index: i32,
/// Unity metadata flags.
pub meta_flag: i32,
/// Referenced type hash for newer Unity versions.
pub ref_type_hash: Option<u64>,
}
/// Object table entry from a Unity serialized file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitySerializedObject {
/// Unity path ID of the object.
pub path_id: i64,
/// Object byte offset relative to data section.
pub byte_start: u64,
/// Object byte size.
pub byte_size: u32,
/// Index into the file type table.
pub type_index: usize,
/// Unity class ID resolved from `type_index`.
pub class_id: i32,
}
fn parse_text_asset(path_id: i64, data: &[u8], endian: Endian) -> Result<UnitySerializedTextAsset> {
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 {
source_path: None,
path_id,
name,
bytes,
})
}
fn read_serialized_type(
reader: &mut Reader<'_>,
version: u32,
enable_type_tree: u8,
index: usize,
) -> Result<UnitySerializedType> {
let class_id = reader.read_i32("type_class_id")?;
let is_stripped_type = if version >= 16 {
reader.read_u8("type_is_stripped")? != 0
} else {
false
};
let script_type_index = if version >= 17 {
Some(reader.read_i16("type_script_index")?)
} else {
None
};
let mut script_id = None;
let mut old_type_hash = None;
if version >= 13 {
if (version < 16 && class_id < 0) || (version >= 16 && class_id == 114) {
script_id = Some(read_hash16(reader, "type_script_id")?);
}
old_type_hash = Some(read_hash16(reader, "type_hash")?);
}
let type_tree = if enable_type_tree != 0 {
read_type_tree(reader, version)?
} else {
Vec::new()
};
Ok(UnitySerializedType {
index,
class_id,
is_stripped_type,
script_type_index,
script_id,
old_type_hash,
type_tree,
})
}
fn read_hash16(reader: &mut Reader<'_>, field: &str) -> Result<[u8; 16]> {
reader
.read_bytes(16, field)?
.try_into()
.map_err(|_| AssetBundleError::parse_field(field, reader.offset(), "expected 16 bytes"))
}
fn read_type_tree(reader: &mut Reader<'_>, version: u32) -> Result<Vec<UnityTypeTreeNode>> {
if version >= 12 || version == 10 {
let node_count = reader.read_i32("type_tree_node_count")?;
if node_count < 0 {
return Err(AssetBundleError::Parse(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(AssetBundleError::Parse(format!(
"Invalid Unity type tree string buffer size: {string_buffer_size}"
)));
}
let mut raw_nodes = Vec::with_capacity(node_count as usize);
for _ in 0..node_count {
raw_nodes.push(RawTypeTreeNode {
version: reader.read_u16("type_tree_node_version")?,
level: reader.read_u8("type_tree_node_level")?,
is_array: reader.read_u8("type_tree_node_is_array")? != 0,
type_offset: reader.read_i32("type_tree_node_type_offset")?,
name_offset: reader.read_i32("type_tree_node_name_offset")?,
byte_size: reader.read_i32("type_tree_node_byte_size")?,
index: reader.read_i32("type_tree_node_index")?,
meta_flag: reader.read_i32("type_tree_node_meta_flag")?,
ref_type_hash: if version >= 19 {
Some(reader.read_u64("type_tree_node_ref_type_hash")?)
} else {
None
},
});
}
let string_buffer =
reader.read_bytes(string_buffer_size as usize, "type_tree_string_buffer")?;
if version >= 21 {
let dependency_count = reader.read_i32("type_tree_dependency_count")?;
if dependency_count < 0 {
return Err(AssetBundleError::Parse(format!(
"Invalid Unity type tree dependency count: {dependency_count}"
)));
}
reader.read_bytes(dependency_count as usize * 4, "type_tree_dependencies")?;
}
Ok(raw_nodes
.into_iter()
.map(|node| UnityTypeTreeNode {
version: node.version,
level: node.level,
is_array: node.is_array,
type_name: type_tree_string(string_buffer, node.type_offset),
name: type_tree_string(string_buffer, node.name_offset),
byte_size: node.byte_size,
index: node.index,
meta_flag: node.meta_flag,
ref_type_hash: node.ref_type_hash,
})
.collect())
} else {
read_legacy_type_tree(reader, version)
}
}
fn read_legacy_type_tree(reader: &mut Reader<'_>, version: u32) -> Result<Vec<UnityTypeTreeNode>> {
let type_name = reader.read_c_string("legacy_type_tree_type")?;
let name = reader.read_c_string("legacy_type_tree_name")?;
let byte_size = reader.read_i32("legacy_type_tree_byte_size")?;
let index = if version == 2 {
0
} else {
reader.read_i32("legacy_type_tree_index")?
};
let is_array = reader.read_i32("legacy_type_tree_is_array")? != 0;
let version_value = reader.read_i32("legacy_type_tree_version")?;
let meta_flag = reader.read_i32("legacy_type_tree_meta_flag")?;
let child_count = reader.read_i32("legacy_type_tree_child_count")?;
if child_count < 0 {
return Err(AssetBundleError::Parse(format!(
"Invalid legacy type tree child count: {child_count}"
)));
}
let mut nodes = vec![UnityTypeTreeNode {
version: version_value as u16,
level: 0,
is_array,
type_name,
name,
byte_size,
index,
meta_flag,
ref_type_hash: None,
}];
for _ in 0..child_count {
nodes.extend(read_legacy_type_tree(reader, version)?);
}
Ok(nodes)
}
struct RawTypeTreeNode {
version: u16,
level: u8,
is_array: bool,
type_offset: i32,
name_offset: i32,
byte_size: i32,
index: i32,
meta_flag: i32,
ref_type_hash: Option<u64>,
}
fn type_tree_string(buffer: &[u8], offset: i32) -> String {
let raw = offset as u32;
if raw & 0x8000_0000 != 0 {
let common_index = raw & 0x7fff_ffff;
return common_type_tree_string(common_index)
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("unity_common_string_{common_index}"));
}
let offset = raw as usize;
if offset >= buffer.len() {
return format!("invalid_string_offset_{offset}");
}
let bytes = &buffer[offset..];
let len = bytes
.iter()
.position(|&byte| byte == 0)
.unwrap_or(bytes.len());
std::str::from_utf8(&bytes[..len])
.map(ToOwned::to_owned)
.unwrap_or_else(|_| format!("invalid_utf8_string_offset_{offset}"))
}
fn common_type_tree_string(index: u32) -> Option<&'static str> {
match index {
0 => Some("AABB"),
5 => Some("Array"),
19 => Some("Base"),
28 => Some("bool"),
35 => Some("char"),
41 => Some("ColorRGBA"),
51 => Some("data"),
56 => Some("deque"),
62 => Some("double"),
69 => Some("dynamic_array"),
83 => Some("FastPropertyName"),
100 => Some("first"),
106 => Some("float"),
112 => Some("Font"),
117 => Some("GameObject"),
128 => Some("Generic Mono"),
141 => Some("GradientNEW"),
153 => Some("GUID"),
158 => Some("GUIStyle"),
167 => Some("int"),
171 => Some("list"),
176 => Some("long long"),
186 => Some("map"),
190 => Some("Matrix4x4f"),
200 => Some("m_ByteSize"),
211 => Some("m_Curve"),
219 => Some("m_EditorClassIdentifier"),
243 => Some("m_EditorHideFlags"),
261 => Some("m_Enabled"),
271 => Some("m_ExtensionPtr"),
286 => Some("m_GameObject"),
299 => Some("m_Index"),
307 => Some("m_IsArray"),
317 => Some("m_IsStatic"),
328 => Some("m_MetaFlag"),
339 => Some("m_Name"),
346 => Some("m_ObjectHideFlags"),
364 => Some("m_PrefabInternal"),
381 => Some("m_PrefabParentObject"),
402 => Some("m_Script"),
411 => Some("m_StaticEditorFlags"),
431 => Some("m_Type"),
438 => Some("m_Version"),
448 => Some("Object"),
455 => Some("pair"),
460 => Some("PPtr<Component>"),
476 => Some("PPtr<GameObject>"),
493 => Some("PPtr<Material>"),
508 => Some("PPtr<MonoBehaviour>"),
528 => Some("PPtr<MonoScript>"),
546 => Some("PPtr<Object>"),
559 => Some("PPtr<Prefab>"),
572 => Some("PPtr<Sprite>"),
585 => Some("PPtr<TextAsset>"),
601 => Some("PPtr<Texture>"),
615 => Some("PPtr<Texture2D>"),
631 => Some("PPtr<Transform>"),
647 => Some("Quaternionf"),
659 => Some("Rectf"),
665 => Some("RectInt"),
673 => Some("second"),
680 => Some("set"),
684 => Some("short"),
690 => Some("size"),
695 => Some("SInt16"),
702 => Some("SInt32"),
709 => Some("SInt64"),
716 => Some("SInt8"),
722 => Some("staticvector"),
735 => Some("string"),
742 => Some("TextAsset"),
752 => Some("Texture2D"),
762 => Some("Transform"),
772 => Some("TypelessData"),
785 => Some("UInt16"),
792 => Some("UInt32"),
799 => Some("UInt64"),
806 => Some("UInt8"),
812 => Some("unsigned int"),
825 => Some("unsigned long long"),
844 => Some("unsigned short"),
859 => Some("vector"),
866 => Some("Vector2f"),
875 => Some("Vector3f"),
884 => Some("Vector4f"),
893 => Some("m_ScriptingClassIdentifier"),
_ => None,
}
}
#[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 offset(&self) -> usize {
self.offset
}
fn set_endian(&mut self, endian: Endian) {
self.endian = endian;
}
fn read_bytes(&mut self, len: usize, field: &str) -> Result<&'a [u8]> {
let start = self.offset;
let end = self.offset.checked_add(len).ok_or_else(|| {
AssetBundleError::parse_field(field, start, "field length overflows usize")
})?;
if end > self.data.len() {
return Err(AssetBundleError::parse_field(
field,
start,
format!(
"unexpected end: need {}, have {}",
len,
self.data.len().saturating_sub(start)
),
));
}
let bytes = &self.data[self.offset..end];
self.offset = end;
Ok(bytes)
}
fn align(&mut self, alignment: usize) -> Result<()> {
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> {
Ok(self.read_bytes(1, field)?[0])
}
fn read_u16(&mut self, field: &str) -> Result<u16> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
let start = self.offset;
let remaining = &self.data[self.offset..];
let Some(length) = remaining.iter().position(|&byte| byte == 0) else {
return Err(AssetBundleError::parse_field(
field,
start,
"missing null terminator",
));
};
let bytes = self.read_bytes(length, field)?;
self.offset += 1;
std::str::from_utf8(bytes)
.map(ToOwned::to_owned)
.map_err(|error| {
AssetBundleError::parse_field(field, start, format!("invalid UTF-8: {error}"))
})
}
fn read_len_prefixed_string(&mut self, field: &str) -> Result<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| {
AssetBundleError::parse_field(field, self.offset, format!("invalid UTF-8: {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_and_object_table() {
let file = synthetic_serialized_file();
let parsed =
UnitySerializedFile::from_named_slice(Some("CAB-test".to_string()), &file).unwrap();
assert_eq!(parsed.source_path.as_deref(), Some("CAB-test"));
assert_eq!(parsed.version, 22);
assert_eq!(parsed.unity_version, "2021.3.56f2");
assert_eq!(parsed.platform, 19);
assert_eq!(parsed.types.len(), 1);
assert_eq!(parsed.types[0].class_id, 49);
assert_eq!(parsed.objects.len(), 1);
assert_eq!(parsed.objects[0].path_id, 1);
assert_eq!(parsed.objects[0].class_id, 49);
assert_eq!(parsed.text_assets.len(), 1);
let asset = parsed.text_asset("GameMainConfig").unwrap();
assert_eq!(asset.source_path.as_deref(), Some("CAB-test"));
assert_eq!(asset.name, "GameMainConfig");
assert_eq!(asset.bytes, b"hello");
}
}
+180 -3
View File
@@ -1,7 +1,184 @@
//! AssetBundle 类型定义占位
//! AssetBundle and UnityFS public types.
/// Asset 类型(待实现)
use crate::serialized::{UnitySerializedFile, UnitySerializedTextAsset};
/// Asset type extracted from a Unity bundle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AssetType {
/// 文本资源
/// Unity TextAsset.
TextAsset,
}
/// Raw AssetBundle bytes with optional source path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawAssetBundle {
/// File bytes.
pub data: Vec<u8>,
/// Source path or logical name, when known.
pub path: Option<String>,
}
/// Parsed AssetBundle summary used by higher layers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAssetBundle {
/// Unity editor version declared by the bundle.
pub unity_version: String,
/// Directory paths exposed by the UnityFS container.
pub assets: Vec<String>,
/// Original bytes retained for future serialization.
pub raw_data: Vec<u8>,
/// UnityFS header information.
pub unityfs_header: Option<UnityFsHeader>,
/// UnityFS compressed block entries.
pub blocks: Vec<UnityFsBlockInfo>,
/// UnityFS directory entries.
pub directories: Vec<UnityFsDirectoryInfo>,
/// Files extracted from the UnityFS uncompressed data region.
pub files: Vec<UnityFsFile>,
/// Serialized files parsed from UnityFS directory files.
pub serialized_files: Vec<UnitySerializedFile>,
/// TextAsset objects extracted from serialized files.
pub text_assets: Vec<UnitySerializedTextAsset>,
/// Non-fatal serialized-file parse diagnostics for extracted files.
pub serialized_parse_errors: Vec<UnitySerializedParseError>,
}
/// Parsed UnityFS container.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnityFsBundle {
/// UnityFS header.
pub header: UnityFsHeader,
/// 16-byte block info hash stored before block entries.
pub blocks_info_hash: [u8; 16],
/// UnityFS compressed block entries.
pub blocks: Vec<UnityFsBlockInfo>,
/// UnityFS directory entries.
pub directories: Vec<UnityFsDirectoryInfo>,
/// Offset where compressed block payload bytes begin.
pub data_start_offset: u64,
/// Total compressed payload bytes declared by block entries.
pub compressed_data_size: u64,
/// Total uncompressed payload bytes declared by block entries.
pub uncompressed_data_size: u64,
/// Original bytes retained for future extraction/serialization.
pub raw_data: Vec<u8>,
/// Files extracted from the UnityFS uncompressed data region.
pub files: Vec<UnityFsFile>,
/// Serialized files parsed from UnityFS directory files.
pub serialized_files: Vec<UnitySerializedFile>,
/// TextAsset objects extracted from serialized files.
pub text_assets: Vec<UnitySerializedTextAsset>,
/// Non-fatal serialized-file parse diagnostics for extracted files.
pub serialized_parse_errors: Vec<UnitySerializedParseError>,
}
impl UnityFsBundle {
/// Returns directory paths in stable order.
pub fn asset_paths(&self) -> Vec<String> {
self.directories
.iter()
.map(|directory| directory.path.clone())
.collect()
}
}
impl From<UnityFsBundle> for ParsedAssetBundle {
fn from(bundle: UnityFsBundle) -> Self {
Self {
unity_version: bundle.header.unity_version.clone(),
assets: bundle.asset_paths(),
raw_data: bundle.raw_data,
unityfs_header: Some(bundle.header),
blocks: bundle.blocks,
directories: bundle.directories,
files: bundle.files,
serialized_files: bundle.serialized_files,
text_assets: bundle.text_assets,
serialized_parse_errors: bundle.serialized_parse_errors,
}
}
}
/// UnityFS header.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnityFsHeader {
/// UnityFS format version.
pub format_version: u32,
/// Bundle target version, for example `5.x.x`.
pub target_version: String,
/// Unity editor version, for example `2021.3.56f2`.
pub unity_version: String,
/// Total file size declared by the header.
pub total_size: u64,
/// Compressed block info byte size.
pub compressed_blocks_info_size: u32,
/// Uncompressed block info byte size.
pub uncompressed_blocks_info_size: u32,
/// Raw UnityFS flags.
pub flags: u32,
}
/// UnityFS compression mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum UnityFsCompression {
/// Uncompressed.
None,
/// LZMA compression.
Lzma,
/// LZ4 compression.
Lz4,
/// LZ4HC compression.
Lz4Hc,
/// Unknown compression mode.
Unknown(u16),
}
/// UnityFS compressed block entry.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnityFsBlockInfo {
/// Uncompressed block size.
pub uncompressed_size: u32,
/// Compressed block size.
pub compressed_size: u32,
/// Raw block flags.
pub flags: u16,
/// Compression mode decoded from `flags`.
pub compression: UnityFsCompression,
}
/// UnityFS directory entry.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnityFsDirectoryInfo {
/// Entry offset in the uncompressed data region.
pub offset: u64,
/// Entry byte size.
pub size: u64,
/// Raw directory flags.
pub flags: u32,
/// Entry path.
pub path: String,
}
/// File extracted from a UnityFS directory entry.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnityFsFile {
/// Entry path from the UnityFS directory table.
pub path: String,
/// Offset in the uncompressed UnityFS data region.
pub offset: u64,
/// File byte size.
pub size: u64,
/// Raw directory flags.
pub flags: u32,
/// Extracted file bytes.
pub data: Vec<u8>,
}
/// Non-fatal parse error for an extracted UnityFS file.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnitySerializedParseError {
/// UnityFS directory path that failed serialized-file parsing.
pub path: String,
/// Human-readable parser error.
pub error: String,
}