mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
feat(assetbundle): 完善官方资源解析与双目录发布
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user