//! 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), /// 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, }, /// 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, }, /// 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, }, /// 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), /// Repeated pair/object fields decoded from a map node. Map(Vec), /// Nested object fields. Object(Vec), /// 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, /// Decoded managed-reference fields. fields: Vec, /// Raw bytes retained when the TypeTree node has no children but a /// fixed byte size. bytes: Vec, }, /// Unity managed-reference registry decoded from TypeTree-covered fields. ManagedReferenceRegistry { /// Registry records inferred from `references` array entries. references: Vec, /// Decoded raw registry fields. These preserve all original field paths /// and are used for text extraction and field patch lookup. fields: Vec, }, /// 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, }, } /// 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, /// 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, /// Managed class or concrete type name. #[serde(default, skip_serializing_if = "Option::is_none")] pub type_name: Option, /// Managed namespace. #[serde(default, skip_serializing_if = "Option::is_none")] pub namespace: Option, /// Managed assembly name. #[serde(default, skip_serializing_if = "Option::is_none")] pub assembly_name: Option, } 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, } /// 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), /// 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, }, /// 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, }, /// Fixed-size Unity byte value type. FixedBytes { /// TypeTree type name. type_name: String, /// Raw bytes in serialized field order. bytes: Vec, }, /// 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), /// Whole-map replacement. Existing entries or the TypeTree data node are /// used as the entry encoding schema. Map(Vec), /// Replacement for one TypeTree object value. Object(Vec), } /// 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> { 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> { 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> { 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, /// 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, /// 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, } /// 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, /// 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, /// Object table entries declared by the file. pub objects: Vec, /// Serialized file bytes retained for field-level object decoding. raw_data: Vec, /// 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, /// Byte order used by serialized metadata and object data. endian: Endian, text_assets: Vec, } impl UnitySerializedFile { /// Parses a serialized file from raw bytes. pub fn from_slice(data: &[u8]) -> Result { Self::from_named_slice(None::, data) } /// Parses a serialized file from raw bytes with a source path. pub fn from_named_slice(path: impl Into>, data: &[u8]) -> Result { 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) -> Result { 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> { 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> { 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> { 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> { 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> { 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> { 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, ) -> Result> { 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::>(); 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, /// 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, } /// 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, } /// 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { children .iter() .copied() .find(|index| nodes[*index].type_name == "Array" || nodes[*index].name == "Array") } fn collection_array_children(nodes: &[UnityTypeTreeNode], index: usize) -> Vec { 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 { 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 { 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 { match normalized_metadata_key(type_name).as_str() { "guid" | "hash128" => Some(16), _ => None, } } fn enum_value_child_index(nodes: &[UnityTypeTreeNode], children: &[usize]) -> Option { 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 { 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 { 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 { let mut records = Vec::new(); collect_managed_reference_records(fields, &mut records); records } fn collect_managed_reference_records( fields: &[UnitySerializedField], records: &mut Vec, ) { 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 { 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 { 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, namespace: Option, assembly_name: Option, } 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, Option) { 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 { 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 { 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> { 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> { 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> { 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> { 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> { 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> { 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 { 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, context: ReplacementEncodingContext<'_>, ) -> Result> { 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, context: ReplacementEncodingContext<'_>, ) -> Result> { 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> { 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> { 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> { 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> { 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 { 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, node: &UnityTypeTreeNode) -> Result> { 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> { 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> { 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> { 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, field_byte_size: usize, type_name: &str, ) -> Result> { 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, alignment: usize) { let remainder = output.len() % alignment; if remainder != 0 { output.resize(output.len() + alignment - remainder, 0); } } fn push_u32_endian(output: &mut Vec, 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, 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, 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, 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, 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, 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 { let mut output = Vec::with_capacity(4); push_u32_endian(&mut output, value, endian); output } fn encode_u64_value(value: u64, endian: Endian) -> Vec { 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 { 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> { 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> { 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, } 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"), 476 => Some("PPtr"), 493 => Some("PPtr"), 508 => Some("PPtr"), 528 => Some("PPtr"), 546 => Some("PPtr"), 559 => Some("PPtr"), 572 => Some("PPtr"), 585 => Some("PPtr"), 601 => Some("PPtr"), 615 => Some("PPtr"), 631 => Some("PPtr"), 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 { Ok(self.read_bytes(1, field)?[0]) } fn read_i8(&mut self, field: &str) -> Result { Ok(self.read_u8(field)? as i8) } fn read_u16(&mut self, field: &str) -> Result { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, value: i16) { data.extend_from_slice(&value.to_le_bytes()); } fn push_u32_le(data: &mut Vec, value: u32) { data.extend_from_slice(&value.to_le_bytes()); } fn push_i32_le(data: &mut Vec, value: i32) { data.extend_from_slice(&value.to_le_bytes()); } fn push_i64_le(data: &mut Vec, value: i64) { data.extend_from_slice(&value.to_le_bytes()); } fn push_u64_le(data: &mut Vec, value: u64) { data.extend_from_slice(&value.to_le_bytes()); } fn push_u32_be(data: &mut Vec, value: u32) { data.extend_from_slice(&value.to_be_bytes()); } fn push_u64_be(data: &mut Vec, value: u64) { data.extend_from_slice(&value.to_be_bytes()); } fn align(data: &mut Vec, alignment: usize) { let remainder = data.len() % alignment; if remainder != 0 { data.resize(data.len() + alignment - remainder, 0); } } fn synthetic_serialized_file() -> Vec { 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 { 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 { synthetic_serialized_managed_reference_with_type(b"managedReference") } fn synthetic_serialized_managed_reference_with_type(managed_type_name: &[u8]) -> Vec { 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 { 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 { 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 { 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 { synthetic_serialized_string_array_with_values(&["hello", "world"]) } fn synthetic_serialized_empty_string_array() -> Vec { synthetic_serialized_string_array_with_values(&[]) } fn synthetic_serialized_vector_string_array() -> Vec { synthetic_serialized_vector_string_array_with_values(&["hello", "world"]) } fn synthetic_serialized_empty_vector_string_array() -> Vec { synthetic_serialized_vector_string_array_with_values(&[]) } fn synthetic_serialized_list_string_array() -> Vec { synthetic_serialized_collection_string_array_with_values( b"List", &["hello", "world"], ) } fn synthetic_serialized_empty_hashset_string_array() -> Vec { synthetic_serialized_collection_string_array_with_values(b"HashSet", &[]) } fn synthetic_serialized_vector_string_array_with_values(values: &[&str]) -> Vec { 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 { 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 { 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 { 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 { 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 = (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 { fn push_string(strings: &mut Vec, 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::>(); 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 { 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 { 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 { 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 { synthetic_serialized_string_map_with_entries(&[("jp", "hello"), ("cn", "world")]) } fn synthetic_serialized_empty_string_map() -> Vec { synthetic_serialized_string_map_with_entries(&[]) } fn synthetic_serialized_string_map_with_entries(entries: &[(&str, &str)]) -> Vec { synthetic_serialized_string_map_with_entry_names(entries, b"first", b"second") } fn synthetic_serialized_key_value_string_map() -> Vec { synthetic_serialized_string_map_with_entry_names( &[("jp", "hello"), ("cn", "world")], b"key", b"value", ) } fn synthetic_serialized_scriptableobject_key_value_string_map() -> Vec { 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 { 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 { 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 { 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"); let UnitySerializedValue::Array(items) = &fields[0].value else { panic!("expected List 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 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"); let UnitySerializedValue::Array(items) = &fields[0].value else { panic!("expected empty HashSet 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 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")); assert!(is_collection_container_type("HashSet")); assert!(is_collection_container_type( "System.Collections.Generic.List" )); assert!(is_collection_container_type( "System.Collections.Generic.HashSet" )); 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 = (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 = (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::>(); 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() ); } }