Files
BlueArchiveToolkit/crates/bat-assetbundle/src/serialized.rs
T
nyaKazuha 69b6e36bf0
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
feat(assetbundle): 完成已验证结构的重建发布闭环
2026-09-10 00:46:48 +08:00

6888 lines
253 KiB
Rust

//! 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;
/// Value decoded from a Unity serialized TypeTree node.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", content = "value")]
pub enum UnitySerializedValue {
/// Boolean value.
Bool(bool),
/// Signed integer value.
Signed(i64),
/// Unsigned integer value.
Unsigned(u64),
/// IEEE-754 single precision value stored as raw bits.
Float32(u32),
/// IEEE-754 double precision value stored as raw bits.
Float64(u64),
/// UTF-8 string value.
String(String),
/// Raw byte sequence.
Bytes(Vec<u8>),
/// Fixed-size Unity value type made of IEEE-754 single precision raw bits,
/// for example `Vector3f`, `ColorRGBA`, `Quaternionf`, `Rectf` or `AABB`.
Float32Struct {
/// TypeTree type name.
type_name: String,
/// Raw `f32::to_bits()` values in serialized field order.
values: Vec<u32>,
},
/// Fixed-size Unity value type made of signed 32-bit integer components,
/// for example `Vector2Int`, `Vector3Int`, `RectInt` or `BoundsInt`.
Int32Struct {
/// TypeTree type name.
type_name: String,
/// Signed integer values in serialized field order.
values: Vec<i32>,
},
/// Fixed-size Unity byte value type, for example `GUID` or `Hash128`.
FixedBytes {
/// TypeTree type name.
type_name: String,
/// Raw bytes in serialized field order.
bytes: Vec<u8>,
},
/// Unity enum value decoded through a TypeTree `value__` child.
Enum {
/// TypeTree enum type name.
type_name: String,
/// Backing integer storage type, for example `int` or `UInt32`.
storage_type: String,
/// Signed enum value.
value: i64,
},
/// Unity bit field value decoded through a `m_Bits`/`bits` child, for
/// example `LayerMask` or `BitField`.
BitField {
/// TypeTree bit field type name.
type_name: String,
/// Backing integer storage type, for example `int` or `UInt32`.
storage_type: String,
/// Bit mask value.
bits: i64,
},
/// Pointer to an object in the same or another serialized file.
PPtr {
/// Referenced serialized file identifier.
file_id: i32,
/// Referenced Unity path ID.
path_id: i64,
},
/// Repeated fields decoded from an array/vector node.
Array(Vec<UnitySerializedField>),
/// Repeated pair/object fields decoded from a map node.
Map(Vec<UnitySerializedField>),
/// Nested object fields.
Object(Vec<UnitySerializedField>),
/// Managed-reference payload decoded from TypeTree-covered fields or
/// retained as fixed-size raw bytes.
ManagedReference {
/// TypeTree type name for the managed-reference node.
type_name: String,
/// Best-effort metadata decoded from TypeTree-covered registry fields.
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<UnityManagedReferenceMetadata>,
/// Decoded managed-reference fields.
fields: Vec<UnitySerializedField>,
/// Raw bytes retained when the TypeTree node has no children but a
/// fixed byte size.
bytes: Vec<u8>,
},
/// Unity managed-reference registry decoded from TypeTree-covered fields.
ManagedReferenceRegistry {
/// Registry records inferred from `references` array entries.
references: Vec<UnityManagedReferenceRecord>,
/// Decoded raw registry fields. These preserve all original field paths
/// and are used for text extraction and field patch lookup.
fields: Vec<UnitySerializedField>,
},
/// Bytes retained when a node has a declared fixed size but no known
/// primitive or child-node decoder.
Unknown {
/// TypeTree type name.
type_name: String,
/// Raw bytes belonging to the node.
bytes: Vec<u8>,
},
}
/// Best-effort metadata for one managed reference.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnityManagedReferenceMetadata {
/// Unity managed-reference ID, when present in TypeTree-covered fields.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reference_id: Option<i64>,
/// Raw managed full type name, when Unity stores it as one combined field.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_type_name: Option<String>,
/// Managed class or concrete type name.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub type_name: Option<String>,
/// Managed namespace.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
/// Managed assembly name.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assembly_name: Option<String>,
}
impl UnityManagedReferenceMetadata {
fn is_empty(&self) -> bool {
self.reference_id.is_none()
&& self.full_type_name.is_none()
&& self.type_name.is_none()
&& self.namespace.is_none()
&& self.assembly_name.is_none()
}
}
/// One managed-reference registry entry.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnityManagedReferenceRecord {
/// Registry metadata decoded from record fields.
pub metadata: UnityManagedReferenceMetadata,
/// Payload fields for this reference, usually the `data` child.
pub fields: Vec<UnitySerializedField>,
}
/// Semantic replacement value for a decoded Unity TypeTree field.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum UnitySerializedReplacementValue {
/// Boolean value.
Bool(bool),
/// Signed integer value.
Signed(i64),
/// Unsigned integer value.
Unsigned(u64),
/// IEEE-754 single precision raw bits.
Float32(u32),
/// IEEE-754 double precision raw bits.
Float64(u64),
/// UTF-8 string value.
String(String),
/// Raw byte sequence for TypelessData/bytes nodes.
Bytes(Vec<u8>),
/// Fixed-size Unity value type made of IEEE-754 single precision raw bits.
Float32Struct {
/// TypeTree type name.
type_name: String,
/// Raw `f32::to_bits()` values in serialized field order.
values: Vec<u32>,
},
/// Fixed-size Unity value type made of signed 32-bit integer components.
Int32Struct {
/// TypeTree type name.
type_name: String,
/// Signed integer values in serialized field order.
values: Vec<i32>,
},
/// Fixed-size Unity byte value type.
FixedBytes {
/// TypeTree type name.
type_name: String,
/// Raw bytes in serialized field order.
bytes: Vec<u8>,
},
/// Unity enum replacement value.
Enum {
/// TypeTree enum type name.
type_name: String,
/// Backing integer storage type, for example `int` or `UInt32`.
storage_type: String,
/// Signed enum value.
value: i64,
},
/// Unity bit field replacement value.
BitField {
/// TypeTree bit field type name.
type_name: String,
/// Backing integer storage type, for example `int` or `UInt32`.
storage_type: String,
/// Bit mask value.
bits: i64,
},
/// Pointer to an object in the same or another serialized file.
PPtr {
/// Referenced serialized file identifier.
file_id: i32,
/// Referenced Unity path ID.
path_id: i64,
},
/// Whole-array replacement. Existing items or the TypeTree data node are
/// used as the element encoding schema, so length changes are supported
/// even when the current array is empty.
Array(Vec<UnitySerializedReplacementValue>),
/// Whole-map replacement. Existing entries or the TypeTree data node are
/// used as the entry encoding schema.
Map(Vec<UnitySerializedReplacementValue>),
/// Replacement for one TypeTree object value.
Object(Vec<UnitySerializedFieldReplacement>),
}
/// One named child replacement inside a TypeTree object.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnitySerializedFieldReplacement {
/// TypeTree child field name, for example `first`, `second` or `message`.
pub name: String,
/// Replacement value for that child.
pub value: UnitySerializedReplacementValue,
}
impl UnitySerializedReplacementValue {
/// Returns true when this semantic value matches a decoded field value.
pub fn matches_serialized_value(&self, value: &UnitySerializedValue) -> bool {
matches!(
(self, value),
(Self::Bool(expected), UnitySerializedValue::Bool(actual)) if expected == actual
) || matches!(
(self, value),
(Self::Signed(expected), UnitySerializedValue::Signed(actual)) if expected == actual
) || matches!(
(self, value),
(Self::Unsigned(expected), UnitySerializedValue::Unsigned(actual)) if expected == actual
) || matches!(
(self, value),
(Self::Float32(expected), UnitySerializedValue::Float32(actual)) if expected == actual
) || matches!(
(self, value),
(Self::Float64(expected), UnitySerializedValue::Float64(actual)) if expected == actual
) || matches!(
(self, value),
(Self::String(expected), UnitySerializedValue::String(actual)) if expected == actual
) || matches!(
(self, value),
(Self::Bytes(expected), UnitySerializedValue::Bytes(actual)) if expected == actual
) || matches!(
(self, value),
(Self::Bytes(expected), UnitySerializedValue::Unknown { bytes: actual, .. }) if expected == actual
) || matches!(
(self, value),
(
Self::Float32Struct {
type_name: expected_type,
values: expected_values,
},
UnitySerializedValue::Float32Struct {
type_name: actual_type,
values: actual_values,
},
) if normalized_metadata_key(expected_type) == normalized_metadata_key(actual_type)
&& expected_values == actual_values
) || matches!(
self,
Self::Float32Struct {
type_name: expected_type,
values: expected_values,
} if object_value_matches_float32_struct(value, expected_type, expected_values)
) || matches!(
(self, value),
(
Self::Int32Struct {
type_name: expected_type,
values: expected_values,
},
UnitySerializedValue::Int32Struct {
type_name: actual_type,
values: actual_values,
},
) if normalized_metadata_key(expected_type) == normalized_metadata_key(actual_type)
&& expected_values == actual_values
) || matches!(
self,
Self::Int32Struct {
type_name: expected_type,
values: expected_values,
} if object_value_matches_int32_struct(value, expected_type, expected_values)
) || matches!(
(self, value),
(
Self::FixedBytes {
type_name: expected_type,
bytes: expected_bytes,
},
UnitySerializedValue::FixedBytes {
type_name: actual_type,
bytes: actual_bytes,
},
) if normalized_metadata_key(expected_type) == normalized_metadata_key(actual_type)
&& expected_bytes == actual_bytes
) || matches!(
self,
Self::FixedBytes {
type_name: expected_type,
bytes: expected_bytes,
} if object_value_matches_fixed_bytes(value, expected_type, expected_bytes)
) || matches!(
(
self,
value,
),
(
Self::Enum {
type_name: expected_type,
storage_type: expected_storage,
value: expected_value,
},
UnitySerializedValue::Enum {
type_name: actual_type,
storage_type: actual_storage,
value: actual_value,
},
) if normalized_metadata_key(expected_type) == normalized_metadata_key(actual_type)
&& normalized_metadata_key(expected_storage)
== normalized_metadata_key(actual_storage)
&& expected_value == actual_value
) || matches!(
(
self,
value,
),
(
Self::BitField {
type_name: expected_type,
storage_type: expected_storage,
bits: expected_bits,
},
UnitySerializedValue::BitField {
type_name: actual_type,
storage_type: actual_storage,
bits: actual_bits,
},
) if normalized_metadata_key(expected_type) == normalized_metadata_key(actual_type)
&& normalized_metadata_key(expected_storage)
== normalized_metadata_key(actual_storage)
&& expected_bits == actual_bits
) || matches!(
(self, value),
(
Self::PPtr {
file_id: expected_file_id,
path_id: expected_path_id,
},
UnitySerializedValue::PPtr {
file_id: actual_file_id,
path_id: actual_path_id,
},
) if expected_file_id == actual_file_id && expected_path_id == actual_path_id
) || matches!(
(self, value),
(
Self::Array(expected_items),
UnitySerializedValue::Array(actual_items),
) if expected_items.len() == actual_items.len()
&& expected_items
.iter()
.zip(actual_items)
.all(|(expected, actual)| expected.matches_serialized_value(&actual.value))
) || matches!(
(self, value),
(
Self::Map(expected_items),
UnitySerializedValue::Map(actual_items),
) if expected_items.len() == actual_items.len()
&& expected_items
.iter()
.zip(actual_items)
.all(|(expected, actual)| expected.matches_serialized_value(&actual.value))
) || matches!(
(self, value),
(Self::Object(expected_fields), UnitySerializedValue::Object(actual_fields))
if replacement_fields_match_serialized_fields(expected_fields, actual_fields)
) || matches!(
(self, value),
(
Self::Object(expected_fields),
UnitySerializedValue::ManagedReference {
fields: actual_fields,
..
},
) if replacement_fields_match_serialized_fields(expected_fields, actual_fields)
) || matches!(
(self, value),
(
Self::Object(expected_fields),
UnitySerializedValue::ManagedReferenceRegistry {
fields: actual_fields,
..
},
) if replacement_fields_match_serialized_fields(expected_fields, actual_fields)
)
}
}
fn replacement_fields_match_serialized_fields(
expected_fields: &[UnitySerializedFieldReplacement],
actual_fields: &[UnitySerializedField],
) -> bool {
expected_fields.iter().all(|expected| {
actual_fields
.iter()
.find(|actual| actual.name == expected.name)
.is_some_and(|actual| expected.value.matches_serialized_value(&actual.value))
})
}
fn object_value_matches_float32_struct(
value: &UnitySerializedValue,
expected_type: &str,
expected_values: &[u32],
) -> bool {
object_float32_struct_values(value, expected_type)
.is_some_and(|actual_values| actual_values == expected_values)
}
fn object_value_matches_int32_struct(
value: &UnitySerializedValue,
expected_type: &str,
expected_values: &[i32],
) -> bool {
object_int32_struct_values(value, expected_type)
.is_some_and(|actual_values| actual_values == expected_values)
}
fn object_value_matches_fixed_bytes(
value: &UnitySerializedValue,
expected_type: &str,
expected_bytes: &[u8],
) -> bool {
object_fixed_bytes_value(value, expected_type)
.is_some_and(|actual_bytes| actual_bytes == expected_bytes)
}
fn object_float32_struct_values(value: &UnitySerializedValue, type_name: &str) -> Option<Vec<u32>> {
let component_count = unity_float32_struct_component_count(type_name)?;
let UnitySerializedValue::Object(fields) = value else {
return None;
};
if fields.len() != component_count {
return None;
}
fields
.iter()
.map(|field| match field.value {
UnitySerializedValue::Float32(value) => Some(value),
_ => None,
})
.collect()
}
fn object_int32_struct_values(value: &UnitySerializedValue, type_name: &str) -> Option<Vec<i32>> {
let component_count = unity_int32_struct_component_count(type_name)?;
let UnitySerializedValue::Object(fields) = value else {
return None;
};
if fields.len() != component_count {
return None;
}
fields
.iter()
.map(|field| match field.value {
UnitySerializedValue::Signed(value) => i32::try_from(value).ok(),
_ => None,
})
.collect()
}
fn object_fixed_bytes_value(value: &UnitySerializedValue, type_name: &str) -> Option<Vec<u8>> {
let byte_size = unity_fixed_bytes_size(type_name)?;
let UnitySerializedValue::Object(fields) = value else {
return None;
};
if fields.len() != byte_size {
return None;
}
fields
.iter()
.map(|field| match field.value {
UnitySerializedValue::Unsigned(value) => u8::try_from(value).ok(),
UnitySerializedValue::Signed(value) => u8::try_from(value).ok(),
_ => None,
})
.collect()
}
/// One field decoded from a Unity serialized object.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnitySerializedField {
/// Stable path relative to the serialized object, for example
/// `config.entries[0].message`.
pub path: String,
/// TypeTree field name.
pub name: String,
/// TypeTree field type name.
pub type_name: String,
/// Byte offset relative to the beginning of the object payload.
pub offset: usize,
/// Number of bytes consumed by this field, including alignment padding.
pub byte_size: usize,
/// Index into the source TypeTree node table, when the field was decoded
/// from an embedded TypeTree.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub type_tree_node_index: Option<usize>,
/// Decoded value.
pub value: UnitySerializedValue,
}
/// 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>,
/// Serialized file bytes retained for field-level object decoding.
raw_data: Vec<u8>,
/// Absolute offset of the serialized object data section.
data_offset: usize,
/// Absolute byte offsets of object table entries in metadata order.
object_table_offsets: Vec<usize>,
/// Byte order used by serialized metadata and object data.
endian: Endian,
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 data_offset_usize = usize::try_from(data_offset).map_err(|_| {
AssetBundleError::Parse(format!(
"Unity serialized data_offset {} does not fit usize",
data_offset
))
})?;
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 object_table_offsets = Vec::with_capacity(object_count as usize);
let mut text_assets = Vec::new();
for _ in 0..object_count {
if version >= 14 {
reader.align(4)?;
}
object_table_offsets.push(reader.offset());
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,
raw_data: data.to_vec(),
data_offset: data_offset_usize,
object_table_offsets,
endian,
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)
}
/// Returns true when the object references an embedded TypeTree.
pub fn object_has_type_tree(&self, object: &UnitySerializedObject) -> bool {
self.types
.get(object.type_index)
.is_some_and(|type_info| !type_info.type_tree.is_empty())
}
/// Replaces one TextAsset payload and rewrites the serialized object table.
///
/// The serialized file header, metadata layout and byte order are
/// preserved. Object payload size may change; subsequent object offsets
/// are updated accordingly. Versions before 14 are rejected because their
/// object table uses a different path-ID layout.
pub fn replace_text_asset(
&self,
path_id: i64,
expected_name: Option<&str>,
replacement: &[u8],
) -> Result<Vec<u8>> {
let target_index = self
.objects
.iter()
.position(|object| object.path_id == path_id && object.class_id == 49)
.ok_or_else(|| {
AssetBundleError::Parse(format!("TextAsset object path_id {path_id} not found"))
})?;
let text_asset = self
.text_assets
.iter()
.find(|asset| asset.path_id == path_id)
.ok_or_else(|| {
AssetBundleError::Parse(format!(
"TextAsset payload path_id {path_id} was not extracted"
))
})?;
if expected_name.is_some_and(|name| name != text_asset.name) {
return Err(AssetBundleError::Parse(format!(
"TextAsset path_id {path_id} name mismatch: expected {:?}, actual {:?}",
expected_name, text_asset.name
)));
}
let replacement_object = encode_text_asset(&text_asset.name, replacement, self.endian)?;
self.rewrite_object_payload(target_index, replacement_object)
}
/// Replaces one TypeTree string field and rewrites the serialized object.
///
/// The field is looked up by its stable `field_path` in the decoded
/// TypeTree tree. This supports MonoBehaviour, ScriptableObject and
/// managed-reference children as long as the field is a UTF-8 string node.
pub fn replace_string_field(
&self,
path_id: i64,
field_path: &str,
expected_value: Option<&str>,
replacement: &str,
) -> Result<Vec<u8>> {
let object_index = self
.objects
.iter()
.position(|object| object.path_id == path_id)
.ok_or_else(|| {
AssetBundleError::Parse(format!("Unity object path_id {path_id} not found"))
})?;
let object = &self.objects[object_index];
let fields = self.fields_for_object_entry(object)?;
let field = find_field_by_path(&fields, field_path).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} field {} not found",
path_id, field_path
))
})?;
let current = match &field.value {
UnitySerializedValue::String(text) => text,
other => {
return Err(AssetBundleError::UnsupportedFormat(format!(
"Unity object path_id {} field {} is not a string field (found {:?})",
path_id, field_path, other
)));
}
};
if expected_value.is_some_and(|expected| expected != current) {
return Err(AssetBundleError::Parse(format!(
"Unity object path_id {} field {} mismatch: expected {:?}, actual {:?}",
path_id, field_path, expected_value, current
)));
}
let expected_encoded = encode_aligned_string(current, self.endian)?;
let encoded = encode_aligned_string(replacement, self.endian)?;
self.replace_field_bytes(path_id, field_path, Some(&expected_encoded), &encoded)
}
/// Replaces one decoded TypeTree field with a semantic value.
///
/// This supports primitive values, `string`, `TypelessData`/`bytes`,
/// `PPtr`, object fields and TypeTree-covered array/map containers.
pub fn replace_field_value(
&self,
path_id: i64,
field_path: &str,
expected_value: Option<&UnitySerializedReplacementValue>,
replacement: &UnitySerializedReplacementValue,
) -> Result<Vec<u8>> {
let object_index = self
.objects
.iter()
.position(|object| object.path_id == path_id)
.ok_or_else(|| {
AssetBundleError::Parse(format!("Unity object path_id {path_id} not found"))
})?;
let object = &self.objects[object_index];
let type_info = self.types.get(object.type_index).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} references missing type index {}",
object.path_id, object.type_index
))
})?;
let fields = self.fields_for_object_entry(object)?;
let field = find_field_by_path(&fields, field_path).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} field {} not found",
path_id, field_path
))
})?;
if expected_value.is_some_and(|expected| !expected.matches_serialized_value(&field.value)) {
return Err(AssetBundleError::Parse(format!(
"Unity object path_id {} field {} value mismatch",
path_id, field_path
)));
}
let context = ReplacementEncodingContext {
serialized_version: self.version,
endian: self.endian,
nodes: Some(&type_info.type_tree),
};
let encoded = encode_replacement_value(
replacement,
&field.value,
&field.type_name,
field.byte_size,
field.type_tree_node_index,
context,
)?;
self.replace_field_bytes(path_id, field_path, None, &encoded)
}
/// Replaces one decoded TypeTree field with raw bytes and rewrites the object.
pub fn replace_field_bytes(
&self,
path_id: i64,
field_path: &str,
expected_bytes: Option<&[u8]>,
replacement: &[u8],
) -> Result<Vec<u8>> {
let object_index = self
.objects
.iter()
.position(|object| object.path_id == path_id)
.ok_or_else(|| {
AssetBundleError::Parse(format!("Unity object path_id {path_id} not found"))
})?;
let object = &self.objects[object_index];
let fields = self.fields_for_object_entry(object)?;
let field = find_field_by_path(&fields, field_path).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} field {} not found",
path_id, field_path
))
})?;
let object_start = self
.data_offset
.checked_add(usize::try_from(object.byte_start).map_err(|_| {
AssetBundleError::Parse(format!(
"Unity object path_id {} byte_start does not fit usize",
object.path_id
))
})?)
.ok_or_else(|| AssetBundleError::Parse("Unity object offset overflow".to_string()))?;
let object_end = object_start
.checked_add(object.byte_size as usize)
.ok_or_else(|| AssetBundleError::Parse("Unity object size overflow".to_string()))?;
let object_data = self.raw_data.get(object_start..object_end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} byte range {}..{} exceeds file size {}",
object.path_id,
object_start,
object_end,
self.raw_data.len()
))
})?;
let start = field.offset;
let end = start
.checked_add(field.byte_size)
.ok_or_else(|| AssetBundleError::Parse("field range overflow".to_string()))?;
if end > object_data.len() {
return Err(AssetBundleError::Parse(format!(
"Unity object path_id {} field {} range {}..{} exceeds object size {}",
object.path_id,
field_path,
start,
end,
object_data.len()
)));
}
if expected_bytes.is_some_and(|expected| expected != &object_data[start..end]) {
return Err(AssetBundleError::Parse(format!(
"Unity object path_id {} field {} byte mismatch",
path_id, field_path
)));
}
let mut replacement_object = Vec::with_capacity(
object_data
.len()
.saturating_sub(field.byte_size)
.saturating_add(replacement.len()),
);
replacement_object.extend_from_slice(&object_data[..start]);
replacement_object.extend_from_slice(replacement);
replacement_object.extend_from_slice(&object_data[end..]);
self.rewrite_object_payload(object_index, replacement_object)
}
/// Decodes the TypeTree fields for one object.
///
/// `TextAsset` extraction remains available through [`Self::text_assets`].
/// This method is intended for `MonoBehaviour`, `ScriptableObject` and
/// other objects whose file contains a TypeTree. It never guesses a
/// layout when the TypeTree is absent or a node type is unknown.
pub fn fields_for_object(&self, path_id: i64) -> Result<Vec<UnitySerializedField>> {
let object = self
.objects
.iter()
.find(|object| object.path_id == path_id)
.ok_or_else(|| {
AssetBundleError::Parse(format!("Unity object path_id {path_id} not found"))
})?;
self.fields_for_object_entry(object)
}
/// Decodes the TypeTree fields for an object table entry.
pub fn fields_for_object_entry(
&self,
object: &UnitySerializedObject,
) -> Result<Vec<UnitySerializedField>> {
let type_info = self.types.get(object.type_index).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} references missing type index {}",
object.path_id, object.type_index
))
})?;
if type_info.type_tree.is_empty() {
return Err(AssetBundleError::UnsupportedFormat(format!(
"Unity object path_id {} class_id {} has no TypeTree",
object.path_id, object.class_id
)));
}
let object_start = self
.data_offset
.checked_add(usize::try_from(object.byte_start).map_err(|_| {
AssetBundleError::Parse(format!(
"Unity object path_id {} byte_start does not fit usize",
object.path_id
))
})?)
.ok_or_else(|| AssetBundleError::Parse("Unity object offset overflow".to_string()))?;
let object_end = object_start
.checked_add(object.byte_size as usize)
.ok_or_else(|| AssetBundleError::Parse("Unity object size overflow".to_string()))?;
let object_data = self.raw_data.get(object_start..object_end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} byte range {}..{} exceeds file size {}",
object.path_id,
object_start,
object_end,
self.raw_data.len()
))
})?;
let mut decoder = FieldDecoder::new(object_data, self.version, self.endian);
let root = decoder.decode_node(&type_info.type_tree, 0, String::new())?;
match root.value {
UnitySerializedValue::Object(fields) => Ok(fields),
value => Ok(vec![UnitySerializedField {
path: root.name.clone(),
name: root.name,
type_name: root.type_name,
offset: root.offset,
byte_size: root.byte_size,
type_tree_node_index: root.type_tree_node_index,
value,
}]),
}
}
/// Returns the raw payload bytes for one object table entry.
///
/// The returned slice is still owned by the parsed serialized file. It is
/// useful to compare untouched objects across a variable-length rebuild.
pub fn object_bytes(&self, object: &UnitySerializedObject) -> Result<&[u8]> {
let object_start = self
.data_offset
.checked_add(usize::try_from(object.byte_start).map_err(|_| {
AssetBundleError::Parse(format!(
"Unity object path_id {} byte_start does not fit usize",
object.path_id
))
})?)
.ok_or_else(|| AssetBundleError::Parse("Unity object offset overflow".to_string()))?;
let object_end = object_start
.checked_add(object.byte_size as usize)
.ok_or_else(|| AssetBundleError::Parse("Unity object size overflow".to_string()))?;
self.raw_data.get(object_start..object_end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"Unity object path_id {} byte range {}..{} exceeds file size {}",
object.path_id,
object_start,
object_end,
self.raw_data.len()
))
})
}
fn rewrite_object_payload(
&self,
target_index: usize,
replacement_object: Vec<u8>,
) -> Result<Vec<u8>> {
if self.version < 14 {
return Err(AssetBundleError::UnsupportedFormat(format!(
"serialized object payload rewriting does not support version {}",
self.version
)));
}
let original_data = self.raw_data.get(self.data_offset..).ok_or_else(|| {
AssetBundleError::Parse("serialized data offset is invalid".to_string())
})?;
let mut ordered_objects = self.objects.iter().enumerate().collect::<Vec<_>>();
ordered_objects.sort_by_key(|(_, object)| object.byte_start);
let mut rewritten_data = Vec::with_capacity(
original_data
.len()
.saturating_sub(self.objects[target_index].byte_size as usize)
.saturating_add(replacement_object.len()),
);
let mut new_offsets = vec![0usize; self.objects.len()];
let mut cursor = 0usize;
for (index, object) in ordered_objects {
let start = usize::try_from(object.byte_start).map_err(|_| {
AssetBundleError::Parse(format!(
"object path_id {} byte_start does not fit usize",
object.path_id
))
})?;
let end = start
.checked_add(object.byte_size as usize)
.ok_or_else(|| AssetBundleError::Parse("object range overflow".to_string()))?;
if start < cursor || end > original_data.len() {
return Err(AssetBundleError::Parse(format!(
"object path_id {} range {}..{} is invalid",
object.path_id, start, end
)));
}
rewritten_data.extend_from_slice(&original_data[cursor..start]);
new_offsets[index] = rewritten_data.len();
if index == target_index {
rewritten_data.extend_from_slice(&replacement_object);
} else {
rewritten_data.extend_from_slice(&original_data[start..end]);
}
cursor = end;
}
rewritten_data.extend_from_slice(&original_data[cursor..]);
let mut output = self.raw_data[..self.data_offset].to_vec();
output.extend_from_slice(&rewritten_data);
let file_size = u64::try_from(output.len())
.map_err(|_| AssetBundleError::Parse("rewritten file size overflow".to_string()))?;
if self.version >= 22 {
write_u64_be(&mut output, 24, file_size)?;
} else {
let file_size = u32::try_from(file_size).map_err(|_| {
AssetBundleError::Parse("rewritten legacy file exceeds u32 size".to_string())
})?;
write_u32_be(&mut output, 4, file_size)?;
}
for (index, object) in self.objects.iter().enumerate() {
let entry_offset = *self.object_table_offsets.get(index).ok_or_else(|| {
AssetBundleError::Parse(format!(
"missing object table offset for path_id {}",
object.path_id
))
})?;
let byte_size = if index == target_index {
replacement_object.len()
} else {
object.byte_size as usize
};
let byte_size = u32::try_from(byte_size).map_err(|_| {
AssetBundleError::Parse("rewritten object exceeds u32 size".to_string())
})?;
if self.version >= 22 {
write_u64_endian(
&mut output,
entry_offset + 8,
new_offsets[index] as u64,
self.endian,
)?;
write_u32_endian(&mut output, entry_offset + 16, byte_size, self.endian)?;
} else {
let byte_start = u32::try_from(new_offsets[index]).map_err(|_| {
AssetBundleError::Parse("rewritten object offset exceeds u32".to_string())
})?;
write_u32_endian(&mut output, entry_offset + 8, byte_start, self.endian)?;
write_u32_endian(&mut output, entry_offset + 12, byte_size, self.endian)?;
}
}
Ok(output)
}
}
/// 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,
}
struct FieldDecoder<'a> {
reader: Reader<'a>,
serialized_version: u32,
}
impl<'a> FieldDecoder<'a> {
fn new(data: &'a [u8], serialized_version: u32, endian: Endian) -> Self {
let mut reader = Reader::new(data);
reader.set_endian(endian);
Self {
reader,
serialized_version,
}
}
fn decode_node(
&mut self,
nodes: &[UnityTypeTreeNode],
index: usize,
path: String,
) -> Result<UnitySerializedField> {
let node = nodes.get(index).ok_or_else(|| {
AssetBundleError::Parse(format!("TypeTree node index {index} is out of range"))
})?;
let start = self.reader.offset();
let value = self.decode_value(nodes, index, path.clone())?;
if node.meta_flag & TYPE_TREE_ALIGN_BYTES != 0 {
self.reader.align(4)?;
}
let end = self.reader.offset();
Ok(UnitySerializedField {
path,
name: node.name.clone(),
type_name: node.type_name.clone(),
offset: start,
byte_size: end.saturating_sub(start),
type_tree_node_index: Some(index),
value,
})
}
fn decode_value(
&mut self,
nodes: &[UnityTypeTreeNode],
index: usize,
path: String,
) -> Result<UnitySerializedValue> {
let node = nodes.get(index).ok_or_else(|| {
AssetBundleError::Parse(format!("TypeTree node index {index} is out of range"))
})?;
let end = node_end(nodes, index);
let children = direct_children(nodes, index, end);
if node.type_name == "map" {
return self.decode_map(nodes, &children, path);
}
if is_vector_container_node(node) {
let array_children = collection_array_children(nodes, index);
return self.decode_array(nodes, index, &array_children, path, false);
}
if is_array_node(node) {
return self.decode_array(nodes, index, &children, path, false);
}
if node.type_name.starts_with("PPtr<") || node.type_name == "PPtr" {
return self.decode_pptr();
}
if let Some(bits_child_index) = bitfield_bits_child_index(nodes, &children, node) {
return self.decode_bitfield(nodes, bits_child_index, path, node);
}
if let Some(value_child_index) = enum_value_child_index(nodes, &children) {
return self.decode_enum(nodes, value_child_index, path, node);
}
if is_managed_reference_registry_node(node) {
return self.decode_managed_reference_registry(nodes, &children, path);
}
if is_managed_reference_node(node) {
return self.decode_managed_reference(nodes, &children, path, node);
}
if children.is_empty() {
if let Some(component_count) = unity_float32_struct_component_count(&node.type_name) {
return self.decode_float32_struct(path, node, component_count);
}
if let Some(component_count) = unity_int32_struct_component_count(&node.type_name) {
return self.decode_int32_struct(path, node, component_count);
}
if let Some(byte_size) = unity_fixed_bytes_size(&node.type_name) {
return self.decode_fixed_bytes(path, node, byte_size);
}
}
match node.type_name.as_str() {
"bool" => Ok(UnitySerializedValue::Bool(self.reader.read_u8(&path)? != 0)),
"char" | "SInt8" => Ok(UnitySerializedValue::Signed(
self.reader.read_i8(&path)? as i64
)),
"UInt8" | "byte" => Ok(UnitySerializedValue::Unsigned(u64::from(
self.reader.read_u8(&path)?,
))),
"short" | "SInt16" => Ok(UnitySerializedValue::Signed(i64::from(
self.reader.read_i16(&path)?,
))),
"UInt16" | "unsigned short" => Ok(UnitySerializedValue::Unsigned(u64::from(
self.reader.read_u16(&path)?,
))),
"int" | "SInt32" => Ok(UnitySerializedValue::Signed(i64::from(
self.reader.read_i32(&path)?,
))),
"UInt32" | "unsigned int" => Ok(UnitySerializedValue::Unsigned(u64::from(
self.reader.read_u32(&path)?,
))),
"long long" | "SInt64" => {
Ok(UnitySerializedValue::Signed(self.reader.read_i64(&path)?))
}
"UInt64" | "unsigned long long" => {
Ok(UnitySerializedValue::Unsigned(self.reader.read_u64(&path)?))
}
"float" => Ok(UnitySerializedValue::Float32(self.reader.read_u32(&path)?)),
"double" => Ok(UnitySerializedValue::Float64(self.reader.read_u64(&path)?)),
"string" => Ok(UnitySerializedValue::String(
self.reader.read_aligned_string(&path)?,
)),
"TypelessData" | "bytes" => {
let size = self.reader.read_u32(&path)? as usize;
Ok(UnitySerializedValue::Bytes(
self.reader.read_bytes(size, &path)?.to_vec(),
))
}
_ if !children.is_empty() => {
let mut fields = Vec::with_capacity(children.len());
for child_index in children {
let child = &nodes[child_index];
let child_path = child_path(&path, &child.name);
fields.push(self.decode_node(nodes, child_index, child_path)?);
}
Ok(UnitySerializedValue::Object(fields))
}
_ if node.byte_size >= 0 => {
let size = usize::try_from(node.byte_size).map_err(|_| {
AssetBundleError::parse_field(
&path,
self.reader.offset(),
"TypeTree byte_size does not fit usize",
)
})?;
Ok(UnitySerializedValue::Unknown {
type_name: node.type_name.clone(),
bytes: self.reader.read_bytes(size, &path)?.to_vec(),
})
}
_ => Err(AssetBundleError::parse_field(
&path,
self.reader.offset(),
format!("unsupported TypeTree node type {}", node.type_name),
)),
}
}
fn decode_float32_struct(
&mut self,
path: String,
node: &UnityTypeTreeNode,
component_count: usize,
) -> Result<UnitySerializedValue> {
validate_fixed_leaf_byte_size(&path, self.reader.offset(), node, component_count * 4)?;
let mut values = Vec::with_capacity(component_count);
for _ in 0..component_count {
values.push(self.reader.read_u32(&path)?);
}
Ok(UnitySerializedValue::Float32Struct {
type_name: node.type_name.clone(),
values,
})
}
fn decode_int32_struct(
&mut self,
path: String,
node: &UnityTypeTreeNode,
component_count: usize,
) -> Result<UnitySerializedValue> {
validate_fixed_leaf_byte_size(&path, self.reader.offset(), node, component_count * 4)?;
let mut values = Vec::with_capacity(component_count);
for _ in 0..component_count {
values.push(self.reader.read_i32(&path)?);
}
Ok(UnitySerializedValue::Int32Struct {
type_name: node.type_name.clone(),
values,
})
}
fn decode_fixed_bytes(
&mut self,
path: String,
node: &UnityTypeTreeNode,
byte_size: usize,
) -> Result<UnitySerializedValue> {
validate_fixed_leaf_byte_size(&path, self.reader.offset(), node, byte_size)?;
Ok(UnitySerializedValue::FixedBytes {
type_name: node.type_name.clone(),
bytes: self.reader.read_bytes(byte_size, &path)?.to_vec(),
})
}
fn decode_array(
&mut self,
nodes: &[UnityTypeTreeNode],
array_index: usize,
children: &[usize],
path: String,
is_map: bool,
) -> Result<UnitySerializedValue> {
let size = self.reader.read_i32(&path)?;
if size < 0 {
return Err(AssetBundleError::parse_field(
&path,
self.reader.offset().saturating_sub(4),
format!("negative array size {size}"),
));
}
let size = usize::try_from(size).map_err(|_| {
AssetBundleError::parse_field(&path, self.reader.offset(), "array size overflow")
})?;
if size > MAX_COLLECTION_ITEMS {
return Err(AssetBundleError::parse_field(
&path,
self.reader.offset().saturating_sub(4),
format!("array size {size} exceeds limit {MAX_COLLECTION_ITEMS}"),
));
}
let data_index = children
.iter()
.copied()
.find(|index| nodes[*index].name == "data")
.or_else(|| children.last().copied())
.ok_or_else(|| {
AssetBundleError::parse_field(
&path,
self.reader.offset(),
"array/map node has no data child",
)
})?;
let mut values = Vec::with_capacity(size);
for index in 0..size {
let item_path = format!("{path}[{index}]");
let mut field = self.decode_node(nodes, data_index, item_path)?;
field.name = collection_item_name(&nodes[array_index], index);
values.push(field);
}
Ok(if is_map {
UnitySerializedValue::Map(values)
} else {
UnitySerializedValue::Array(values)
})
}
fn decode_map(
&mut self,
nodes: &[UnityTypeTreeNode],
children: &[usize],
path: String,
) -> Result<UnitySerializedValue> {
let array_index = children
.iter()
.copied()
.find(|index| is_array_node(&nodes[*index]) || nodes[*index].name == "Array");
if let Some(array_index) = array_index {
let array_children = collection_array_children(nodes, array_index);
return self.decode_array(nodes, array_index, &array_children, path, true);
}
let fallback_index = children.first().copied().unwrap_or(0);
self.decode_array(nodes, fallback_index, children, path, true)
}
fn decode_pptr(&mut self) -> Result<UnitySerializedValue> {
let file_id = self.reader.read_i32("PPtr.file_id")?;
let path_id = if self.serialized_version >= 14 {
self.reader.read_i64("PPtr.path_id")?
} else {
i64::from(self.reader.read_i32("PPtr.path_id")?)
};
Ok(UnitySerializedValue::PPtr { file_id, path_id })
}
fn decode_enum(
&mut self,
nodes: &[UnityTypeTreeNode],
value_child_index: usize,
path: String,
node: &UnityTypeTreeNode,
) -> Result<UnitySerializedValue> {
let child = &nodes[value_child_index];
let value_field =
self.decode_node(nodes, value_child_index, child_path(&path, &child.name))?;
let value = match value_field.value {
UnitySerializedValue::Signed(value) => value,
UnitySerializedValue::Unsigned(value) => i64::try_from(value).map_err(|_| {
AssetBundleError::parse_field(
&value_field.path,
value_field.offset,
format!("enum value {value} exceeds i64 range"),
)
})?,
value => {
return Err(AssetBundleError::parse_field(
&value_field.path,
value_field.offset,
format!("enum backing field decoded as non-integer value {value:?}"),
));
}
};
Ok(UnitySerializedValue::Enum {
type_name: node.type_name.clone(),
storage_type: value_field.type_name,
value,
})
}
fn decode_bitfield(
&mut self,
nodes: &[UnityTypeTreeNode],
bits_child_index: usize,
path: String,
node: &UnityTypeTreeNode,
) -> Result<UnitySerializedValue> {
let child = &nodes[bits_child_index];
let bits_field =
self.decode_node(nodes, bits_child_index, child_path(&path, &child.name))?;
let bits = match bits_field.value {
UnitySerializedValue::Signed(value) => value,
UnitySerializedValue::Unsigned(value) => i64::try_from(value).map_err(|_| {
AssetBundleError::parse_field(
&bits_field.path,
bits_field.offset,
format!("bit field value {value} exceeds i64 range"),
)
})?,
value => {
return Err(AssetBundleError::parse_field(
&bits_field.path,
bits_field.offset,
format!("bit field backing field decoded as non-integer value {value:?}"),
));
}
};
Ok(UnitySerializedValue::BitField {
type_name: node.type_name.clone(),
storage_type: bits_field.type_name,
bits,
})
}
fn decode_managed_reference(
&mut self,
nodes: &[UnityTypeTreeNode],
children: &[usize],
path: String,
node: &UnityTypeTreeNode,
) -> Result<UnitySerializedValue> {
if !children.is_empty() {
let mut fields = Vec::with_capacity(children.len());
for child_index in children {
let child = &nodes[*child_index];
let child_path = child_path(&path, &child.name);
fields.push(self.decode_node(nodes, *child_index, child_path)?);
}
return Ok(UnitySerializedValue::ManagedReference {
type_name: node.type_name.clone(),
metadata: managed_reference_metadata_from_fields(&fields),
fields,
bytes: Vec::new(),
});
}
if node.byte_size >= 0 {
let size = usize::try_from(node.byte_size).map_err(|_| {
AssetBundleError::parse_field(
&path,
self.reader.offset(),
"managed reference byte_size does not fit usize",
)
})?;
return Ok(UnitySerializedValue::ManagedReference {
type_name: node.type_name.clone(),
metadata: None,
fields: Vec::new(),
bytes: self.reader.read_bytes(size, &path)?.to_vec(),
});
}
Err(AssetBundleError::parse_field(
&path,
self.reader.offset(),
format!(
"managed reference TypeTree node {}.{} has no decodable children or fixed byte_size",
node.type_name, node.name
),
))
}
fn decode_managed_reference_registry(
&mut self,
nodes: &[UnityTypeTreeNode],
children: &[usize],
path: String,
) -> Result<UnitySerializedValue> {
let mut fields = Vec::with_capacity(children.len());
for child_index in children {
let child = &nodes[*child_index];
let child_path = child_path(&path, &child.name);
fields.push(self.decode_node(nodes, *child_index, child_path)?);
}
Ok(UnitySerializedValue::ManagedReferenceRegistry {
references: managed_reference_records_from_fields(&fields),
fields,
})
}
}
const TYPE_TREE_ALIGN_BYTES: i32 = 0x4000;
const MAX_COLLECTION_ITEMS: usize = 1_000_000;
fn node_end(nodes: &[UnityTypeTreeNode], index: usize) -> usize {
let level = nodes[index].level;
nodes
.iter()
.enumerate()
.skip(index + 1)
.find(|(_, node)| node.level <= level)
.map(|(index, _)| index)
.unwrap_or(nodes.len())
}
fn direct_children(nodes: &[UnityTypeTreeNode], index: usize, end: usize) -> Vec<usize> {
let level = nodes[index].level.saturating_add(1);
(index + 1..end)
.filter(|child_index| nodes[*child_index].level == level)
.collect()
}
fn is_array_node(node: &UnityTypeTreeNode) -> bool {
node.type_name == "Array" || is_collection_container_type(&node.type_name) || node.is_array
}
fn is_vector_container_node(node: &UnityTypeTreeNode) -> bool {
is_collection_container_type(&node.type_name)
}
fn is_collection_container_type(type_name: &str) -> bool {
let key = normalized_metadata_key(type_name);
let lower = type_name.trim().to_ascii_lowercase();
key == "vector"
|| key == "staticvector"
|| key == "list"
|| key == "hashset"
|| lower.starts_with("list<")
|| lower.starts_with("hashset<")
|| lower.starts_with("set<")
|| lower.starts_with("system.collections.generic.list")
|| lower.starts_with("system.collections.generic.hashset")
}
fn unity_array_child_index(nodes: &[UnityTypeTreeNode], children: &[usize]) -> Option<usize> {
children
.iter()
.copied()
.find(|index| nodes[*index].type_name == "Array" || nodes[*index].name == "Array")
}
fn collection_array_children(nodes: &[UnityTypeTreeNode], index: usize) -> Vec<usize> {
let end = node_end(nodes, index);
let children = direct_children(nodes, index, end);
if let Some(array_index) = unity_array_child_index(nodes, &children) {
let array_end = node_end(nodes, array_index);
direct_children(nodes, array_index, array_end)
} else {
children
}
}
fn unity_float32_struct_component_count(type_name: &str) -> Option<usize> {
match normalized_metadata_key(type_name).as_str() {
"vector2" | "vector2f" => Some(2),
"vector3" | "vector3f" => Some(3),
"vector4" | "vector4f" | "quaternion" | "quaternionf" | "colorrgba" | "color" | "rect"
| "rectf" => Some(4),
"aabb" | "bounds" | "ray" | "rayf" => Some(6),
"matrix4x4" | "matrix4x4f" => Some(16),
_ => None,
}
}
fn unity_int32_struct_component_count(type_name: &str) -> Option<usize> {
match normalized_metadata_key(type_name).as_str() {
"rangeint" | "vector2int" => Some(2),
"vector3int" => Some(3),
"rectint" => Some(4),
"boundsint" => Some(6),
_ => None,
}
}
fn unity_fixed_bytes_size(type_name: &str) -> Option<usize> {
match normalized_metadata_key(type_name).as_str() {
"guid" | "hash128" => Some(16),
_ => None,
}
}
fn enum_value_child_index(nodes: &[UnityTypeTreeNode], children: &[usize]) -> Option<usize> {
if children.len() != 1 {
return None;
}
let child_index = children[0];
let child = &nodes[child_index];
let name_key = normalized_metadata_key(&child.name);
(name_key == "value" && enum_integer_storage_kind(&child.type_name).is_some())
.then_some(child_index)
}
fn bitfield_bits_child_index(
nodes: &[UnityTypeTreeNode],
children: &[usize],
node: &UnityTypeTreeNode,
) -> Option<usize> {
if !is_bitfield_container_type(&node.type_name) || children.len() != 1 {
return None;
}
let child_index = children[0];
let child = &nodes[child_index];
let name_key = normalized_metadata_key(&child.name);
(name_key == "bits" && enum_integer_storage_kind(&child.type_name).is_some())
.then_some(child_index)
}
fn is_bitfield_container_type(type_name: &str) -> bool {
matches!(
normalized_metadata_key(type_name).as_str(),
"layermask" | "bitfield"
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EnumIntegerStorageKind {
Signed,
Unsigned,
}
fn enum_integer_storage_kind(type_name: &str) -> Option<EnumIntegerStorageKind> {
match type_name {
"char" | "SInt8" | "short" | "SInt16" | "int" | "SInt32" | "long long" | "SInt64" => {
Some(EnumIntegerStorageKind::Signed)
}
"UInt8" | "byte" | "UInt16" | "unsigned short" | "UInt32" | "unsigned int" | "UInt64"
| "unsigned long long" => Some(EnumIntegerStorageKind::Unsigned),
_ => None,
}
}
fn validate_fixed_leaf_byte_size(
path: &str,
offset: usize,
node: &UnityTypeTreeNode,
expected_size: usize,
) -> Result<()> {
if node.byte_size < 0 {
return Ok(());
}
let actual_size = usize::try_from(node.byte_size).map_err(|_| {
AssetBundleError::parse_field(path, offset, "TypeTree byte_size does not fit usize")
})?;
if actual_size != expected_size {
return Err(AssetBundleError::parse_field(
path,
offset,
format!(
"TypeTree node {}.{} byte_size {} does not match expected fixed leaf size {}",
node.type_name, node.name, actual_size, expected_size
),
));
}
Ok(())
}
fn is_managed_reference_registry_node(node: &UnityTypeTreeNode) -> bool {
let type_key = normalized_metadata_key(&node.type_name);
let name_key = normalized_metadata_key(&node.name);
matches!(
type_key.as_str(),
"managedreferencesregistry"
| "managedreferenceregistry"
| "serializedreferences"
| "serializedreferenceregistry"
| "serializedreferencesregistry"
) || matches!(
name_key.as_str(),
"managedreferencesregistry"
| "managedreferenceregistry"
| "managedreferences"
| "serializedreferences"
| "serializedreferenceregistry"
| "serializedreferencesregistry"
)
}
fn is_managed_reference_node(node: &UnityTypeTreeNode) -> bool {
let type_key = normalized_metadata_key(&node.type_name);
let name_key = normalized_metadata_key(&node.name);
matches!(
type_key.as_str(),
"managedreference"
| "managedreferenceentry"
| "managedreferencedata"
| "serializedreference"
| "serializedreferenceentry"
| "serializedreferencedata"
| "referencedata"
) || matches!(
name_key.as_str(),
"managedreference"
| "managedreferenceentry"
| "managedreferencedata"
| "serializedreference"
| "serializedreferenceentry"
| "serializedreferencedata"
| "referencedata"
)
}
fn managed_reference_records_from_fields(
fields: &[UnitySerializedField],
) -> Vec<UnityManagedReferenceRecord> {
let mut records = Vec::new();
collect_managed_reference_records(fields, &mut records);
records
}
fn collect_managed_reference_records(
fields: &[UnitySerializedField],
records: &mut Vec<UnityManagedReferenceRecord>,
) {
for field in fields {
match &field.value {
UnitySerializedValue::Array(items) | UnitySerializedValue::Map(items) => {
for item in items {
if let Some(record) = managed_reference_record_from_field(item) {
records.push(record);
} else if let Some(children) = serialized_field_children(item) {
collect_managed_reference_records(children, records);
}
}
}
UnitySerializedValue::Object(children)
| UnitySerializedValue::ManagedReference {
fields: children, ..
}
| UnitySerializedValue::ManagedReferenceRegistry {
fields: children, ..
} => {
collect_managed_reference_records(children, records);
}
_ => {}
}
}
}
fn managed_reference_record_from_field(
field: &UnitySerializedField,
) -> Option<UnityManagedReferenceRecord> {
let children = serialized_field_children(field)?;
let metadata = managed_reference_metadata_from_fields(children)?;
let fields = managed_reference_payload_fields(children);
Some(UnityManagedReferenceRecord { metadata, fields })
}
pub(crate) fn managed_reference_metadata_from_fields(
fields: &[UnitySerializedField],
) -> Option<UnityManagedReferenceMetadata> {
let mut metadata = UnityManagedReferenceMetadata {
reference_id: None,
full_type_name: None,
type_name: None,
namespace: None,
assembly_name: None,
};
collect_managed_reference_metadata(fields, &mut metadata);
(!metadata.is_empty()).then_some(metadata)
}
fn collect_managed_reference_metadata(
fields: &[UnitySerializedField],
metadata: &mut UnityManagedReferenceMetadata,
) {
for field in fields {
let key = normalized_metadata_key(&field.name);
match &field.value {
UnitySerializedValue::Signed(value) if is_reference_id_key(&key) => {
metadata.reference_id.get_or_insert(*value);
}
UnitySerializedValue::Unsigned(value) if is_reference_id_key(&key) => {
if let Ok(value) = i64::try_from(*value) {
metadata.reference_id.get_or_insert(value);
}
}
UnitySerializedValue::String(value) if is_type_name_key(&key) => {
collect_managed_reference_type_name(&key, value, metadata);
}
UnitySerializedValue::String(value) if is_namespace_key(&key) => {
if !value.is_empty() {
metadata.namespace.get_or_insert_with(|| value.clone());
}
}
UnitySerializedValue::String(value) if is_assembly_key(&key) => {
if !value.is_empty() {
metadata.assembly_name.get_or_insert_with(|| value.clone());
}
}
UnitySerializedValue::Object(children)
| UnitySerializedValue::ManagedReference {
fields: children, ..
}
| UnitySerializedValue::ManagedReferenceRegistry {
fields: children, ..
} if !is_payload_data_key(&key) => {
collect_managed_reference_metadata(children, metadata);
}
UnitySerializedValue::Array(items) | UnitySerializedValue::Map(items)
if !is_payload_data_key(&key) =>
{
collect_managed_reference_metadata(items, metadata);
}
_ => {}
}
}
}
fn collect_managed_reference_type_name(
key: &str,
value: &str,
metadata: &mut UnityManagedReferenceMetadata,
) {
let trimmed = value.trim();
if trimmed.is_empty() {
return;
}
let parsed = parse_managed_reference_type_name(trimmed);
if is_full_type_name_key(key)
|| parsed.namespace.as_ref().is_some()
|| parsed.assembly_name.as_ref().is_some()
{
metadata
.full_type_name
.get_or_insert_with(|| trimmed.to_string());
}
if let Some(type_name) = parsed.type_name {
metadata.type_name.get_or_insert(type_name);
}
if let Some(namespace) = parsed.namespace {
metadata.namespace.get_or_insert(namespace);
}
if let Some(assembly_name) = parsed.assembly_name {
metadata.assembly_name.get_or_insert(assembly_name);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedManagedReferenceTypeName {
type_name: Option<String>,
namespace: Option<String>,
assembly_name: Option<String>,
}
fn parse_managed_reference_type_name(value: &str) -> ParsedManagedReferenceTypeName {
let mut type_part = value.trim();
let mut assembly_name = None;
if let Some((type_name, assembly)) = type_part.split_once(',') {
type_part = type_name.trim();
let assembly = assembly
.split(',')
.next()
.map(str::trim)
.filter(|assembly| !assembly.is_empty());
if let Some(assembly) = assembly {
assembly_name = Some(assembly.to_string());
}
} else if let Some((assembly, type_name)) = type_part.split_once("::") {
let assembly = assembly.trim();
let type_name = type_name.trim();
if !assembly.is_empty() && !type_name.is_empty() {
assembly_name = Some(assembly.to_string());
type_part = type_name;
}
} else if let Some(space_index) = type_part.find(char::is_whitespace) {
let (assembly, type_name) = type_part.split_at(space_index);
let assembly = assembly.trim();
let type_name = type_name.trim();
if !assembly.is_empty() && !type_name.is_empty() {
assembly_name = Some(assembly.to_string());
type_part = type_name;
}
}
let (namespace, type_name) = split_namespace_and_type_name(type_part);
ParsedManagedReferenceTypeName {
type_name,
namespace,
assembly_name,
}
}
fn split_namespace_and_type_name(value: &str) -> (Option<String>, Option<String>) {
let value = value.trim();
if value.is_empty() {
return (None, None);
}
if let Some((namespace, type_name)) = value.rsplit_once('.') {
let namespace = (!namespace.is_empty()).then(|| namespace.to_string());
let type_name = (!type_name.is_empty()).then(|| type_name.to_string());
(namespace, type_name)
} else {
(None, Some(value.to_string()))
}
}
fn managed_reference_payload_fields(fields: &[UnitySerializedField]) -> Vec<UnitySerializedField> {
for field in fields {
if is_payload_data_key(&normalized_metadata_key(&field.name)) {
if let Some(children) = serialized_field_children(field) {
return children.to_vec();
}
}
}
fields
.iter()
.filter(|field| !is_managed_reference_metadata_field(field))
.cloned()
.collect()
}
fn serialized_field_children(field: &UnitySerializedField) -> Option<&[UnitySerializedField]> {
match &field.value {
UnitySerializedValue::Object(children)
| UnitySerializedValue::Array(children)
| UnitySerializedValue::Map(children)
| UnitySerializedValue::ManagedReference {
fields: children, ..
}
| UnitySerializedValue::ManagedReferenceRegistry {
fields: children, ..
} => Some(children),
_ => None,
}
}
fn is_managed_reference_metadata_field(field: &UnitySerializedField) -> bool {
let key = normalized_metadata_key(&field.name);
is_reference_id_key(&key)
|| is_type_name_key(&key)
|| is_namespace_key(&key)
|| is_assembly_key(&key)
}
fn normalized_metadata_key(name: &str) -> String {
name.strip_prefix("m_")
.unwrap_or(name)
.chars()
.filter(|character| character.is_ascii_alphanumeric())
.flat_map(char::to_lowercase)
.collect()
}
fn is_reference_id_key(key: &str) -> bool {
matches!(
key,
"rid"
| "id"
| "identifier"
| "refid"
| "refids"
| "referenceid"
| "managedreferenceid"
| "managedreferenceids"
| "managedreferencesid"
| "managedreferencesids"
| "serializedreferenceid"
| "serializedreferenceids"
)
}
fn is_type_name_key(key: &str) -> bool {
matches!(
key,
"type"
| "typeid"
| "typeinfo"
| "typename"
| "fullname"
| "fulltypename"
| "class"
| "classname"
| "managedreferenceclassname"
| "serializedreferenceclassname"
| "klass"
| "managedtype"
| "managedreferencetype"
| "managedreferencefullname"
| "managedreferencefulltypename"
| "serializedreferencetype"
| "serializedreferencefullname"
| "serializedreferencefulltypename"
| "assemblyqualifiedname"
)
}
fn is_full_type_name_key(key: &str) -> bool {
matches!(
key,
"managedreferencefullname"
| "managedreferencefulltypename"
| "assemblyqualifiedname"
| "managedtype"
| "managedreferencetype"
| "serializedreferencetype"
| "serializedreferencefullname"
| "serializedreferencefulltypename"
| "typename"
| "fullname"
| "fulltypename"
| "type"
| "typeid"
| "typeinfo"
)
}
fn is_namespace_key(key: &str) -> bool {
matches!(
key,
"ns" | "namespace"
| "namespacename"
| "managedreferencenamespace"
| "managedreferencenamespacename"
| "serializedreferencenamespace"
| "serializedreferencenamespacename"
)
}
fn is_assembly_key(key: &str) -> bool {
matches!(
key,
"asm"
| "asmname"
| "assembly"
| "assemblyname"
| "managedreferenceassembly"
| "managedreferenceassemblyname"
| "serializedreferenceassembly"
| "serializedreferenceassemblyname"
)
}
fn is_payload_data_key(key: &str) -> bool {
matches!(
key,
"data"
| "payload"
| "value"
| "object"
| "instance"
| "managedreferencepayload"
| "referencepayload"
| "serializedreferencepayload"
| "managedreferencevalue"
| "referencevalue"
| "serializedreferencevalue"
| "managedreferenceobject"
| "referenceobject"
| "serializedreferenceobject"
| "managedreferencedata"
| "referencedata"
| "serializeddata"
| "serializedreferencedata"
)
}
fn child_path(parent: &str, name: &str) -> String {
if parent.is_empty() {
name.to_string()
} else if name.is_empty() {
parent.to_string()
} else {
format!("{parent}.{name}")
}
}
fn collection_item_name(array_node: &UnityTypeTreeNode, index: usize) -> String {
if array_node.name.is_empty() || array_node.name == "Array" {
format!("[{index}]")
} else {
format!("{}[{index}]", array_node.name)
}
}
fn find_field_by_path<'a>(
fields: &'a [UnitySerializedField],
field_path: &str,
) -> Option<&'a UnitySerializedField> {
for field in fields {
if field.path == field_path {
return Some(field);
}
match &field.value {
UnitySerializedValue::Object(children)
| UnitySerializedValue::ManagedReference {
fields: children, ..
}
| UnitySerializedValue::ManagedReferenceRegistry {
fields: children, ..
} => {
if let Some(found) = find_field_by_path(children, field_path) {
return Some(found);
}
}
UnitySerializedValue::Array(values) | UnitySerializedValue::Map(values) => {
for value in values {
if value.path == field_path {
return Some(value);
}
match &value.value {
UnitySerializedValue::Object(children)
| UnitySerializedValue::ManagedReference {
fields: children, ..
}
| UnitySerializedValue::ManagedReferenceRegistry {
fields: children, ..
} => {
if let Some(found) = find_field_by_path(children, field_path) {
return Some(found);
}
}
UnitySerializedValue::Array(children)
| UnitySerializedValue::Map(children) => {
if let Some(found) = find_field_by_path(children, field_path) {
return Some(found);
}
}
_ => {}
}
}
}
_ => {}
}
}
None
}
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 encode_text_asset(name: &str, bytes: &[u8], endian: Endian) -> Result<Vec<u8>> {
let name = name.as_bytes();
let name_len = u32::try_from(name.len())
.map_err(|_| AssetBundleError::Parse("TextAsset name exceeds u32 length".to_string()))?;
let bytes_len = u32::try_from(bytes.len())
.map_err(|_| AssetBundleError::Parse("TextAsset bytes exceed u32 length".to_string()))?;
let mut output = Vec::with_capacity(name.len() + bytes.len() + 12);
push_u32_endian(&mut output, name_len, endian);
output.extend_from_slice(name);
let remainder = output.len() % 4;
if remainder != 0 {
output.resize(output.len() + 4 - remainder, 0);
}
push_u32_endian(&mut output, bytes_len, endian);
output.extend_from_slice(bytes);
Ok(output)
}
fn encode_aligned_string(value: &str, endian: Endian) -> Result<Vec<u8>> {
let bytes = value.as_bytes();
let len = u32::try_from(bytes.len())
.map_err(|_| AssetBundleError::Parse("string field exceeds u32 length".to_string()))?;
let mut output = Vec::with_capacity(bytes.len() + 8);
push_u32_endian(&mut output, len, endian);
output.extend_from_slice(bytes);
let remainder = output.len() % 4;
if remainder != 0 {
output.resize(output.len() + 4 - remainder, 0);
}
Ok(output)
}
fn encode_float32_struct_values(
type_name: &str,
values: &[u32],
endian: Endian,
) -> Result<Vec<u8>> {
let expected_count = unity_float32_struct_component_count(type_name).ok_or_else(|| {
AssetBundleError::UnsupportedFormat(format!(
"Unity fixed float32 struct type {type_name} is not supported"
))
})?;
if values.len() != expected_count {
return Err(AssetBundleError::Parse(format!(
"Unity fixed float32 struct {type_name} expects {expected_count} values, got {}",
values.len()
)));
}
let mut encoded = Vec::with_capacity(values.len() * 4);
for value in values {
push_u32_endian(&mut encoded, *value, endian);
}
Ok(encoded)
}
fn encode_int32_struct_values(type_name: &str, values: &[i32], endian: Endian) -> Result<Vec<u8>> {
let expected_count = unity_int32_struct_component_count(type_name).ok_or_else(|| {
AssetBundleError::UnsupportedFormat(format!(
"Unity fixed int32 struct type {type_name} is not supported"
))
})?;
if values.len() != expected_count {
return Err(AssetBundleError::Parse(format!(
"Unity fixed int32 struct {type_name} expects {expected_count} values, got {}",
values.len()
)));
}
let mut encoded = Vec::with_capacity(values.len() * 4);
for value in values {
push_i32_endian(&mut encoded, *value, endian);
}
Ok(encoded)
}
fn encode_fixed_bytes_value(type_name: &str, bytes: &[u8]) -> Result<Vec<u8>> {
let expected_size = unity_fixed_bytes_size(type_name).ok_or_else(|| {
AssetBundleError::UnsupportedFormat(format!(
"Unity fixed-byte struct type {type_name} is not supported"
))
})?;
if bytes.len() != expected_size {
return Err(AssetBundleError::Parse(format!(
"Unity fixed-byte struct {type_name} expects {expected_size} bytes, got {}",
bytes.len()
)));
}
Ok(bytes.to_vec())
}
fn encode_enum_integer(storage_type: &str, value: i64, endian: Endian) -> Result<Vec<u8>> {
match enum_integer_storage_kind(storage_type) {
Some(EnumIntegerStorageKind::Signed) => encode_signed_integer(storage_type, value, endian),
Some(EnumIntegerStorageKind::Unsigned) => {
let value = u64::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!(
"enum storage {storage_type} replacement {value} cannot be negative"
))
})?;
encode_unsigned_integer(storage_type, value, endian)
}
None => Err(AssetBundleError::UnsupportedFormat(format!(
"enum backing field type {storage_type} is not a supported integer"
))),
}
}
fn enum_child_replacement(
storage_type: &str,
value: i64,
) -> Result<UnitySerializedReplacementValue> {
match enum_integer_storage_kind(storage_type) {
Some(EnumIntegerStorageKind::Signed) => Ok(UnitySerializedReplacementValue::Signed(value)),
Some(EnumIntegerStorageKind::Unsigned) => {
let value = u64::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!(
"enum storage {storage_type} replacement {value} cannot be negative"
))
})?;
Ok(UnitySerializedReplacementValue::Unsigned(value))
}
None => Err(AssetBundleError::UnsupportedFormat(format!(
"enum backing field type {storage_type} is not a supported integer"
))),
}
}
fn encode_replacement_value(
replacement: &UnitySerializedReplacementValue,
current: &UnitySerializedValue,
type_name: &str,
field_byte_size: usize,
node_index: Option<usize>,
context: ReplacementEncodingContext<'_>,
) -> Result<Vec<u8>> {
match replacement {
UnitySerializedReplacementValue::Bool(value) => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::Bool(_)),
"bool",
)?;
pad_fixed_encoded_value(vec![u8::from(*value)], field_byte_size, type_name)
}
UnitySerializedReplacementValue::Signed(value) => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::Signed(_)),
"signed integer",
)?;
let encoded = encode_signed_integer(type_name, *value, context.endian)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::Unsigned(value) => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::Unsigned(_)),
"unsigned integer",
)?;
let encoded = encode_unsigned_integer(type_name, *value, context.endian)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::Float32(value) => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::Float32(_)),
"float32",
)?;
pad_fixed_encoded_value(
encode_u32_value(*value, context.endian),
field_byte_size,
type_name,
)
}
UnitySerializedReplacementValue::Float64(value) => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::Float64(_)),
"float64",
)?;
pad_fixed_encoded_value(
encode_u64_value(*value, context.endian),
field_byte_size,
type_name,
)
}
UnitySerializedReplacementValue::String(value) => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::String(_)),
"string",
)?;
encode_aligned_string(value, context.endian)
}
UnitySerializedReplacementValue::Bytes(value) => match current {
UnitySerializedValue::Bytes(current_bytes) => {
let value_len = u32::try_from(value.len()).map_err(|_| {
AssetBundleError::Parse(format!("bytes field {type_name} exceeds u32 length"))
})?;
let mut encoded = Vec::with_capacity(value.len() + 8);
push_u32_endian(&mut encoded, value_len, context.endian);
encoded.extend_from_slice(value);
if field_byte_size > 4usize.saturating_add(current_bytes.len()) {
align_vec_to(&mut encoded, 4);
}
Ok(encoded)
}
UnitySerializedValue::Unknown {
type_name: unknown_type,
bytes: current_bytes,
} => {
if value.len() != current_bytes.len() {
return Err(AssetBundleError::Parse(format!(
"unknown fixed field {unknown_type} replacement length {} must match current byte length {}",
value.len(),
current_bytes.len()
)));
}
pad_fixed_encoded_value(value.clone(), field_byte_size, unknown_type)
}
_ => Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not TypelessData/bytes or fixed-size unknown bytes"
))),
},
UnitySerializedReplacementValue::Float32Struct {
type_name: replacement_type,
values,
} => {
let Some(current_type) = current_float32_struct_type_name(current, type_name) else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not a fixed float32 Unity struct"
)));
};
if normalized_metadata_key(replacement_type) != normalized_metadata_key(current_type) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement type {} does not match current fixed float32 struct {}",
replacement_type, current_type
)));
}
let encoded = encode_float32_struct_values(current_type, values, context.endian)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::Int32Struct {
type_name: replacement_type,
values,
} => {
let Some(current_type) = current_int32_struct_type_name(current, type_name) else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not a fixed int32 Unity struct"
)));
};
if normalized_metadata_key(replacement_type) != normalized_metadata_key(current_type) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement type {} does not match current fixed int32 struct {}",
replacement_type, current_type
)));
}
let encoded = encode_int32_struct_values(current_type, values, context.endian)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::FixedBytes {
type_name: replacement_type,
bytes,
} => {
let Some(current_type) = current_fixed_bytes_type_name(current, type_name) else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not a fixed-byte Unity struct"
)));
};
if normalized_metadata_key(replacement_type) != normalized_metadata_key(current_type) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement type {} does not match current fixed-byte struct {}",
replacement_type, current_type
)));
}
let encoded = encode_fixed_bytes_value(current_type, bytes)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::Enum {
type_name: replacement_type,
storage_type,
value,
} => {
let UnitySerializedValue::Enum {
type_name: current_type,
storage_type: current_storage,
..
} = current
else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not an enum"
)));
};
if normalized_metadata_key(replacement_type) != normalized_metadata_key(current_type) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement enum type {} does not match current enum {}",
replacement_type, current_type
)));
}
if normalized_metadata_key(storage_type) != normalized_metadata_key(current_storage) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement enum storage {} does not match current enum storage {}",
storage_type, current_storage
)));
}
let encoded = encode_enum_integer(current_storage, *value, context.endian)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::BitField {
type_name: replacement_type,
storage_type,
bits,
} => {
let UnitySerializedValue::BitField {
type_name: current_type,
storage_type: current_storage,
..
} = current
else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not a bit field"
)));
};
if normalized_metadata_key(replacement_type) != normalized_metadata_key(current_type) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement bit field type {} does not match current bit field {}",
replacement_type, current_type
)));
}
if normalized_metadata_key(storage_type) != normalized_metadata_key(current_storage) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement bit field storage {} does not match current bit field storage {}",
storage_type, current_storage
)));
}
let encoded = encode_enum_integer(current_storage, *bits, context.endian)?;
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::PPtr { file_id, path_id } => {
require_current_kind(
current,
matches!(current, UnitySerializedValue::PPtr { .. }),
"PPtr",
)?;
let mut encoded = Vec::with_capacity(16);
push_i32_endian(&mut encoded, *file_id, context.endian);
if context.serialized_version >= 14 {
push_i64_endian(&mut encoded, *path_id, context.endian);
} else {
let path_id = i32::try_from(*path_id).map_err(|_| {
AssetBundleError::Parse(format!(
"PPtr path_id {} exceeds legacy i32 range",
path_id
))
})?;
push_i32_endian(&mut encoded, path_id, context.endian);
}
pad_fixed_encoded_value(encoded, field_byte_size, type_name)
}
UnitySerializedReplacementValue::Array(items) => {
encode_collection_replacement(items, current, "array", node_index, context)
}
UnitySerializedReplacementValue::Map(items) => {
encode_collection_replacement(items, current, "map", node_index, context)
}
UnitySerializedReplacementValue::Object(fields) => {
encode_object_replacement(fields, current, context)
}
}
}
#[derive(Clone, Copy)]
struct ReplacementEncodingContext<'a> {
serialized_version: u32,
endian: Endian,
nodes: Option<&'a [UnityTypeTreeNode]>,
}
fn current_float32_struct_type_name<'a>(
current: &'a UnitySerializedValue,
field_type_name: &'a str,
) -> Option<&'a str> {
match current {
UnitySerializedValue::Float32Struct { type_name, .. } => Some(type_name),
UnitySerializedValue::Object(_)
if object_float32_struct_values(current, field_type_name).is_some() =>
{
Some(field_type_name)
}
_ => None,
}
}
fn current_int32_struct_type_name<'a>(
current: &'a UnitySerializedValue,
field_type_name: &'a str,
) -> Option<&'a str> {
match current {
UnitySerializedValue::Int32Struct { type_name, .. } => Some(type_name),
UnitySerializedValue::Object(_)
if object_int32_struct_values(current, field_type_name).is_some() =>
{
Some(field_type_name)
}
_ => None,
}
}
fn current_fixed_bytes_type_name<'a>(
current: &'a UnitySerializedValue,
field_type_name: &'a str,
) -> Option<&'a str> {
match current {
UnitySerializedValue::FixedBytes { type_name, .. } => Some(type_name),
UnitySerializedValue::Object(_)
if object_fixed_bytes_value(current, field_type_name).is_some() =>
{
Some(field_type_name)
}
_ => None,
}
}
fn encode_collection_replacement(
replacements: &[UnitySerializedReplacementValue],
current: &UnitySerializedValue,
collection_kind: &str,
node_index: Option<usize>,
context: ReplacementEncodingContext<'_>,
) -> Result<Vec<u8>> {
let current_items = match (collection_kind, current) {
("array", UnitySerializedValue::Array(items))
| ("map", UnitySerializedValue::Map(items)) => items,
_ => {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field value {:?} is not {collection_kind}",
current
)));
}
};
let len = i32::try_from(replacements.len()).map_err(|_| {
AssetBundleError::Parse(format!(
"{collection_kind} replacement length {} exceeds i32 range",
replacements.len()
))
})?;
if replacements.len() > MAX_COLLECTION_ITEMS {
return Err(AssetBundleError::Parse(format!(
"{collection_kind} replacement length {} exceeds limit {MAX_COLLECTION_ITEMS}",
replacements.len()
)));
}
if replacements.is_empty() {
let mut output = Vec::with_capacity(4);
push_i32_endian(&mut output, len, context.endian);
return Ok(output);
}
let mut output = Vec::with_capacity(
4usize.saturating_add(
current_items
.first()
.map(|template| template.byte_size)
.unwrap_or(0)
.saturating_mul(replacements.len()),
),
);
push_i32_endian(&mut output, len, context.endian);
if let Some(template) = current_items.first() {
for (index, replacement) in replacements.iter().enumerate() {
let current_item = current_items.get(index).unwrap_or(template);
output.extend_from_slice(&encode_replacement_value(
replacement,
&current_item.value,
&current_item.type_name,
current_item.byte_size,
current_item.type_tree_node_index,
context,
)?);
}
} else {
let nodes = context.nodes.ok_or_else(|| {
AssetBundleError::UnsupportedFormat(format!(
"cannot encode non-empty replacement for an empty TypeTree {collection_kind} without TypeTree nodes"
))
})?;
let node_index = node_index.ok_or_else(|| {
AssetBundleError::UnsupportedFormat(format!(
"cannot encode non-empty replacement for an empty TypeTree {collection_kind} without TypeTree node index"
))
})?;
let data_index = collection_data_node_index(nodes, node_index, collection_kind)?;
for replacement in replacements {
output.extend_from_slice(&encode_replacement_node(
replacement,
nodes,
data_index,
context.serialized_version,
context.endian,
)?);
}
}
Ok(output)
}
fn encode_replacement_node(
replacement: &UnitySerializedReplacementValue,
nodes: &[UnityTypeTreeNode],
index: usize,
serialized_version: u32,
endian: Endian,
) -> Result<Vec<u8>> {
let node = nodes.get(index).ok_or_else(|| {
AssetBundleError::Parse(format!("TypeTree node index {index} is out of range"))
})?;
let mut encoded =
encode_replacement_node_value(replacement, nodes, index, node, serialized_version, endian)?;
if node.meta_flag & TYPE_TREE_ALIGN_BYTES != 0 {
align_vec_to(&mut encoded, 4);
}
Ok(encoded)
}
fn encode_replacement_node_value(
replacement: &UnitySerializedReplacementValue,
nodes: &[UnityTypeTreeNode],
index: usize,
node: &UnityTypeTreeNode,
serialized_version: u32,
endian: Endian,
) -> Result<Vec<u8>> {
if node.type_name == "map" {
let UnitySerializedReplacementValue::Map(items) = replacement else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement {:?} is not map for TypeTree node {}",
replacement, node.name
)));
};
return encode_collection_replacement_from_schema(
items,
nodes,
collection_data_node_index(nodes, index, "map")?,
serialized_version,
endian,
"map",
);
}
if is_array_node(node) {
let UnitySerializedReplacementValue::Array(items) = replacement else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement {:?} is not array for TypeTree node {}",
replacement, node.name
)));
};
return encode_collection_replacement_from_schema(
items,
nodes,
collection_data_node_index(nodes, index, "array")?,
serialized_version,
endian,
"array",
);
}
let end = node_end(nodes, index);
let children = direct_children(nodes, index, end);
if let Some(bits_child_index) = bitfield_bits_child_index(nodes, &children, node) {
let UnitySerializedReplacementValue::BitField {
type_name,
storage_type,
bits,
} = replacement
else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement {:?} is not bit field for TypeTree node {}.{}",
replacement, node.type_name, node.name
)));
};
if normalized_metadata_key(type_name) != normalized_metadata_key(&node.type_name) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement bit field type {} does not match TypeTree bit field {}",
type_name, node.type_name
)));
}
let child = &nodes[bits_child_index];
if normalized_metadata_key(storage_type) != normalized_metadata_key(&child.type_name) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement bit field storage {} does not match TypeTree bit field storage {}",
storage_type, child.type_name
)));
}
let encoded = encode_replacement_node(
&enum_child_replacement(&child.type_name, *bits)?,
nodes,
bits_child_index,
serialized_version,
endian,
)?;
return pad_schema_encoded_value(encoded, node);
}
if let Some(value_child_index) = enum_value_child_index(nodes, &children) {
let UnitySerializedReplacementValue::Enum {
type_name,
storage_type,
value,
} = replacement
else {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement {:?} is not enum for TypeTree node {}.{}",
replacement, node.type_name, node.name
)));
};
if normalized_metadata_key(type_name) != normalized_metadata_key(&node.type_name) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement enum type {} does not match TypeTree enum {}",
type_name, node.type_name
)));
}
let child = &nodes[value_child_index];
if normalized_metadata_key(storage_type) != normalized_metadata_key(&child.type_name) {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement enum storage {} does not match TypeTree enum storage {}",
storage_type, child.type_name
)));
}
let encoded = encode_replacement_node(
&enum_child_replacement(&child.type_name, *value)?,
nodes,
value_child_index,
serialized_version,
endian,
)?;
return pad_schema_encoded_value(encoded, node);
}
let encoded = match replacement {
UnitySerializedReplacementValue::Bool(value) if node.type_name == "bool" => {
vec![u8::from(*value)]
}
UnitySerializedReplacementValue::Signed(value) => {
encode_signed_integer(&node.type_name, *value, endian)?
}
UnitySerializedReplacementValue::Unsigned(value) => {
encode_unsigned_integer(&node.type_name, *value, endian)?
}
UnitySerializedReplacementValue::Float32(value) if node.type_name == "float" => {
encode_u32_value(*value, endian)
}
UnitySerializedReplacementValue::Float64(value) if node.type_name == "double" => {
encode_u64_value(*value, endian)
}
UnitySerializedReplacementValue::String(value) if node.type_name == "string" => {
encode_aligned_string(value, endian)?
}
UnitySerializedReplacementValue::Bytes(value)
if matches!(node.type_name.as_str(), "TypelessData" | "bytes") =>
{
let value_len = u32::try_from(value.len()).map_err(|_| {
AssetBundleError::Parse(format!("bytes field {} exceeds u32 length", node.name))
})?;
let mut encoded = Vec::with_capacity(value.len() + 4);
push_u32_endian(&mut encoded, value_len, endian);
encoded.extend_from_slice(value);
encoded
}
UnitySerializedReplacementValue::Bytes(value)
if node.byte_size >= 0
&& direct_children(nodes, index, node_end(nodes, index)).is_empty() =>
{
let field_byte_size = usize::try_from(node.byte_size).map_err(|_| {
AssetBundleError::Parse(format!(
"TypeTree node {} byte_size does not fit usize",
node.name
))
})?;
if value.len() != field_byte_size {
return Err(AssetBundleError::Parse(format!(
"unknown fixed field {} replacement length {} must match TypeTree byte_size {}",
node.name,
value.len(),
field_byte_size
)));
}
value.clone()
}
UnitySerializedReplacementValue::Float32Struct { type_name, values }
if normalized_metadata_key(type_name) == normalized_metadata_key(&node.type_name) =>
{
encode_float32_struct_values(&node.type_name, values, endian)?
}
UnitySerializedReplacementValue::Int32Struct { type_name, values }
if normalized_metadata_key(type_name) == normalized_metadata_key(&node.type_name) =>
{
encode_int32_struct_values(&node.type_name, values, endian)?
}
UnitySerializedReplacementValue::FixedBytes { type_name, bytes }
if normalized_metadata_key(type_name) == normalized_metadata_key(&node.type_name) =>
{
encode_fixed_bytes_value(&node.type_name, bytes)?
}
UnitySerializedReplacementValue::PPtr { file_id, path_id }
if node.type_name.starts_with("PPtr<") || node.type_name == "PPtr" =>
{
let mut encoded = Vec::with_capacity(16);
push_i32_endian(&mut encoded, *file_id, endian);
if serialized_version >= 14 {
push_i64_endian(&mut encoded, *path_id, endian);
} else {
let path_id = i32::try_from(*path_id).map_err(|_| {
AssetBundleError::Parse(format!(
"PPtr path_id {} exceeds legacy i32 range",
path_id
))
})?;
push_i32_endian(&mut encoded, path_id, endian);
}
encoded
}
UnitySerializedReplacementValue::Object(fields) => {
encode_object_replacement_from_schema(fields, nodes, index, serialized_version, endian)?
}
_ => {
return Err(AssetBundleError::UnsupportedFormat(format!(
"replacement {:?} does not match TypeTree node {}.{}",
replacement, node.type_name, node.name
)));
}
};
pad_schema_encoded_value(encoded, node)
}
fn encode_collection_replacement_from_schema(
replacements: &[UnitySerializedReplacementValue],
nodes: &[UnityTypeTreeNode],
data_index: usize,
serialized_version: u32,
endian: Endian,
collection_kind: &str,
) -> Result<Vec<u8>> {
let len = i32::try_from(replacements.len()).map_err(|_| {
AssetBundleError::Parse(format!(
"{collection_kind} replacement length {} exceeds i32 range",
replacements.len()
))
})?;
if replacements.len() > MAX_COLLECTION_ITEMS {
return Err(AssetBundleError::Parse(format!(
"{collection_kind} replacement length {} exceeds limit {MAX_COLLECTION_ITEMS}",
replacements.len()
)));
}
let mut output = Vec::new();
push_i32_endian(&mut output, len, endian);
for replacement in replacements {
output.extend_from_slice(&encode_replacement_node(
replacement,
nodes,
data_index,
serialized_version,
endian,
)?);
}
Ok(output)
}
fn encode_object_replacement_from_schema(
replacements: &[UnitySerializedFieldReplacement],
nodes: &[UnityTypeTreeNode],
index: usize,
serialized_version: u32,
endian: Endian,
) -> Result<Vec<u8>> {
let end = node_end(nodes, index);
let children = direct_children(nodes, index, end);
let mut used = vec![false; replacements.len()];
let mut output = Vec::new();
for child_index in children {
let child = &nodes[child_index];
let Some((replacement_index, replacement)) = replacements
.iter()
.enumerate()
.find(|(index, replacement)| !used[*index] && replacement.name == child.name)
else {
return Err(AssetBundleError::Parse(format!(
"object replacement missing field {}",
child.name
)));
};
used[replacement_index] = true;
output.extend_from_slice(&encode_replacement_node(
&replacement.value,
nodes,
child_index,
serialized_version,
endian,
)?);
}
if let Some(extra) = replacements
.iter()
.zip(used.iter())
.find_map(|(replacement, used)| (!*used).then_some(replacement))
{
return Err(AssetBundleError::Parse(format!(
"object replacement field {} does not exist in current TypeTree object",
extra.name
)));
}
Ok(output)
}
fn collection_data_node_index(
nodes: &[UnityTypeTreeNode],
index: usize,
collection_kind: &str,
) -> Result<usize> {
let node = nodes.get(index).ok_or_else(|| {
AssetBundleError::Parse(format!("TypeTree node index {index} is out of range"))
})?;
let end = node_end(nodes, index);
let children = direct_children(nodes, index, end);
if collection_kind == "map" && node.type_name == "map" {
let array_index = children
.iter()
.copied()
.find(|child_index| is_array_node(&nodes[*child_index]))
.or_else(|| children.first().copied())
.ok_or_else(|| {
AssetBundleError::parse_field(
&node.name,
0,
"map node has no array child for replacement schema",
)
})?;
return collection_data_node_index(nodes, array_index, "array");
}
let array_children = if collection_kind == "array" {
collection_array_children(nodes, index)
} else {
children
};
array_children
.iter()
.copied()
.find(|child_index| nodes[*child_index].name == "data")
.or_else(|| array_children.last().copied())
.ok_or_else(|| {
AssetBundleError::parse_field(
&node.name,
0,
"array/map node has no data child for replacement schema",
)
})
}
fn pad_schema_encoded_value(encoded: Vec<u8>, node: &UnityTypeTreeNode) -> Result<Vec<u8>> {
if node.byte_size < 0 {
return Ok(encoded);
}
let field_byte_size = usize::try_from(node.byte_size).map_err(|_| {
AssetBundleError::Parse(format!(
"TypeTree node {} byte_size does not fit usize",
node.name
))
})?;
pad_fixed_encoded_value(encoded, field_byte_size, &node.type_name)
}
fn encode_object_replacement(
replacements: &[UnitySerializedFieldReplacement],
current: &UnitySerializedValue,
context: ReplacementEncodingContext<'_>,
) -> Result<Vec<u8>> {
let current_fields = match current {
UnitySerializedValue::Object(fields)
| UnitySerializedValue::ManagedReference { fields, .. }
| UnitySerializedValue::ManagedReferenceRegistry { fields, .. } => fields,
_ => {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field value {:?} is not object",
current
)));
}
};
let mut used = vec![false; replacements.len()];
let mut output = Vec::new();
for current_field in current_fields {
let Some((replacement_index, replacement)) = replacements
.iter()
.enumerate()
.find(|(index, replacement)| !used[*index] && replacement.name == current_field.name)
else {
return Err(AssetBundleError::Parse(format!(
"object replacement missing field {}",
current_field.name
)));
};
used[replacement_index] = true;
output.extend_from_slice(&encode_replacement_value(
&replacement.value,
&current_field.value,
&current_field.type_name,
current_field.byte_size,
current_field.type_tree_node_index,
context,
)?);
}
if let Some(extra) = replacements
.iter()
.zip(used.iter())
.find_map(|(replacement, used)| (!*used).then_some(replacement))
{
return Err(AssetBundleError::Parse(format!(
"object replacement field {} does not exist in current TypeTree object",
extra.name
)));
}
Ok(output)
}
fn require_current_kind(
current: &UnitySerializedValue,
matches_kind: bool,
expected: &str,
) -> Result<()> {
if matches_kind {
Ok(())
} else {
Err(AssetBundleError::UnsupportedFormat(format!(
"field value {:?} is not {expected}",
current
)))
}
}
fn encode_signed_integer(type_name: &str, value: i64, endian: Endian) -> Result<Vec<u8>> {
let mut output = Vec::new();
match type_name {
"char" | "SInt8" => output.push(i8::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!("{type_name} replacement {value} out of range"))
})? as u8),
"short" | "SInt16" => {
push_i16_endian(
&mut output,
i16::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!("{type_name} replacement {value} out of range"))
})?,
endian,
);
}
"int" | "SInt32" => {
push_i32_endian(
&mut output,
i32::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!("{type_name} replacement {value} out of range"))
})?,
endian,
);
}
"long long" | "SInt64" => push_i64_endian(&mut output, value, endian),
_ => {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not a supported signed integer"
)))
}
}
Ok(output)
}
fn encode_unsigned_integer(type_name: &str, value: u64, endian: Endian) -> Result<Vec<u8>> {
let mut output = Vec::new();
match type_name {
"UInt8" | "byte" => output.push(u8::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!("{type_name} replacement {value} out of range"))
})?),
"UInt16" | "unsigned short" => {
push_u16_endian(
&mut output,
u16::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!("{type_name} replacement {value} out of range"))
})?,
endian,
);
}
"UInt32" | "unsigned int" => {
push_u32_endian(
&mut output,
u32::try_from(value).map_err(|_| {
AssetBundleError::Parse(format!("{type_name} replacement {value} out of range"))
})?,
endian,
);
}
"UInt64" | "unsigned long long" => push_u64_endian(&mut output, value, endian),
_ => {
return Err(AssetBundleError::UnsupportedFormat(format!(
"field type {type_name} is not a supported unsigned integer"
)))
}
}
Ok(output)
}
fn pad_fixed_encoded_value(
mut encoded: Vec<u8>,
field_byte_size: usize,
type_name: &str,
) -> Result<Vec<u8>> {
if encoded.len() > field_byte_size {
return Err(AssetBundleError::Parse(format!(
"encoded {type_name} value is {} bytes but field size is {}",
encoded.len(),
field_byte_size
)));
}
encoded.resize(field_byte_size, 0);
Ok(encoded)
}
fn align_vec_to(output: &mut Vec<u8>, alignment: usize) {
let remainder = output.len() % alignment;
if remainder != 0 {
output.resize(output.len() + alignment - remainder, 0);
}
}
fn push_u32_endian(output: &mut Vec<u8>, value: u32, endian: Endian) {
match endian {
Endian::Little => output.extend_from_slice(&value.to_le_bytes()),
Endian::Big => output.extend_from_slice(&value.to_be_bytes()),
}
}
fn push_i16_endian(output: &mut Vec<u8>, value: i16, endian: Endian) {
match endian {
Endian::Little => output.extend_from_slice(&value.to_le_bytes()),
Endian::Big => output.extend_from_slice(&value.to_be_bytes()),
}
}
fn push_u16_endian(output: &mut Vec<u8>, value: u16, endian: Endian) {
match endian {
Endian::Little => output.extend_from_slice(&value.to_le_bytes()),
Endian::Big => output.extend_from_slice(&value.to_be_bytes()),
}
}
fn push_i32_endian(output: &mut Vec<u8>, value: i32, endian: Endian) {
match endian {
Endian::Little => output.extend_from_slice(&value.to_le_bytes()),
Endian::Big => output.extend_from_slice(&value.to_be_bytes()),
}
}
fn push_i64_endian(output: &mut Vec<u8>, value: i64, endian: Endian) {
match endian {
Endian::Little => output.extend_from_slice(&value.to_le_bytes()),
Endian::Big => output.extend_from_slice(&value.to_be_bytes()),
}
}
fn push_u64_endian(output: &mut Vec<u8>, value: u64, endian: Endian) {
match endian {
Endian::Little => output.extend_from_slice(&value.to_le_bytes()),
Endian::Big => output.extend_from_slice(&value.to_be_bytes()),
}
}
fn encode_u32_value(value: u32, endian: Endian) -> Vec<u8> {
let mut output = Vec::with_capacity(4);
push_u32_endian(&mut output, value, endian);
output
}
fn encode_u64_value(value: u64, endian: Endian) -> Vec<u8> {
let mut output = Vec::with_capacity(8);
push_u64_endian(&mut output, value, endian);
output
}
fn write_u32_be(output: &mut [u8], offset: usize, value: u32) -> Result<()> {
let end = offset.checked_add(4).ok_or_else(|| {
AssetBundleError::Parse("serialized header write offset overflow".to_string())
})?;
let output_len = output.len();
let bytes = output.get_mut(offset..end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"serialized header write range {}..{} exceeds file size {}",
offset, end, output_len
))
})?;
bytes.copy_from_slice(&value.to_be_bytes());
Ok(())
}
fn write_u64_be(output: &mut [u8], offset: usize, value: u64) -> Result<()> {
let end = offset.checked_add(8).ok_or_else(|| {
AssetBundleError::Parse("serialized header write offset overflow".to_string())
})?;
let output_len = output.len();
let bytes = output.get_mut(offset..end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"serialized header write range {}..{} exceeds file size {}",
offset, end, output_len
))
})?;
bytes.copy_from_slice(&value.to_be_bytes());
Ok(())
}
fn write_u32_endian(output: &mut [u8], offset: usize, value: u32, endian: Endian) -> Result<()> {
let end = offset.checked_add(4).ok_or_else(|| {
AssetBundleError::Parse("serialized metadata write offset overflow".to_string())
})?;
let output_len = output.len();
let bytes = output.get_mut(offset..end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"serialized metadata write range {}..{} exceeds file size {}",
offset, end, output_len
))
})?;
let encoded = match endian {
Endian::Little => value.to_le_bytes(),
Endian::Big => value.to_be_bytes(),
};
bytes.copy_from_slice(&encoded);
Ok(())
}
fn write_u64_endian(output: &mut [u8], offset: usize, value: u64, endian: Endian) -> Result<()> {
let end = offset.checked_add(8).ok_or_else(|| {
AssetBundleError::Parse("serialized metadata write offset overflow".to_string())
})?;
let output_len = output.len();
let bytes = output.get_mut(offset..end).ok_or_else(|| {
AssetBundleError::Parse(format!(
"serialized metadata write range {}..{} exceeds file size {}",
offset, end, output_len
))
})?;
let encoded = match endian {
Endian::Little => value.to_le_bytes(),
Endian::Big => value.to_be_bytes(),
};
bytes.copy_from_slice(&encoded);
Ok(())
}
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_i8(&mut self, field: &str) -> Result<i8> {
Ok(self.read_u8(field)? as i8)
}
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}"))
})
}
fn read_aligned_string(&mut self, field: &str) -> Result<String> {
let value = self.read_len_prefixed_string(field)?;
self.align(4)?;
Ok(value)
}
}
#[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
}
fn synthetic_serialized_monobehaviour() -> Vec<u8> {
let mut object_data = Vec::new();
push_u32_le(&mut object_data, 5);
object_data.extend_from_slice(b"hello");
align(&mut object_data, 4);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let field_type = strings.len();
strings.extend_from_slice(b"string\0");
let field_name = strings.len();
strings.extend_from_slice(b"message\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 2);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, field_type as i32, field_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_managed_reference() -> Vec<u8> {
synthetic_serialized_managed_reference_with_type(b"managedReference")
}
fn synthetic_serialized_managed_reference_with_type(managed_type_name: &[u8]) -> Vec<u8> {
let mut object_data = Vec::new();
push_u32_le(&mut object_data, 5);
object_data.extend_from_slice(b"hello");
align(&mut object_data, 4);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let managed_type = strings.len();
strings.extend_from_slice(managed_type_name);
strings.push(0);
let managed_name = strings.len();
strings.extend_from_slice(b"entry\0");
let field_type = strings.len();
strings.extend_from_slice(b"string\0");
let field_name = strings.len();
strings.extend_from_slice(b"message\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 3);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, managed_type as i32, managed_name as i32),
(2u8, field_type as i32, field_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_managed_reference_registry() -> Vec<u8> {
synthetic_serialized_managed_reference_registry_with_names(
b"managedReferencesRegistry",
b"m_SerializedReferences",
b"type",
b"data",
)
}
struct ManagedReferenceRegistryNames<'a> {
registry_type_name: &'a [u8],
registry_field_name: &'a [u8],
array_field_name: &'a [u8],
rid_field_name: &'a [u8],
type_field_name: &'a [u8],
class_field_name: &'a [u8],
namespace_field_name: &'a [u8],
assembly_field_name: &'a [u8],
payload_field_name: &'a [u8],
}
fn synthetic_serialized_managed_reference_registry_with_names(
registry_type_name: &[u8],
registry_field_name: &[u8],
type_field_name: &[u8],
payload_field_name: &[u8],
) -> Vec<u8> {
synthetic_serialized_managed_reference_registry_with_detailed_names(
ManagedReferenceRegistryNames {
registry_type_name,
registry_field_name,
array_field_name: b"references",
rid_field_name: b"rid",
type_field_name,
class_field_name: b"class",
namespace_field_name: b"ns",
assembly_field_name: b"asm",
payload_field_name,
},
)
}
fn synthetic_serialized_managed_reference_registry_with_detailed_names(
names: ManagedReferenceRegistryNames<'_>,
) -> Vec<u8> {
let ManagedReferenceRegistryNames {
registry_type_name,
registry_field_name,
type_field_name,
array_field_name,
rid_field_name,
class_field_name,
namespace_field_name,
assembly_field_name,
payload_field_name,
} = names;
let mut object_data = Vec::new();
push_i32_le(&mut object_data, 1);
push_i64_le(&mut object_data, 42);
for value in ["ScenarioLine", "BA.Text", "Game"] {
push_u32_le(&mut object_data, value.len() as u32);
object_data.extend_from_slice(value.as_bytes());
align(&mut object_data, 4);
}
push_u32_le(&mut object_data, "こんにちは".len() as u32);
object_data.extend_from_slice("こんにちは".as_bytes());
align(&mut object_data, 4);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let registry_type = strings.len();
strings.extend_from_slice(registry_type_name);
strings.push(0);
let registry_name = strings.len();
strings.extend_from_slice(registry_field_name);
strings.push(0);
let array_type = strings.len();
strings.extend_from_slice(b"Array\0");
let array_name = strings.len();
strings.extend_from_slice(array_field_name);
strings.push(0);
let size_type = strings.len();
strings.extend_from_slice(b"int\0");
let size_name = strings.len();
strings.extend_from_slice(b"size\0");
let entry_type = strings.len();
strings.extend_from_slice(b"ManagedReferenceEntry\0");
let data_name = strings.len();
strings.extend_from_slice(b"data\0");
let rid_type = strings.len();
strings.extend_from_slice(b"long long\0");
let rid_name = strings.len();
strings.extend_from_slice(rid_field_name);
strings.push(0);
let type_info_type = strings.len();
strings.extend_from_slice(b"ManagedReferenceType\0");
let type_info_name = strings.len();
strings.extend_from_slice(type_field_name);
strings.push(0);
let string_type = strings.len();
strings.extend_from_slice(b"string\0");
let class_name = strings.len();
strings.extend_from_slice(class_field_name);
strings.push(0);
let namespace_name = strings.len();
strings.extend_from_slice(namespace_field_name);
strings.push(0);
let assembly_name = strings.len();
strings.extend_from_slice(assembly_field_name);
strings.push(0);
let managed_type = strings.len();
strings.extend_from_slice(b"managedReference\0");
let payload_name = strings.len();
strings.extend_from_slice(payload_field_name);
strings.push(0);
let message_name = strings.len();
strings.extend_from_slice(b"message\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 12);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, registry_type as i32, registry_name as i32),
(2u8, array_type as i32, array_name as i32),
(3u8, size_type as i32, size_name as i32),
(3u8, entry_type as i32, data_name as i32),
(4u8, rid_type as i32, rid_name as i32),
(4u8, type_info_type as i32, type_info_name as i32),
(5u8, string_type as i32, class_name as i32),
(5u8, string_type as i32, namespace_name as i32),
(5u8, string_type as i32, assembly_name as i32),
(4u8, managed_type as i32, payload_name as i32),
(5u8, string_type as i32, message_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_string_array() -> Vec<u8> {
synthetic_serialized_string_array_with_values(&["hello", "world"])
}
fn synthetic_serialized_empty_string_array() -> Vec<u8> {
synthetic_serialized_string_array_with_values(&[])
}
fn synthetic_serialized_vector_string_array() -> Vec<u8> {
synthetic_serialized_vector_string_array_with_values(&["hello", "world"])
}
fn synthetic_serialized_empty_vector_string_array() -> Vec<u8> {
synthetic_serialized_vector_string_array_with_values(&[])
}
fn synthetic_serialized_list_string_array() -> Vec<u8> {
synthetic_serialized_collection_string_array_with_values(
b"List<string>",
&["hello", "world"],
)
}
fn synthetic_serialized_empty_hashset_string_array() -> Vec<u8> {
synthetic_serialized_collection_string_array_with_values(b"HashSet<string>", &[])
}
fn synthetic_serialized_vector_string_array_with_values(values: &[&str]) -> Vec<u8> {
synthetic_serialized_collection_string_array_with_values(b"vector", values)
}
fn synthetic_serialized_collection_string_array_with_values(
collection_type_name: &[u8],
values: &[&str],
) -> Vec<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, values.len() as i32);
for value in values {
push_u32_le(&mut object_data, value.len() as u32);
object_data.extend_from_slice(value.as_bytes());
align(&mut object_data, 4);
}
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let vector_type = strings.len();
strings.extend_from_slice(collection_type_name);
strings.push(0);
let vector_name = strings.len();
strings.extend_from_slice(b"messages\0");
let array_type = strings.len();
strings.extend_from_slice(b"Array\0");
let array_name = strings.len();
strings.extend_from_slice(b"Array\0");
let size_type = strings.len();
strings.extend_from_slice(b"int\0");
let size_name = strings.len();
strings.extend_from_slice(b"size\0");
let data_type = strings.len();
strings.extend_from_slice(b"string\0");
let data_name = strings.len();
strings.extend_from_slice(b"data\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 5);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, vector_type as i32, vector_name as i32),
(2u8, array_type as i32, array_name as i32),
(3u8, size_type as i32, size_name as i32),
(3u8, data_type as i32, data_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_string_array_with_values(values: &[&str]) -> Vec<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, values.len() as i32);
for value in values {
push_u32_le(&mut object_data, value.len() as u32);
object_data.extend_from_slice(value.as_bytes());
align(&mut object_data, 4);
}
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let array_type = strings.len();
strings.extend_from_slice(b"Array\0");
let array_name = strings.len();
strings.extend_from_slice(b"messages\0");
let size_type = strings.len();
strings.extend_from_slice(b"int\0");
let size_name = strings.len();
strings.extend_from_slice(b"size\0");
let data_type = strings.len();
strings.extend_from_slice(b"string\0");
let data_name = strings.len();
strings.extend_from_slice(b"data\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 4);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, array_type as i32, array_name as i32),
(2u8, size_type as i32, size_name as i32),
(2u8, data_type as i32, data_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_int_array() -> Vec<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, 2);
push_i32_le(&mut object_data, 10);
push_i32_le(&mut object_data, 20);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let array_type = strings.len();
strings.extend_from_slice(b"Array\0");
let array_name = strings.len();
strings.extend_from_slice(b"scores\0");
let size_type = strings.len();
strings.extend_from_slice(b"int\0");
let size_name = strings.len();
strings.extend_from_slice(b"size\0");
let data_type = strings.len();
strings.extend_from_slice(b"int\0");
let data_name = strings.len();
strings.extend_from_slice(b"data\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 4);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, array_type as i32, array_name as i32),
(2u8, size_type as i32, size_name as i32),
(2u8, data_type as i32, data_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_unity_leaf_structs() -> Vec<u8> {
let mut object_data = Vec::new();
for value in [
1.0f32.to_bits(),
0.5f32.to_bits(),
0.25f32.to_bits(),
1.0f32.to_bits(),
] {
push_u32_le(&mut object_data, value);
}
let guid: Vec<u8> = (0u8..16).collect();
object_data.extend_from_slice(&guid);
for value in [1.0f32.to_bits(), 2.0f32.to_bits(), 3.0f32.to_bits()] {
push_u32_le(&mut object_data, value);
}
for value in [10, -20] {
push_i32_le(&mut object_data, value);
}
for value in [1, 2, 100, 200] {
push_i32_le(&mut object_data, value);
}
for value in [
0.0f32.to_bits(),
1.0f32.to_bits(),
2.0f32.to_bits(),
3.0f32.to_bits(),
4.0f32.to_bits(),
5.0f32.to_bits(),
] {
push_u32_le(&mut object_data, value);
}
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let color_type = strings.len();
strings.extend_from_slice(b"ColorRGBA\0");
let color_name = strings.len();
strings.extend_from_slice(b"tint\0");
let guid_type = strings.len();
strings.extend_from_slice(b"GUID\0");
let guid_name = strings.len();
strings.extend_from_slice(b"guid\0");
let vector_type = strings.len();
strings.extend_from_slice(b"Vector3f\0");
let vector_name = strings.len();
strings.extend_from_slice(b"position\0");
let vector_int_type = strings.len();
strings.extend_from_slice(b"Vector2Int\0");
let vector_int_name = strings.len();
strings.extend_from_slice(b"grid\0");
let rect_int_type = strings.len();
strings.extend_from_slice(b"RectInt\0");
let rect_int_name = strings.len();
strings.extend_from_slice(b"tile_rect\0");
let bounds_type = strings.len();
strings.extend_from_slice(b"AABB\0");
let bounds_name = strings.len();
strings.extend_from_slice(b"bounds\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 7);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset, byte_size) in [
(0u8, root_type as i32, root_name as i32, -1),
(1u8, color_type as i32, color_name as i32, 16),
(1u8, guid_type as i32, guid_name as i32, 16),
(1u8, vector_type as i32, vector_name as i32, 12),
(1u8, vector_int_type as i32, vector_int_name as i32, 8),
(1u8, rect_int_type as i32, rect_int_name as i32, 16),
(1u8, bounds_type as i32, bounds_name as i32, 24),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, byte_size);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_unity_child_structs() -> Vec<u8> {
fn push_string(strings: &mut Vec<u8>, value: &[u8]) -> usize {
let offset = strings.len();
strings.extend_from_slice(value);
strings.push(0);
offset
}
let mut object_data = Vec::new();
for value in [1.0f32.to_bits(), 2.0f32.to_bits(), 3.0f32.to_bits()] {
push_u32_le(&mut object_data, value);
}
for value in [10, -20] {
push_i32_le(&mut object_data, value);
}
object_data.extend(0u8..16);
let mut strings = Vec::new();
let root_type = push_string(&mut strings, b"MonoBehaviour");
let root_name = push_string(&mut strings, b"");
let vector_type = push_string(&mut strings, b"Vector3f");
let vector_name = push_string(&mut strings, b"position");
let float_type = push_string(&mut strings, b"float");
let x_name = push_string(&mut strings, b"x");
let y_name = push_string(&mut strings, b"y");
let z_name = push_string(&mut strings, b"z");
let vector_int_type = push_string(&mut strings, b"Vector2Int");
let vector_int_name = push_string(&mut strings, b"grid");
let int_type = push_string(&mut strings, b"int");
let guid_type = push_string(&mut strings, b"GUID");
let guid_name = push_string(&mut strings, b"guid");
let uint8_type = push_string(&mut strings, b"UInt8");
let byte_names = (0..16)
.map(|index| push_string(&mut strings, format!("data{index}").as_bytes()))
.collect::<Vec<_>>();
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 25);
push_i32_le(&mut metadata, strings.len() as i32);
let mut nodes = vec![
(0u8, root_type as i32, root_name as i32, -1),
(1u8, vector_type as i32, vector_name as i32, 12),
(2u8, float_type as i32, x_name as i32, 4),
(2u8, float_type as i32, y_name as i32, 4),
(2u8, float_type as i32, z_name as i32, 4),
(1u8, vector_int_type as i32, vector_int_name as i32, 8),
(2u8, int_type as i32, x_name as i32, 4),
(2u8, int_type as i32, y_name as i32, 4),
(1u8, guid_type as i32, guid_name as i32, 16),
];
nodes.extend(
byte_names
.iter()
.map(|name| (2u8, uint8_type as i32, *name as i32, 1)),
);
for (level, type_offset, name_offset, byte_size) in nodes {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, byte_size);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_unknown_fixed_field() -> Vec<u8> {
let mut object_data = vec![1, 2, 3, 4];
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let blob_type = strings.len();
strings.extend_from_slice(b"CustomBlob\0");
let blob_name = strings.len();
strings.extend_from_slice(b"blob\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 2);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset, byte_size) in [
(0u8, root_type as i32, root_name as i32, -1),
(1u8, blob_type as i32, blob_name as i32, 4),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, byte_size);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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.append(&mut object_data);
file
}
fn synthetic_serialized_enum_field() -> Vec<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, 2);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let enum_type = strings.len();
strings.extend_from_slice(b"ScenarioDifficulty\0");
let enum_name = strings.len();
strings.extend_from_slice(b"difficulty\0");
let value_type = strings.len();
strings.extend_from_slice(b"int\0");
let value_name = strings.len();
strings.extend_from_slice(b"value__\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 3);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset, byte_size) in [
(0u8, root_type as i32, root_name as i32, -1),
(1u8, enum_type as i32, enum_name as i32, -1),
(2u8, value_type as i32, value_name as i32, 4),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, byte_size);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_bitfield_field() -> Vec<u8> {
let mut object_data = Vec::new();
push_u32_le(&mut object_data, 5);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let bitfield_type = strings.len();
strings.extend_from_slice(b"LayerMask\0");
let bitfield_name = strings.len();
strings.extend_from_slice(b"target_layers\0");
let bits_type = strings.len();
strings.extend_from_slice(b"UInt32\0");
let bits_name = strings.len();
strings.extend_from_slice(b"m_Bits\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 3);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset, byte_size) in [
(0u8, root_type as i32, root_name as i32, -1),
(1u8, bitfield_type as i32, bitfield_name as i32, -1),
(2u8, bits_type as i32, bits_name as i32, 4),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, byte_size);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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
}
fn synthetic_serialized_string_map() -> Vec<u8> {
synthetic_serialized_string_map_with_entries(&[("jp", "hello"), ("cn", "world")])
}
fn synthetic_serialized_empty_string_map() -> Vec<u8> {
synthetic_serialized_string_map_with_entries(&[])
}
fn synthetic_serialized_string_map_with_entries(entries: &[(&str, &str)]) -> Vec<u8> {
synthetic_serialized_string_map_with_entry_names(entries, b"first", b"second")
}
fn synthetic_serialized_key_value_string_map() -> Vec<u8> {
synthetic_serialized_string_map_with_entry_names(
&[("jp", "hello"), ("cn", "world")],
b"key",
b"value",
)
}
fn synthetic_serialized_scriptableobject_key_value_string_map() -> Vec<u8> {
synthetic_serialized_string_map_with_entry_names_and_root(
&[("jp", "hello"), ("cn", "world")],
b"key",
b"value",
b"ScriptableObject",
115,
)
}
fn synthetic_serialized_empty_key_value_string_map() -> Vec<u8> {
synthetic_serialized_string_map_with_entry_names(&[], b"key", b"value")
}
fn synthetic_serialized_string_map_with_entry_names(
entries: &[(&str, &str)],
first_field_name: &[u8],
second_field_name: &[u8],
) -> Vec<u8> {
synthetic_serialized_string_map_with_entry_names_and_root(
entries,
first_field_name,
second_field_name,
b"MonoBehaviour",
114,
)
}
fn synthetic_serialized_string_map_with_entry_names_and_root(
entries: &[(&str, &str)],
first_field_name: &[u8],
second_field_name: &[u8],
root_type_name: &[u8],
class_id: i32,
) -> Vec<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, entries.len() as i32);
for (key, value) in entries {
push_u32_le(&mut object_data, key.len() as u32);
object_data.extend_from_slice(key.as_bytes());
align(&mut object_data, 4);
push_u32_le(&mut object_data, value.len() as u32);
object_data.extend_from_slice(value.as_bytes());
align(&mut object_data, 4);
}
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(root_type_name);
strings.push(0);
let root_name = strings.len();
strings.push(0);
let map_type = strings.len();
strings.extend_from_slice(b"map\0");
let map_name = strings.len();
strings.extend_from_slice(b"texts\0");
let array_type = strings.len();
strings.extend_from_slice(b"Array\0");
let array_name = strings.len();
strings.extend_from_slice(b"Array\0");
let size_type = strings.len();
strings.extend_from_slice(b"int\0");
let size_name = strings.len();
strings.extend_from_slice(b"size\0");
let pair_type = strings.len();
strings.extend_from_slice(b"pair\0");
let data_name = strings.len();
strings.extend_from_slice(b"data\0");
let string_type = strings.len();
strings.extend_from_slice(b"string\0");
let first_name = strings.len();
strings.extend_from_slice(first_field_name);
strings.push(0);
let second_name = strings.len();
strings.extend_from_slice(second_field_name);
strings.push(0);
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, class_id);
metadata.push(0);
push_i16_le(&mut metadata, 0);
if class_id == 114 {
metadata.extend_from_slice(&[0; 16]);
}
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 7);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, map_type as i32, map_name as i32),
(2u8, array_type as i32, array_name as i32),
(3u8, size_type as i32, size_name as i32),
(3u8, pair_type as i32, data_name as i32),
(4u8, string_type as i32, first_name as i32),
(4u8, string_type as i32, second_name as i32),
] {
push_i16_le(&mut metadata, 1);
metadata.push(level);
metadata.push(0);
push_i32_le(&mut metadata, type_offset);
push_i32_le(&mut metadata, name_offset);
push_i32_le(&mut metadata, -1);
push_i32_le(&mut metadata, 0);
push_i32_le(&mut metadata, 0);
push_u64_le(&mut metadata, 0);
}
metadata.extend_from_slice(&strings);
push_i32_le(&mut metadata, 0);
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");
}
#[test]
fn decodes_monobehaviour_typetree_fields_with_offsets() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_monobehaviour()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "message");
assert_eq!(fields[0].name, "message");
assert_eq!(fields[0].type_name, "string");
assert_eq!(fields[0].offset, 0);
assert_eq!(fields[0].byte_size, 12);
assert_eq!(
fields[0].value,
UnitySerializedValue::String("hello".to_string())
);
}
#[test]
fn decodes_typetree_covered_managed_reference_fields() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_managed_reference()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "entry");
assert_eq!(fields[0].type_name, "managedReference");
let UnitySerializedValue::ManagedReference {
type_name,
metadata,
fields: managed_fields,
bytes,
} = &fields[0].value
else {
panic!("expected managed reference value");
};
assert_eq!(type_name, "managedReference");
assert!(metadata.is_none());
assert!(bytes.is_empty());
assert_eq!(managed_fields.len(), 1);
assert_eq!(managed_fields[0].path, "entry.message");
assert_eq!(
managed_fields[0].value,
UnitySerializedValue::String("hello".to_string())
);
}
#[test]
fn decodes_typetree_covered_serialized_reference_alias() {
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_managed_reference_with_type(b"SerializedReference"),
)
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "entry");
assert_eq!(fields[0].type_name, "SerializedReference");
let UnitySerializedValue::ManagedReference {
type_name,
fields: managed_fields,
..
} = &fields[0].value
else {
panic!("expected serialized reference alias to decode as managed reference");
};
assert_eq!(type_name, "SerializedReference");
assert_eq!(managed_fields.len(), 1);
assert_eq!(managed_fields[0].path, "entry.message");
assert_eq!(
managed_fields[0].value,
UnitySerializedValue::String("hello".to_string())
);
}
#[test]
fn decodes_managed_reference_registry_records() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_managed_reference_registry())
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "m_SerializedReferences");
let UnitySerializedValue::ManagedReferenceRegistry {
references,
fields: registry_fields,
} = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(references.len(), 1);
assert_eq!(references[0].metadata.reference_id, Some(42));
assert_eq!(
references[0].metadata.type_name.as_deref(),
Some("ScenarioLine")
);
assert_eq!(references[0].metadata.namespace.as_deref(), Some("BA.Text"));
assert_eq!(
references[0].metadata.assembly_name.as_deref(),
Some("Game")
);
assert_eq!(references[0].fields.len(), 1);
assert_eq!(
references[0].fields[0].path,
"m_SerializedReferences.references[0].data.message"
);
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
assert_eq!(registry_fields.len(), 1);
}
#[test]
fn decodes_managed_reference_registry_alias_names() {
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_managed_reference_registry_with_names(
b"ManagedReferenceRegistry",
b"m_ManagedReferences",
b"managedReferenceFullTypeName",
b"value",
),
)
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "m_ManagedReferences");
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(references.len(), 1);
assert_eq!(references[0].metadata.reference_id, Some(42));
assert_eq!(
references[0].metadata.type_name.as_deref(),
Some("ScenarioLine")
);
assert_eq!(references[0].fields.len(), 1);
assert_eq!(
references[0].fields[0].path,
"m_ManagedReferences.references[0].value.message"
);
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
}
#[test]
fn decodes_managed_reference_registry_refids_and_verbose_type_names() {
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_managed_reference_registry_with_detailed_names(
ManagedReferenceRegistryNames {
registry_type_name: b"ManagedReferencesRegistry",
registry_field_name: b"m_ManagedReferences",
array_field_name: b"RefIds",
rid_field_name: b"rid",
type_field_name: b"typeID",
class_field_name: b"className",
namespace_field_name: b"namespaceName",
assembly_field_name: b"asmName",
payload_field_name: b"data",
},
),
)
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "m_ManagedReferences");
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(references.len(), 1);
assert_eq!(references[0].metadata.reference_id, Some(42));
assert_eq!(
references[0].metadata.type_name.as_deref(),
Some("ScenarioLine")
);
assert_eq!(references[0].metadata.namespace.as_deref(), Some("BA.Text"));
assert_eq!(
references[0].metadata.assembly_name.as_deref(),
Some("Game")
);
assert_eq!(
references[0].fields[0].path,
"m_ManagedReferences.RefIds[0].data.message"
);
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
}
#[test]
fn decodes_managed_reference_registry_prefixed_metadata_aliases() {
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_managed_reference_registry_with_detailed_names(
ManagedReferenceRegistryNames {
registry_type_name: b"SerializedReferenceRegistry",
registry_field_name: b"m_ManagedReferences",
array_field_name: b"managedReferenceIds",
rid_field_name: b"managedReferenceId",
type_field_name: b"managedReferenceType",
class_field_name: b"managedReferenceClassName",
namespace_field_name: b"managedReferenceNamespaceName",
assembly_field_name: b"managedReferenceAssemblyName",
payload_field_name: b"serializedReferenceData",
},
),
)
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(references.len(), 1);
assert_eq!(references[0].metadata.reference_id, Some(42));
assert_eq!(
references[0].metadata.type_name.as_deref(),
Some("ScenarioLine")
);
assert_eq!(references[0].metadata.namespace.as_deref(), Some("BA.Text"));
assert_eq!(
references[0].metadata.assembly_name.as_deref(),
Some("Game")
);
assert_eq!(
references[0].fields[0].path,
"m_ManagedReferences.managedReferenceIds[0].serializedReferenceData.message"
);
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
}
#[test]
fn decodes_and_replaces_managed_reference_registry_payload_alias_family() {
for payload_field_name in [
b"managedReferencePayload".as_slice(),
b"referencePayload".as_slice(),
b"serializedReferencePayload".as_slice(),
b"managedReferenceValue".as_slice(),
b"referenceValue".as_slice(),
b"serializedReferenceValue".as_slice(),
b"managedReferenceObject".as_slice(),
b"referenceObject".as_slice(),
b"serializedReferenceObject".as_slice(),
] {
let payload_field_name = std::str::from_utf8(payload_field_name).unwrap();
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_managed_reference_registry_with_names(
b"ManagedReferenceRegistry",
b"m_ManagedReferences",
b"managedReferenceFullTypeName",
payload_field_name.as_bytes(),
),
)
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } =
&fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(references.len(), 1);
assert_eq!(
references[0].metadata.type_name.as_deref(),
Some("ScenarioLine")
);
let expected_path =
format!("m_ManagedReferences.references[0].{payload_field_name}.message");
assert_eq!(references[0].fields[0].path, expected_path);
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
let rewritten = parsed
.replace_string_field(1, &expected_path, Some("こんにちは"), "你好")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } =
&fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("你好".to_string())
);
}
}
#[test]
fn collects_managed_reference_records_with_id_aliases_and_multiple_payloads() {
let fields = vec![UnitySerializedField {
path: "m_ManagedReferences.m_RefIds".to_string(),
name: "m_RefIds".to_string(),
type_name: "Array".to_string(),
offset: 0,
byte_size: 128,
type_tree_node_index: None,
value: UnitySerializedValue::Array(vec![
managed_reference_record_fixture(
42,
"Game BA.Text.ScenarioLine",
"m_ManagedReferences.m_RefIds[0].serializedData.message",
"こんにちは",
),
managed_reference_record_fixture(
43,
"Game BA.Text.ChoiceLine",
"m_ManagedReferences.m_RefIds[1].referenceData.message",
"選択肢",
),
]),
}];
let records = managed_reference_records_from_fields(&fields);
assert_eq!(records.len(), 2);
assert_eq!(records[0].metadata.reference_id, Some(42));
assert_eq!(
records[0].metadata.type_name.as_deref(),
Some("ScenarioLine")
);
assert_eq!(
records[0].fields[0].path,
"m_ManagedReferences.m_RefIds[0].serializedData.message"
);
assert_eq!(records[1].metadata.reference_id, Some(43));
assert_eq!(records[1].metadata.type_name.as_deref(), Some("ChoiceLine"));
assert_eq!(
records[1].fields[0].path,
"m_ManagedReferences.m_RefIds[1].referenceData.message"
);
}
#[test]
fn decodes_managed_reference_registry_managed_reference_data_payload() {
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_managed_reference_registry_with_names(
b"managedReferencesRegistry",
b"m_SerializedReferences",
b"type",
b"managedReferenceData",
),
)
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(references.len(), 1);
assert_eq!(
references[0].fields[0].path,
"m_SerializedReferences.references[0].managedReferenceData.message"
);
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
let rewritten = parsed
.replace_string_field(
1,
"m_SerializedReferences.references[0].managedReferenceData.message",
Some("こんにちは"),
"你好",
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("你好".to_string())
);
}
#[test]
fn parses_managed_reference_full_typename_variants() {
let unity_full_name = parse_managed_reference_type_name("Game BA.Text.ScenarioLine");
assert_eq!(
unity_full_name,
ParsedManagedReferenceTypeName {
type_name: Some("ScenarioLine".to_string()),
namespace: Some("BA.Text".to_string()),
assembly_name: Some("Game".to_string()),
}
);
let dotnet_full_name = parse_managed_reference_type_name("BA.Text.ScenarioLine, Game");
assert_eq!(
dotnet_full_name,
ParsedManagedReferenceTypeName {
type_name: Some("ScenarioLine".to_string()),
namespace: Some("BA.Text".to_string()),
assembly_name: Some("Game".to_string()),
}
);
let class_only = parse_managed_reference_type_name("ScenarioLine");
assert_eq!(
class_only,
ParsedManagedReferenceTypeName {
type_name: Some("ScenarioLine".to_string()),
namespace: None,
assembly_name: None,
}
);
let metadata = managed_reference_metadata_from_fields(&[UnitySerializedField {
path: "m_ManagedReferences.references[0].managedReferenceFullTypeName".to_string(),
name: "managedReferenceFullTypeName".to_string(),
type_name: "string".to_string(),
offset: 0,
byte_size: 32,
type_tree_node_index: None,
value: UnitySerializedValue::String("Game BA.Text.ScenarioLine".to_string()),
}])
.unwrap();
assert_eq!(
metadata.full_type_name.as_deref(),
Some("Game BA.Text.ScenarioLine")
);
assert_eq!(metadata.type_name.as_deref(), Some("ScenarioLine"));
assert_eq!(metadata.namespace.as_deref(), Some("BA.Text"));
assert_eq!(metadata.assembly_name.as_deref(), Some("Game"));
}
fn managed_reference_record_fixture(
reference_id: i64,
full_type_name: &str,
payload_path: &str,
payload_text: &str,
) -> UnitySerializedField {
let (payload_parent, payload_name) = payload_path
.rsplit_once('.')
.map(|(path, name)| (path.to_string(), name.to_string()))
.unwrap_or_else(|| (payload_path.to_string(), String::new()));
let payload_field_name = if payload_parent.contains("referenceData") {
"referenceData"
} else {
"serializedData"
};
UnitySerializedField {
path: payload_parent.clone(),
name: "data".to_string(),
type_name: "ManagedReferenceEntry".to_string(),
offset: 0,
byte_size: 64,
type_tree_node_index: None,
value: UnitySerializedValue::Object(vec![
UnitySerializedField {
path: "id".to_string(),
name: "id".to_string(),
type_name: "long long".to_string(),
offset: 0,
byte_size: 8,
type_tree_node_index: None,
value: UnitySerializedValue::Signed(reference_id),
},
UnitySerializedField {
path: "typeInfo".to_string(),
name: "typeInfo".to_string(),
type_name: "string".to_string(),
offset: 8,
byte_size: full_type_name.len() + 4,
type_tree_node_index: None,
value: UnitySerializedValue::String(full_type_name.to_string()),
},
UnitySerializedField {
path: payload_parent,
name: payload_field_name.to_string(),
type_name: "managedReference".to_string(),
offset: 32,
byte_size: payload_text.len() + 4,
type_tree_node_index: None,
value: UnitySerializedValue::Object(vec![UnitySerializedField {
path: payload_path.to_string(),
name: payload_name,
type_name: "string".to_string(),
offset: 32,
byte_size: payload_text.len() + 4,
type_tree_node_index: None,
value: UnitySerializedValue::String(payload_text.to_string()),
}]),
},
]),
}
}
#[test]
fn replaces_managed_reference_registry_payload_string_field() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_managed_reference_registry())
.unwrap();
let rewritten = parsed
.replace_string_field(
1,
"m_SerializedReferences.references[0].data.message",
Some("こんにちは"),
"你好",
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value
else {
panic!("expected managed reference registry value");
};
assert_eq!(
references[0].fields[0].value,
UnitySerializedValue::String("你好".to_string())
);
}
#[test]
fn replaces_monobehaviour_string_field_and_updates_object_size() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_monobehaviour()).unwrap();
let rewritten = parsed
.replace_string_field(1, "message", Some("hello"), "こんにちは")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
assert_eq!(
fields[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
assert_eq!(
u64::from_be_bytes(rewritten[24..32].try_into().unwrap()) as usize,
rewritten.len()
);
}
#[test]
fn replaces_managed_reference_string_field() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_managed_reference()).unwrap();
let rewritten = parsed
.replace_string_field(1, "entry.message", Some("hello"), "world")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::ManagedReference {
fields: managed_fields,
..
} = &fields[0].value
else {
panic!("expected managed reference value");
};
assert_eq!(
managed_fields[0].value,
UnitySerializedValue::String("world".to_string())
);
}
#[test]
fn decodes_and_replaces_string_array_elements_with_offsets() {
let parsed = UnitySerializedFile::from_slice(&synthetic_serialized_string_array()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "messages");
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected string array");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(items[0].offset, 4);
assert_eq!(items[0].byte_size, 12);
assert_eq!(
items[0].value,
UnitySerializedValue::String("hello".to_string())
);
assert_eq!(items[1].path, "messages[1]");
assert_eq!(items[1].offset, 16);
assert_eq!(
items[1].value,
UnitySerializedValue::String("world".to_string())
);
let rewritten = parsed
.replace_string_field(1, "messages[1]", Some("world"), "老師")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected string array");
};
assert_eq!(
items[0].value,
UnitySerializedValue::String("hello".to_string())
);
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
}
#[test]
fn replaces_whole_string_array_with_length_change() {
let parsed = UnitySerializedFile::from_slice(&synthetic_serialized_string_array()).unwrap();
let rewritten = parsed
.replace_field_value(
1,
"messages",
Some(&UnitySerializedReplacementValue::Array(vec![
UnitySerializedReplacementValue::String("hello".to_string()),
UnitySerializedReplacementValue::String("world".to_string()),
])),
&UnitySerializedReplacementValue::Array(vec![
UnitySerializedReplacementValue::String("こんにちは".to_string()),
UnitySerializedReplacementValue::String("老師".to_string()),
UnitySerializedReplacementValue::String("文本".to_string()),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected string array");
};
assert_eq!(items.len(), 3);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(
items[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
assert_eq!(items[1].path, "messages[1]");
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
assert_eq!(items[2].path, "messages[2]");
assert_eq!(
items[2].value,
UnitySerializedValue::String("文本".to_string())
);
assert_eq!(
u64::from_be_bytes(rewritten[24..32].try_into().unwrap()) as usize,
rewritten.len()
);
}
#[test]
fn replaces_empty_string_array_from_typetree_schema() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_empty_string_array()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected string array");
};
assert!(items.is_empty());
let rewritten = parsed
.replace_field_value(
1,
"messages",
Some(&UnitySerializedReplacementValue::Array(vec![])),
&UnitySerializedReplacementValue::Array(vec![
UnitySerializedReplacementValue::String("こんにちは".to_string()),
UnitySerializedReplacementValue::String("老師".to_string()),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected string array");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(
items[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
assert_eq!(items[1].path, "messages[1]");
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
}
#[test]
fn decodes_and_replaces_nested_vector_array_shape() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_vector_string_array()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "messages");
assert_eq!(fields[0].type_name, "vector");
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected vector string array");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(items[0].name, "messages[0]");
assert_eq!(items[0].offset, 4);
assert_eq!(items[0].byte_size, 12);
assert_eq!(
items[0].value,
UnitySerializedValue::String("hello".to_string())
);
assert_eq!(items[1].path, "messages[1]");
assert_eq!(items[1].offset, 16);
assert_eq!(
items[1].value,
UnitySerializedValue::String("world".to_string())
);
let rewritten = parsed
.replace_string_field(1, "messages[1]", Some("world"), "老師")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected vector string array");
};
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
let rewritten = reparsed
.replace_field_value(
1,
"messages",
None,
&UnitySerializedReplacementValue::Array(vec![
UnitySerializedReplacementValue::String("こんにちは".to_string()),
UnitySerializedReplacementValue::String("老師".to_string()),
UnitySerializedReplacementValue::String("文本".to_string()),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected vector string array");
};
assert_eq!(items.len(), 3);
assert_eq!(
items[2].value,
UnitySerializedValue::String("文本".to_string())
);
}
#[test]
fn decodes_and_replaces_list_string_collection_alias() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_list_string_array()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "messages");
assert_eq!(fields[0].type_name, "List<string>");
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected List<string> to decode as array");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(
items[0].value,
UnitySerializedValue::String("hello".to_string())
);
assert_eq!(
items[1].value,
UnitySerializedValue::String("world".to_string())
);
let rewritten = parsed
.replace_string_field(1, "messages[1]", Some("world"), "老師")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected List<string> to decode as array");
};
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
}
#[test]
fn replaces_empty_nested_vector_array_from_typetree_schema() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_empty_vector_string_array())
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected empty vector string array");
};
assert!(items.is_empty());
let rewritten = parsed
.replace_field_value(
1,
"messages",
Some(&UnitySerializedReplacementValue::Array(vec![])),
&UnitySerializedReplacementValue::Array(vec![
UnitySerializedReplacementValue::String("こんにちは".to_string()),
UnitySerializedReplacementValue::String("老師".to_string()),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected vector string array");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(
items[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
assert_eq!(items[1].path, "messages[1]");
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
}
#[test]
fn replaces_empty_hashset_string_collection_alias_from_typetree_schema() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_empty_hashset_string_array())
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields[0].type_name, "HashSet<string>");
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected empty HashSet<string> to decode as array");
};
assert!(items.is_empty());
let rewritten = parsed
.replace_field_value(
1,
"messages",
Some(&UnitySerializedReplacementValue::Array(vec![])),
&UnitySerializedReplacementValue::Array(vec![
UnitySerializedReplacementValue::String("こんにちは".to_string()),
UnitySerializedReplacementValue::String("老師".to_string()),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected HashSet<string> to decode as array");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "messages[0]");
assert_eq!(
items[0].value,
UnitySerializedValue::String("こんにちは".to_string())
);
assert_eq!(
items[1].value,
UnitySerializedValue::String("老師".to_string())
);
}
#[test]
fn recognizes_common_collection_container_type_aliases() {
assert!(is_collection_container_type("vector"));
assert!(is_collection_container_type("staticvector"));
assert!(is_collection_container_type("List<string>"));
assert!(is_collection_container_type("HashSet<string>"));
assert!(is_collection_container_type(
"System.Collections.Generic.List<System.String>"
));
assert!(is_collection_container_type(
"System.Collections.Generic.HashSet<System.String>"
));
assert!(!is_collection_container_type("PlaylistConfig"));
}
#[test]
fn decodes_and_replaces_whole_string_map_with_length_change() {
let parsed = UnitySerializedFile::from_slice(&synthetic_serialized_string_map()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected string map");
};
assert_eq!(fields[0].path, "texts");
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "texts[0]");
let UnitySerializedValue::Object(entry_fields) = &items[0].value else {
panic!("expected map entry object");
};
assert_eq!(entry_fields[0].path, "texts[0].first");
assert_eq!(
entry_fields[0].value,
UnitySerializedValue::String("jp".to_string())
);
assert_eq!(
entry_fields[1].value,
UnitySerializedValue::String("hello".to_string())
);
let rewritten = parsed
.replace_field_value(
1,
"texts",
Some(&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement("jp", "hello"),
map_entry_replacement("cn", "world"),
])),
&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement("jp", "こんにちは"),
map_entry_replacement("cn", "老師"),
map_entry_replacement("tw", "文本"),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected string map");
};
assert_eq!(items.len(), 3);
assert_eq!(map_entry_strings(&items[0]), ("jp", "こんにちは"));
assert_eq!(map_entry_strings(&items[1]), ("cn", "老師"));
assert_eq!(map_entry_strings(&items[2]), ("tw", "文本"));
}
#[test]
fn replaces_empty_string_map_from_typetree_schema() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_empty_string_map()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected string map");
};
assert!(items.is_empty());
let rewritten = parsed
.replace_field_value(
1,
"texts",
Some(&UnitySerializedReplacementValue::Map(vec![])),
&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement("jp", "こんにちは"),
map_entry_replacement("cn", "老師"),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected string map");
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].path, "texts[0]");
assert_eq!(map_entry_strings(&items[0]), ("jp", "こんにちは"));
assert_eq!(items[1].path, "texts[1]");
assert_eq!(map_entry_strings(&items[1]), ("cn", "老師"));
}
#[test]
fn decodes_and_replaces_key_value_string_map_schema() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_key_value_string_map()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected key/value string map");
};
assert_eq!(items.len(), 2);
let UnitySerializedValue::Object(entry_fields) = &items[0].value else {
panic!("expected key/value map entry object");
};
assert_eq!(entry_fields[0].path, "texts[0].key");
assert_eq!(entry_fields[1].path, "texts[0].value");
let rewritten = parsed
.replace_field_value(
1,
"texts",
Some(&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement_with_names("key", "value", "jp", "hello"),
map_entry_replacement_with_names("key", "value", "cn", "world"),
])),
&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement_with_names("key", "value", "jp", "こんにちは"),
map_entry_replacement_with_names("key", "value", "cn", "老師"),
map_entry_replacement_with_names("key", "value", "tw", "文本"),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected key/value string map");
};
assert_eq!(items.len(), 3);
assert_eq!(map_entry_strings(&items[0]), ("jp", "こんにちは"));
assert_eq!(map_entry_strings(&items[1]), ("cn", "老師"));
assert_eq!(map_entry_strings(&items[2]), ("tw", "文本"));
}
#[test]
fn decodes_and_replaces_scriptableobject_key_value_string_map_schema() {
let parsed = UnitySerializedFile::from_slice(
&synthetic_serialized_scriptableobject_key_value_string_map(),
)
.unwrap();
assert_eq!(parsed.types[0].class_id, 115);
assert_eq!(parsed.types[0].type_tree[0].type_name, "ScriptableObject");
assert_eq!(parsed.objects[0].class_id, 115);
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected ScriptableObject key/value string map");
};
assert_eq!(items.len(), 2);
assert_eq!(map_entry_strings(&items[0]), ("jp", "hello"));
assert_eq!(map_entry_strings(&items[1]), ("cn", "world"));
let rewritten = parsed
.replace_field_value(
1,
"texts",
Some(&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement_with_names("key", "value", "jp", "hello"),
map_entry_replacement_with_names("key", "value", "cn", "world"),
])),
&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement_with_names("key", "value", "jp", "こんにちは"),
map_entry_replacement_with_names("key", "value", "cn", "老師"),
map_entry_replacement_with_names("key", "value", "tw", "文本"),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected ScriptableObject key/value string map");
};
assert_eq!(items.len(), 3);
assert_eq!(map_entry_strings(&items[0]), ("jp", "こんにちは"));
assert_eq!(map_entry_strings(&items[1]), ("cn", "老師"));
assert_eq!(map_entry_strings(&items[2]), ("tw", "文本"));
}
#[test]
fn replaces_empty_key_value_string_map_from_typetree_schema() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_empty_key_value_string_map())
.unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected empty key/value string map");
};
assert!(items.is_empty());
let rewritten = parsed
.replace_field_value(
1,
"texts",
Some(&UnitySerializedReplacementValue::Map(vec![])),
&UnitySerializedReplacementValue::Map(vec![
map_entry_replacement_with_names("key", "value", "jp", "こんにちは"),
map_entry_replacement_with_names("key", "value", "cn", "老師"),
]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Map(items) = &fields[0].value else {
panic!("expected key/value string map");
};
assert_eq!(items.len(), 2);
assert_eq!(map_entry_strings(&items[0]), ("jp", "こんにちは"));
assert_eq!(map_entry_strings(&items[1]), ("cn", "老師"));
}
#[test]
fn replaces_signed_array_element_with_semantic_value() {
let parsed = UnitySerializedFile::from_slice(&synthetic_serialized_int_array()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected int array");
};
assert_eq!(items[0].path, "scores[0]");
assert_eq!(items[0].offset, 4);
assert_eq!(items[0].value, UnitySerializedValue::Signed(10));
assert_eq!(items[1].path, "scores[1]");
assert_eq!(items[1].offset, 8);
assert_eq!(items[1].value, UnitySerializedValue::Signed(20));
let rewritten = parsed
.replace_field_value(
1,
"scores[1]",
Some(&UnitySerializedReplacementValue::Signed(20)),
&UnitySerializedReplacementValue::Signed(42),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
let UnitySerializedValue::Array(items) = &fields[0].value else {
panic!("expected int array");
};
assert_eq!(items[0].value, UnitySerializedValue::Signed(10));
assert_eq!(items[1].value, UnitySerializedValue::Signed(42));
}
#[test]
fn decodes_and_replaces_unity_fixed_leaf_structs() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_unity_leaf_structs()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 6);
assert_eq!(fields[0].path, "tint");
assert_eq!(
fields[0].value,
UnitySerializedValue::Float32Struct {
type_name: "ColorRGBA".to_string(),
values: vec![
1.0f32.to_bits(),
0.5f32.to_bits(),
0.25f32.to_bits(),
1.0f32.to_bits(),
],
}
);
assert_eq!(fields[1].path, "guid");
assert_eq!(
fields[1].value,
UnitySerializedValue::FixedBytes {
type_name: "GUID".to_string(),
bytes: (0u8..16).collect(),
}
);
assert_eq!(fields[2].path, "position");
assert_eq!(
fields[2].value,
UnitySerializedValue::Float32Struct {
type_name: "Vector3f".to_string(),
values: vec![1.0f32.to_bits(), 2.0f32.to_bits(), 3.0f32.to_bits()],
}
);
assert_eq!(fields[3].path, "grid");
assert_eq!(
fields[3].value,
UnitySerializedValue::Int32Struct {
type_name: "Vector2Int".to_string(),
values: vec![10, -20],
}
);
assert_eq!(fields[4].path, "tile_rect");
assert_eq!(
fields[4].value,
UnitySerializedValue::Int32Struct {
type_name: "RectInt".to_string(),
values: vec![1, 2, 100, 200],
}
);
assert_eq!(fields[5].path, "bounds");
assert_eq!(
fields[5].value,
UnitySerializedValue::Float32Struct {
type_name: "AABB".to_string(),
values: vec![
0.0f32.to_bits(),
1.0f32.to_bits(),
2.0f32.to_bits(),
3.0f32.to_bits(),
4.0f32.to_bits(),
5.0f32.to_bits(),
],
}
);
let rewritten_position = parsed
.replace_field_value(
1,
"position",
Some(&UnitySerializedReplacementValue::Float32Struct {
type_name: "Vector3f".to_string(),
values: vec![1.0f32.to_bits(), 2.0f32.to_bits(), 3.0f32.to_bits()],
}),
&UnitySerializedReplacementValue::Float32Struct {
type_name: "Vector3f".to_string(),
values: vec![4.0f32.to_bits(), 5.0f32.to_bits(), 6.0f32.to_bits()],
},
)
.unwrap();
let reparsed_position = UnitySerializedFile::from_slice(&rewritten_position).unwrap();
let fields = reparsed_position.fields_for_object(1).unwrap();
assert_eq!(
fields[2].value,
UnitySerializedValue::Float32Struct {
type_name: "Vector3f".to_string(),
values: vec![4.0f32.to_bits(), 5.0f32.to_bits(), 6.0f32.to_bits()],
}
);
let replacement_guid: Vec<u8> = (16u8..32).collect();
let rewritten_guid = parsed
.replace_field_value(
1,
"guid",
Some(&UnitySerializedReplacementValue::FixedBytes {
type_name: "GUID".to_string(),
bytes: (0u8..16).collect(),
}),
&UnitySerializedReplacementValue::FixedBytes {
type_name: "GUID".to_string(),
bytes: replacement_guid.clone(),
},
)
.unwrap();
let reparsed_guid = UnitySerializedFile::from_slice(&rewritten_guid).unwrap();
let fields = reparsed_guid.fields_for_object(1).unwrap();
assert_eq!(
fields[1].value,
UnitySerializedValue::FixedBytes {
type_name: "GUID".to_string(),
bytes: replacement_guid,
}
);
let rewritten_grid = parsed
.replace_field_value(
1,
"grid",
Some(&UnitySerializedReplacementValue::Int32Struct {
type_name: "Vector2Int".to_string(),
values: vec![10, -20],
}),
&UnitySerializedReplacementValue::Int32Struct {
type_name: "Vector2Int".to_string(),
values: vec![30, 40],
},
)
.unwrap();
let reparsed_grid = UnitySerializedFile::from_slice(&rewritten_grid).unwrap();
let fields = reparsed_grid.fields_for_object(1).unwrap();
assert_eq!(
fields[3].value,
UnitySerializedValue::Int32Struct {
type_name: "Vector2Int".to_string(),
values: vec![30, 40],
}
);
}
#[test]
fn replaces_child_shaped_unity_structs_with_semantic_values() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_unity_child_structs()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 3);
assert_eq!(fields[0].path, "position");
let UnitySerializedValue::Object(position_fields) = &fields[0].value else {
panic!("expected child-shaped Vector3f object");
};
assert_eq!(position_fields[0].path, "position.x");
assert_eq!(
position_fields[0].value,
UnitySerializedValue::Float32(1.0f32.to_bits())
);
assert_eq!(position_fields[1].path, "position.y");
assert_eq!(position_fields[2].path, "position.z");
let rewritten_position = parsed
.replace_field_value(
1,
"position",
Some(&UnitySerializedReplacementValue::Float32Struct {
type_name: "Vector3f".to_string(),
values: vec![1.0f32.to_bits(), 2.0f32.to_bits(), 3.0f32.to_bits()],
}),
&UnitySerializedReplacementValue::Float32Struct {
type_name: "Vector3f".to_string(),
values: vec![4.0f32.to_bits(), 5.0f32.to_bits(), 6.0f32.to_bits()],
},
)
.unwrap();
let reparsed_position = UnitySerializedFile::from_slice(&rewritten_position).unwrap();
let fields = reparsed_position.fields_for_object(1).unwrap();
let UnitySerializedValue::Object(position_fields) = &fields[0].value else {
panic!("expected child-shaped Vector3f object");
};
assert_eq!(
position_fields[0].value,
UnitySerializedValue::Float32(4.0f32.to_bits())
);
assert_eq!(
position_fields[1].value,
UnitySerializedValue::Float32(5.0f32.to_bits())
);
assert_eq!(
position_fields[2].value,
UnitySerializedValue::Float32(6.0f32.to_bits())
);
let rewritten_grid = parsed
.replace_field_value(
1,
"grid",
Some(&UnitySerializedReplacementValue::Int32Struct {
type_name: "Vector2Int".to_string(),
values: vec![10, -20],
}),
&UnitySerializedReplacementValue::Int32Struct {
type_name: "Vector2Int".to_string(),
values: vec![30, 40],
},
)
.unwrap();
let reparsed_grid = UnitySerializedFile::from_slice(&rewritten_grid).unwrap();
let fields = reparsed_grid.fields_for_object(1).unwrap();
let UnitySerializedValue::Object(grid_fields) = &fields[1].value else {
panic!("expected child-shaped Vector2Int object");
};
assert_eq!(grid_fields[0].value, UnitySerializedValue::Signed(30));
assert_eq!(grid_fields[1].value, UnitySerializedValue::Signed(40));
let replacement_guid: Vec<u8> = (16u8..32).collect();
let rewritten_guid = parsed
.replace_field_value(
1,
"guid",
Some(&UnitySerializedReplacementValue::FixedBytes {
type_name: "GUID".to_string(),
bytes: (0u8..16).collect(),
}),
&UnitySerializedReplacementValue::FixedBytes {
type_name: "GUID".to_string(),
bytes: replacement_guid.clone(),
},
)
.unwrap();
let reparsed_guid = UnitySerializedFile::from_slice(&rewritten_guid).unwrap();
let fields = reparsed_guid.fields_for_object(1).unwrap();
let UnitySerializedValue::Object(guid_fields) = &fields[2].value else {
panic!("expected child-shaped GUID object");
};
let actual_guid = guid_fields
.iter()
.map(|field| match field.value {
UnitySerializedValue::Unsigned(value) => value as u8,
_ => panic!("expected UInt8 GUID field"),
})
.collect::<Vec<_>>();
assert_eq!(actual_guid, replacement_guid);
}
#[test]
fn replaces_unknown_fixed_field_with_same_length_bytes() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_unknown_fixed_field()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "blob");
assert_eq!(
fields[0].value,
UnitySerializedValue::Unknown {
type_name: "CustomBlob".to_string(),
bytes: vec![1, 2, 3, 4],
}
);
let rewritten = parsed
.replace_field_value(
1,
"blob",
Some(&UnitySerializedReplacementValue::Bytes(vec![1, 2, 3, 4])),
&UnitySerializedReplacementValue::Bytes(vec![9, 8, 7, 6]),
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
assert_eq!(
fields[0].value,
UnitySerializedValue::Unknown {
type_name: "CustomBlob".to_string(),
bytes: vec![9, 8, 7, 6],
}
);
let error = parsed
.replace_field_value(
1,
"blob",
None,
&UnitySerializedReplacementValue::Bytes(vec![1, 2, 3]),
)
.unwrap_err()
.to_string();
assert!(error.contains("must match current byte length"));
}
#[test]
fn decodes_and_replaces_enum_field_with_semantic_value() {
let parsed = UnitySerializedFile::from_slice(&synthetic_serialized_enum_field()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "difficulty");
assert_eq!(fields[0].type_name, "ScenarioDifficulty");
assert_eq!(fields[0].byte_size, 4);
assert_eq!(
fields[0].value,
UnitySerializedValue::Enum {
type_name: "ScenarioDifficulty".to_string(),
storage_type: "int".to_string(),
value: 2,
}
);
let rewritten = parsed
.replace_field_value(
1,
"difficulty",
Some(&UnitySerializedReplacementValue::Enum {
type_name: "ScenarioDifficulty".to_string(),
storage_type: "int".to_string(),
value: 2,
}),
&UnitySerializedReplacementValue::Enum {
type_name: "ScenarioDifficulty".to_string(),
storage_type: "int".to_string(),
value: 3,
},
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
assert_eq!(
fields[0].value,
UnitySerializedValue::Enum {
type_name: "ScenarioDifficulty".to_string(),
storage_type: "int".to_string(),
value: 3,
}
);
}
#[test]
fn decodes_and_replaces_layer_mask_bitfield_with_semantic_value() {
let parsed =
UnitySerializedFile::from_slice(&synthetic_serialized_bitfield_field()).unwrap();
let fields = parsed.fields_for_object(1).unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "target_layers");
assert_eq!(fields[0].type_name, "LayerMask");
assert_eq!(fields[0].byte_size, 4);
assert_eq!(
fields[0].value,
UnitySerializedValue::BitField {
type_name: "LayerMask".to_string(),
storage_type: "UInt32".to_string(),
bits: 5,
}
);
let rewritten = parsed
.replace_field_value(
1,
"target_layers",
Some(&UnitySerializedReplacementValue::BitField {
type_name: "LayerMask".to_string(),
storage_type: "UInt32".to_string(),
bits: 5,
}),
&UnitySerializedReplacementValue::BitField {
type_name: "LayerMask".to_string(),
storage_type: "UInt32".to_string(),
bits: 9,
},
)
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
let fields = reparsed.fields_for_object(1).unwrap();
assert_eq!(
fields[0].value,
UnitySerializedValue::BitField {
type_name: "LayerMask".to_string(),
storage_type: "UInt32".to_string(),
bits: 9,
}
);
}
fn map_entry_replacement(key: &str, value: &str) -> UnitySerializedReplacementValue {
map_entry_replacement_with_names("first", "second", key, value)
}
fn map_entry_replacement_with_names(
key_name: &str,
value_name: &str,
key: &str,
value: &str,
) -> UnitySerializedReplacementValue {
UnitySerializedReplacementValue::Object(vec![
UnitySerializedFieldReplacement {
name: key_name.to_string(),
value: UnitySerializedReplacementValue::String(key.to_string()),
},
UnitySerializedFieldReplacement {
name: value_name.to_string(),
value: UnitySerializedReplacementValue::String(value.to_string()),
},
])
}
fn map_entry_strings(entry: &UnitySerializedField) -> (&str, &str) {
let UnitySerializedValue::Object(fields) = &entry.value else {
panic!("expected map entry object");
};
let UnitySerializedValue::String(key) = &fields[0].value else {
panic!("expected string key");
};
let UnitySerializedValue::String(value) = &fields[1].value else {
panic!("expected string value");
};
(key, value)
}
#[test]
fn replaces_text_asset_and_updates_file_size() {
let parsed = UnitySerializedFile::from_slice(&synthetic_serialized_file()).unwrap();
let rewritten = parsed
.replace_text_asset(1, Some("GameMainConfig"), b"rewritten text")
.unwrap();
let reparsed = UnitySerializedFile::from_slice(&rewritten).unwrap();
assert_eq!(
reparsed.text_asset("GameMainConfig").unwrap().bytes,
b"rewritten text"
);
assert_eq!(
u64::from_be_bytes(rewritten[24..32].try_into().unwrap()) as usize,
rewritten.len()
);
}
}