mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 08:54:55 +08:00
补齐官方 release 解析缓存、TextUnit 明细索引、资源变更集、Crowdin handoff 预留、ResourceRepository 导入元数据和 localized release patch 前置链路。 同时开放文件级 patch.apply 与 UnityFS TextAsset/string/semantic field patch CLI/RPC 入口,并保留官方原版资源与汉化产物双目录发布状态。 验证:cargo test -p bat-assetbundle --locked;cargo clippy -p bat-assetbundle --all-targets --locked -- -D warnings;cargo test -p bat-infrastructure --locked。
6860 lines
252 KiB
Rust
6860 lines
252 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,
|
|
}]),
|
|
}
|
|
}
|
|
|
|
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,
|
|
¤t_item.value,
|
|
¤t_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,
|
|
¤t_field.value,
|
|
¤t_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()
|
|
);
|
|
}
|
|
}
|