//! Minimal UnityFS TextAsset patching. use crate::error::{AssetBundleError, Result}; use crate::parser::UnityFsParser; use crate::serialized::{ UnitySerializedField, UnitySerializedReplacementValue, UnitySerializedValue, }; use crate::types::UnityFsBundle; use md5::{Digest, Md5}; /// One TextAsset replacement inside a serialized UnityFS directory file. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TextAssetPatch { /// UnityFS directory path of the serialized file. pub serialized_file_path: String, /// Unity object path ID. pub path_id: i64, /// Optional expected TextAsset name. pub expected_name: Option, /// Replacement raw bytes. pub replacement: Vec, } impl TextAssetPatch { /// Creates a TextAsset replacement specification. pub fn new( serialized_file_path: impl Into, path_id: i64, replacement: impl Into>, ) -> Self { Self { serialized_file_path: serialized_file_path.into(), path_id, expected_name: None, replacement: replacement.into(), } } } /// One TypeTree string-field replacement inside a serialized UnityFS directory file. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StringFieldPatch { /// UnityFS directory path of the serialized file. pub serialized_file_path: String, /// Unity object path ID. pub path_id: i64, /// Stable TypeTree field path. pub field_path: String, /// Optional expected source string. pub expected_value: Option, /// Replacement string. pub replacement: String, } impl StringFieldPatch { /// Creates a string-field replacement specification. pub fn new( serialized_file_path: impl Into, path_id: i64, field_path: impl Into, replacement: impl Into, ) -> Self { Self { serialized_file_path: serialized_file_path.into(), path_id, field_path: field_path.into(), expected_value: None, replacement: replacement.into(), } } } /// One semantic TypeTree field replacement inside a serialized UnityFS directory file. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldPatch { /// UnityFS directory path of the serialized file. pub serialized_file_path: String, /// Unity object path ID. pub path_id: i64, /// Stable TypeTree field path. pub field_path: String, /// Optional expected source value. pub expected_value: Option, /// Replacement value encoded according to the current TypeTree field type. pub replacement: UnitySerializedReplacementValue, } impl FieldPatch { /// Creates a semantic field replacement specification. pub fn new( serialized_file_path: impl Into, path_id: i64, field_path: impl Into, replacement: UnitySerializedReplacementValue, ) -> Self { Self { serialized_file_path: serialized_file_path.into(), path_id, field_path: field_path.into(), expected_value: None, replacement, } } } /// Patches one TextAsset and rebuilds the UnityFS container. /// /// The rebuilt bundle uses a single uncompressed data block. This keeps the /// patch path deterministic and avoids relying on a compressor-specific /// implementation while preserving all directory file paths and metadata. pub fn patch_unityfs_text_asset(data: &[u8], patch: &TextAssetPatch) -> Result> { let parser = UnityFsParser::new(); let mut bundle = parser.parse_bytes(data)?; let serialized = bundle .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some(patch.serialized_file_path.as_str())) .ok_or_else(|| { AssetBundleError::Parse(format!( "serialized file {} not found in UnityFS bundle", patch.serialized_file_path )) })?; let rewritten_serialized = serialized.replace_text_asset( patch.path_id, patch.expected_name.as_deref(), &patch.replacement, )?; let file = bundle .files .iter_mut() .find(|file| file.path == patch.serialized_file_path) .ok_or_else(|| { AssetBundleError::Parse(format!( "UnityFS directory file {} not found", patch.serialized_file_path )) })?; file.data = rewritten_serialized; file.size = file.data.len() as u64; let rebuilt = rebuild_unityfs(&bundle)?; let verified = parser.parse_bytes(&rebuilt)?; let asset = verified .text_assets .iter() .find(|asset| asset.path_id == patch.path_id) .ok_or_else(|| { AssetBundleError::Parse(format!( "patched TextAsset path_id {} was not found after rebuild", patch.path_id )) })?; if asset.bytes != patch.replacement { return Err(AssetBundleError::Parse(format!( "patched TextAsset path_id {} failed post-build verification", patch.path_id ))); } Ok(rebuilt) } /// Patches one TypeTree string field and rebuilds the UnityFS container. pub fn patch_unityfs_string_field(data: &[u8], patch: &StringFieldPatch) -> Result> { let parser = UnityFsParser::new(); let mut bundle = parser.parse_bytes(data)?; let serialized = bundle .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some(patch.serialized_file_path.as_str())) .ok_or_else(|| { AssetBundleError::Parse(format!( "serialized file {} not found in UnityFS bundle", patch.serialized_file_path )) })?; let rewritten_serialized = serialized.replace_string_field( patch.path_id, &patch.field_path, patch.expected_value.as_deref(), &patch.replacement, )?; let file = bundle .files .iter_mut() .find(|file| file.path == patch.serialized_file_path) .ok_or_else(|| { AssetBundleError::Parse(format!( "UnityFS directory file {} not found", patch.serialized_file_path )) })?; file.data = rewritten_serialized; file.size = file.data.len() as u64; let rebuilt = rebuild_unityfs(&bundle)?; let verified = parser.parse_bytes(&rebuilt)?; let serialized = verified .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some(patch.serialized_file_path.as_str())) .ok_or_else(|| { AssetBundleError::Parse(format!( "patched serialized file {} was not found after rebuild", patch.serialized_file_path )) })?; let fields = serialized.fields_for_object(patch.path_id)?; let value = find_string_field_value(&fields, &patch.field_path).ok_or_else(|| { AssetBundleError::Parse(format!( "patched field {} was not found after rebuild", patch.field_path )) })?; if value != patch.replacement { return Err(AssetBundleError::Parse(format!( "patched field {} failed post-build verification", patch.field_path ))); } Ok(rebuilt) } /// Patches one semantic TypeTree field and rebuilds the UnityFS container. pub fn patch_unityfs_field(data: &[u8], patch: &FieldPatch) -> Result> { let parser = UnityFsParser::new(); let mut bundle = parser.parse_bytes(data)?; let serialized = bundle .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some(patch.serialized_file_path.as_str())) .ok_or_else(|| { AssetBundleError::Parse(format!( "serialized file {} not found in UnityFS bundle", patch.serialized_file_path )) })?; let rewritten_serialized = serialized.replace_field_value( patch.path_id, &patch.field_path, patch.expected_value.as_ref(), &patch.replacement, )?; let file = bundle .files .iter_mut() .find(|file| file.path == patch.serialized_file_path) .ok_or_else(|| { AssetBundleError::Parse(format!( "UnityFS directory file {} not found", patch.serialized_file_path )) })?; file.data = rewritten_serialized; file.size = file.data.len() as u64; let rebuilt = rebuild_unityfs(&bundle)?; let verified = parser.parse_bytes(&rebuilt)?; let serialized = verified .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some(patch.serialized_file_path.as_str())) .ok_or_else(|| { AssetBundleError::Parse(format!( "patched serialized file {} was not found after rebuild", patch.serialized_file_path )) })?; let fields = serialized.fields_for_object(patch.path_id)?; let value = find_field_value(&fields, &patch.field_path).ok_or_else(|| { AssetBundleError::Parse(format!( "patched field {} was not found after rebuild", patch.field_path )) })?; if !patch.replacement.matches_serialized_value(value) { return Err(AssetBundleError::Parse(format!( "patched field {} failed post-build verification", patch.field_path ))); } Ok(rebuilt) } fn rebuild_unityfs(bundle: &UnityFsBundle) -> Result> { if bundle.files.len() != bundle.directories.len() { return Err(AssetBundleError::Parse(format!( "UnityFS file/directory count mismatch: files={}, directories={}", bundle.files.len(), bundle.directories.len() ))); } let mut uncompressed_data = Vec::new(); let mut directory_offsets = Vec::with_capacity(bundle.files.len()); for file in &bundle.files { let offset = u64::try_from(uncompressed_data.len()) .map_err(|_| AssetBundleError::Parse("UnityFS data offset overflow".to_string()))?; directory_offsets.push(offset); uncompressed_data.extend_from_slice(&file.data); } let data_size = u32::try_from(uncompressed_data.len()).map_err(|_| { AssetBundleError::Parse("UnityFS rebuilt data exceeds u32 size".to_string()) })?; let mut blocks_info_body = Vec::new(); push_i32_be(&mut blocks_info_body, 1); push_u32_be(&mut blocks_info_body, data_size); push_u32_be(&mut blocks_info_body, data_size); push_u16_be(&mut blocks_info_body, 0); push_i32_be( &mut blocks_info_body, i32::try_from(bundle.files.len()).map_err(|_| { AssetBundleError::Parse("UnityFS directory count exceeds i32".to_string()) })?, ); for (file, offset) in bundle.files.iter().zip(directory_offsets) { push_u64_be(&mut blocks_info_body, offset); push_u64_be( &mut blocks_info_body, u64::try_from(file.data.len()) .map_err(|_| AssetBundleError::Parse("UnityFS file size overflow".to_string()))?, ); push_u32_be(&mut blocks_info_body, file.flags); push_c_string(&mut blocks_info_body, &file.path); } let digest = Md5::digest(&blocks_info_body); let mut blocks_info = Vec::with_capacity(16 + blocks_info_body.len()); blocks_info.extend_from_slice(&digest); blocks_info.extend_from_slice(&blocks_info_body); let mut output = Vec::new(); push_c_string(&mut output, "UnityFS"); push_u32_be(&mut output, bundle.header.format_version); push_c_string(&mut output, &bundle.header.target_version); push_c_string(&mut output, &bundle.header.unity_version); let total_size_offset = output.len(); push_u64_be(&mut output, 0); push_u32_be( &mut output, u32::try_from(blocks_info.len()).map_err(|_| { AssetBundleError::Parse("UnityFS block info exceeds u32 size".to_string()) })?, ); push_u32_be( &mut output, u32::try_from(blocks_info.len()).map_err(|_| { AssetBundleError::Parse("UnityFS block info exceeds u32 size".to_string()) })?, ); push_u32_be(&mut output, 0); if bundle.header.format_version >= 7 { align_vec(&mut output, 16); } output.extend_from_slice(&blocks_info); output.extend_from_slice(&uncompressed_data); let total_size = u64::try_from(output.len()) .map_err(|_| AssetBundleError::Parse("UnityFS rebuilt size overflow".to_string()))?; output[total_size_offset..total_size_offset + 8].copy_from_slice(&total_size.to_be_bytes()); Ok(output) } fn find_field_value<'a>( fields: &'a [UnitySerializedField], field_path: &str, ) -> Option<&'a UnitySerializedValue> { for field in fields { if field.path == field_path { return Some(&field.value); } match &field.value { UnitySerializedValue::Object(children) | UnitySerializedValue::ManagedReference { fields: children, .. } | UnitySerializedValue::ManagedReferenceRegistry { fields: children, .. } | UnitySerializedValue::Array(children) | UnitySerializedValue::Map(children) => { if let Some(found) = find_field_value(children, field_path) { return Some(found); } } _ => {} } } None } fn find_string_field_value<'a>( fields: &'a [UnitySerializedField], field_path: &str, ) -> Option<&'a str> { for field in fields { if field.path == field_path { if let UnitySerializedValue::String(value) = &field.value { return Some(value); } } match &field.value { UnitySerializedValue::Object(children) | UnitySerializedValue::ManagedReference { fields: children, .. } | UnitySerializedValue::ManagedReferenceRegistry { fields: children, .. } | UnitySerializedValue::Array(children) | UnitySerializedValue::Map(children) => { if let Some(found) = find_string_field_value(children, field_path) { return Some(found); } } _ => {} } } None } fn push_c_string(output: &mut Vec, value: &str) { output.extend_from_slice(value.as_bytes()); output.push(0); } fn push_i32_be(output: &mut Vec, value: i32) { output.extend_from_slice(&value.to_be_bytes()); } fn push_u16_be(output: &mut Vec, value: u16) { output.extend_from_slice(&value.to_be_bytes()); } fn push_u32_be(output: &mut Vec, value: u32) { output.extend_from_slice(&value.to_be_bytes()); } fn push_u64_be(output: &mut Vec, value: u64) { output.extend_from_slice(&value.to_be_bytes()); } fn align_vec(output: &mut Vec, alignment: usize) { let remainder = output.len() % alignment; if remainder != 0 { output.resize(output.len() + alignment - remainder, 0); } } #[cfg(test)] mod tests { use super::*; use crate::parser::Parser; use crate::serialized::UnitySerializedFieldReplacement; fn synthetic_bundle(payload: &[u8]) -> Vec { synthetic_bundle_with_path(payload, "CAB-test") } fn synthetic_bundle_with_path(payload: &[u8], path: &str) -> Vec { let mut blocks_info = vec![0; 16]; push_i32_be(&mut blocks_info, 1); push_u32_be(&mut blocks_info, payload.len() as u32); push_u32_be(&mut blocks_info, payload.len() as u32); push_u16_be(&mut blocks_info, 0); push_i32_be(&mut blocks_info, 1); push_u64_be(&mut blocks_info, 0); push_u64_be(&mut blocks_info, payload.len() as u64); push_u32_be(&mut blocks_info, 0); push_c_string(&mut blocks_info, path); let mut data = Vec::new(); push_c_string(&mut data, "UnityFS"); push_u32_be(&mut data, 8); push_c_string(&mut data, "5.x.x"); push_c_string(&mut data, "2021.3.56f2"); let total_size_offset = data.len(); push_u64_be(&mut data, 0); push_u32_be(&mut data, blocks_info.len() as u32); push_u32_be(&mut data, blocks_info.len() as u32); push_u32_be(&mut data, 0); align_vec(&mut data, 16); data.extend_from_slice(&blocks_info); data.extend_from_slice(payload); let total_size = data.len() as u64; data[total_size_offset..total_size_offset + 8].copy_from_slice(&total_size.to_be_bytes()); data } fn push_i16_le(data: &mut Vec, value: i16) { 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_u32_le(data: &mut Vec, value: u32) { 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 synthetic_serialized_text_asset(bytes: &[u8]) -> Vec { let mut object_data = Vec::new(); push_u32_le(&mut object_data, 8); object_data.extend_from_slice(b"Scenario"); align_vec(&mut object_data, 4); push_u32_le(&mut object_data, bytes.len() as u32); object_data.extend_from_slice(bytes); 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_vec(&mut metadata, 4); push_u64_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_vec(&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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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_payload_name(b"data") } fn synthetic_serialized_managed_reference_registry_with_managed_reference_data() -> Vec { synthetic_serialized_managed_reference_registry_with_payload_name(b"managedReferenceData") } fn synthetic_serialized_managed_reference_registry_with_payload_name( payload_field_name: &[u8], ) -> Vec { let mut object_data = Vec::new(); push_i32_le(&mut object_data, 1); object_data.extend_from_slice(&42i64.to_le_bytes()); 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_vec(&mut object_data, 4); } push_u32_le(&mut object_data, "こんにちは".len() as u32); object_data.extend_from_slice("こんにちは".as_bytes()); align_vec(&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(b"managedReferencesRegistry\0"); let registry_name = strings.len(); strings.extend_from_slice(b"m_SerializedReferences\0"); let array_type = strings.len(); strings.extend_from_slice(b"Array\0"); let array_name = strings.len(); strings.extend_from_slice(b"references\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(b"rid\0"); let type_info_type = strings.len(); strings.extend_from_slice(b"ManagedReferenceType\0"); let type_info_name = strings.len(); strings.extend_from_slice(b"type\0"); let string_type = strings.len(); strings.extend_from_slice(b"string\0"); let class_name = strings.len(); strings.extend_from_slice(b"class\0"); let namespace_name = strings.len(); strings.extend_from_slice(b"ns\0"); let assembly_name = strings.len(); strings.extend_from_slice(b"asm\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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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 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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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_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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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 { let mut object_data = Vec::new(); push_i32_le(&mut object_data, 2); push_u32_le(&mut object_data, 5); object_data.extend_from_slice(b"hello"); align_vec(&mut object_data, 4); push_u32_le(&mut object_data, 5); object_data.extend_from_slice(b"world"); align_vec(&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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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_vector_string_array() -> Vec { let mut object_data = Vec::new(); push_i32_le(&mut object_data, 2); push_u32_le(&mut object_data, 5); object_data.extend_from_slice(b"hello"); align_vec(&mut object_data, 4); push_u32_le(&mut object_data, 5); object_data.extend_from_slice(b"world"); align_vec(&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(b"vector\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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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_entry_names(b"first", b"second") } fn synthetic_serialized_key_value_string_map() -> Vec { synthetic_serialized_string_map_with_entry_names(b"key", b"value") } fn synthetic_serialized_scriptableobject_key_value_string_map() -> Vec { synthetic_serialized_string_map_with_entry_names_and_root( b"key", b"value", b"ScriptableObject", 115, ) } fn synthetic_serialized_string_map_with_entry_names( first_field_name: &[u8], second_field_name: &[u8], ) -> Vec { synthetic_serialized_string_map_with_entry_names_and_root( first_field_name, second_field_name, b"MonoBehaviour", 114, ) } fn synthetic_serialized_string_map_with_entry_names_and_root( 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, 2); for (key, value) in [("jp", "hello"), ("cn", "world")] { push_u32_le(&mut object_data, key.len() as u32); object_data.extend_from_slice(key.as_bytes()); align_vec(&mut object_data, 4); push_u32_le(&mut object_data, value.len() as u32); object_data.extend_from_slice(value.as_bytes()); align_vec(&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), ] { metadata.extend_from_slice(&1u16.to_le_bytes()); 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_vec(&mut metadata, 4); metadata.extend_from_slice(&1i64.to_le_bytes()); 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 rebuilds_uncompressed_unityfs_without_changing_directory_paths() { let parsed = UnityFsParser::new() .parse(&synthetic_bundle(b"payload")) .unwrap(); let rebuilt = rebuild_unityfs( &UnityFsParser::new() .parse_bytes(&synthetic_bundle(b"payload")) .unwrap(), ) .unwrap(); let reparsed = UnityFsParser::new().parse(&rebuilt).unwrap(); assert_eq!(reparsed.directories[0].path, "CAB-test"); assert_eq!(reparsed.files[0].data, b"payload"); assert_eq!(parsed.unity_version, reparsed.unity_version); } #[test] fn patches_text_asset_and_verifies_reparsed_payload() { let original_text = "こんにちは".as_bytes(); let bundle = synthetic_bundle_with_path( &synthetic_serialized_text_asset(original_text), "CAB-scenario", ); let replacement = "你好,阿拜多斯".as_bytes().to_vec(); let patched = patch_unityfs_text_asset( &bundle, &TextAssetPatch { serialized_file_path: "CAB-scenario".to_string(), path_id: 1, expected_name: Some("Scenario".to_string()), replacement: replacement.clone(), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let asset = reparsed .text_assets .iter() .find(|asset| asset.path_id == 1) .unwrap(); assert_eq!(reparsed.files[0].path, "CAB-scenario"); assert_eq!(asset.name, "Scenario"); assert_eq!(asset.bytes, replacement); } #[test] fn patches_monobehaviour_string_field_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_monobehaviour(), "CAB-story"); let patched = patch_unityfs_string_field( &bundle, &StringFieldPatch { serialized_file_path: "CAB-story".to_string(), path_id: 1, field_path: "message".to_string(), expected_value: Some("hello".to_string()), replacement: "你好".to_string(), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-story")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); assert_eq!(reparsed.files[0].path, "CAB-story"); assert_eq!( fields[0].value, UnitySerializedValue::String("你好".to_string()) ); } #[test] fn patches_managed_reference_payload_string_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path( &synthetic_serialized_managed_reference_registry(), "CAB-managed-story", ); let patched = patch_unityfs_string_field( &bundle, &StringFieldPatch { serialized_file_path: "CAB-managed-story".to_string(), path_id: 1, field_path: "m_SerializedReferences.references[0].data.message".to_string(), expected_value: Some("こんにちは".to_string()), replacement: "你好".to_string(), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-managed-story")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value else { panic!("expected managed reference registry"); }; 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[0].value, UnitySerializedValue::String("你好".to_string()) ); } #[test] fn patches_managed_reference_data_payload_string_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path( &synthetic_serialized_managed_reference_registry_with_managed_reference_data(), "CAB-managed-data-story", ); let patched = patch_unityfs_string_field( &bundle, &StringFieldPatch { serialized_file_path: "CAB-managed-data-story".to_string(), path_id: 1, field_path: "m_SerializedReferences.references[0].managedReferenceData.message" .to_string(), expected_value: Some("こんにちは".to_string()), replacement: "你好".to_string(), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-managed-data-story")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); let UnitySerializedValue::ManagedReferenceRegistry { references, .. } = &fields[0].value else { panic!("expected managed reference registry"); }; 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()) ); } #[test] fn patches_unknown_fixed_bytes_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_unknown_fixed_field(), "CAB-unknown"); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-unknown".to_string(), path_id: 1, field_path: "blob".to_string(), expected_value: Some(UnitySerializedReplacementValue::Bytes(vec![1, 2, 3, 4])), replacement: UnitySerializedReplacementValue::Bytes(vec![9, 8, 7, 6]), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-unknown")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); assert_eq!(reparsed.files[0].path, "CAB-unknown"); assert_eq!( fields[0].value, UnitySerializedValue::Unknown { type_name: "CustomBlob".to_string(), bytes: vec![9, 8, 7, 6], } ); } #[test] fn patches_enum_field_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_enum_field(), "CAB-enum-story"); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-enum-story".to_string(), path_id: 1, field_path: "difficulty".to_string(), expected_value: Some(UnitySerializedReplacementValue::Enum { type_name: "ScenarioDifficulty".to_string(), storage_type: "int".to_string(), value: 2, }), replacement: UnitySerializedReplacementValue::Enum { type_name: "ScenarioDifficulty".to_string(), storage_type: "int".to_string(), value: 3, }, }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-enum-story")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); assert_eq!(reparsed.files[0].path, "CAB-enum-story"); assert_eq!( fields[0].value, UnitySerializedValue::Enum { type_name: "ScenarioDifficulty".to_string(), storage_type: "int".to_string(), value: 3, } ); } #[test] fn patches_layer_mask_bitfield_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_bitfield_field(), "CAB-mask-story"); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-mask-story".to_string(), path_id: 1, field_path: "target_layers".to_string(), expected_value: Some(UnitySerializedReplacementValue::BitField { type_name: "LayerMask".to_string(), storage_type: "UInt32".to_string(), bits: 5, }), replacement: UnitySerializedReplacementValue::BitField { type_name: "LayerMask".to_string(), storage_type: "UInt32".to_string(), bits: 9, }, }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-mask-story")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); assert_eq!(reparsed.files[0].path, "CAB-mask-story"); assert_eq!( fields[0].value, UnitySerializedValue::BitField { type_name: "LayerMask".to_string(), storage_type: "UInt32".to_string(), bits: 9, } ); } #[test] fn patches_string_array_element_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_string_array(), "CAB-array-story"); let patched = patch_unityfs_string_field( &bundle, &StringFieldPatch { serialized_file_path: "CAB-array-story".to_string(), path_id: 1, field_path: "messages[1]".to_string(), expected_value: Some("world".to_string()), replacement: "老師".to_string(), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-array-story")) .unwrap(); let fields = serialized.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 patches_whole_string_array_with_length_change_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_string_array(), "CAB-array-story"); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-array-story".to_string(), path_id: 1, field_path: "messages".to_string(), expected_value: Some(UnitySerializedReplacementValue::Array(vec![ UnitySerializedReplacementValue::String("hello".to_string()), UnitySerializedReplacementValue::String("world".to_string()), ])), replacement: UnitySerializedReplacementValue::Array(vec![ UnitySerializedReplacementValue::String("こんにちは".to_string()), UnitySerializedReplacementValue::String("老師".to_string()), UnitySerializedReplacementValue::String("文本".to_string()), ]), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-array-story")) .unwrap(); let fields = serialized.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].value, UnitySerializedValue::String("こんにちは".to_string()) ); assert_eq!( items[1].value, UnitySerializedValue::String("老師".to_string()) ); assert_eq!( items[2].value, UnitySerializedValue::String("文本".to_string()) ); } #[test] fn patches_nested_vector_array_with_length_change_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path( &synthetic_serialized_vector_string_array(), "CAB-vector-story", ); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-vector-story".to_string(), path_id: 1, field_path: "messages".to_string(), expected_value: Some(UnitySerializedReplacementValue::Array(vec![ UnitySerializedReplacementValue::String("hello".to_string()), UnitySerializedReplacementValue::String("world".to_string()), ])), replacement: UnitySerializedReplacementValue::Array(vec![ UnitySerializedReplacementValue::String("こんにちは".to_string()), UnitySerializedReplacementValue::String("老師".to_string()), UnitySerializedReplacementValue::String("文本".to_string()), ]), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-vector-story")) .unwrap(); let fields = serialized.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[0].path, "messages[0]"); assert_eq!( items[0].value, UnitySerializedValue::String("こんにちは".to_string()) ); assert_eq!( items[1].value, UnitySerializedValue::String("老師".to_string()) ); assert_eq!( items[2].value, UnitySerializedValue::String("文本".to_string()) ); } #[test] fn patches_whole_string_map_with_length_change_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_string_map(), "CAB-map-story"); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-map-story".to_string(), path_id: 1, field_path: "texts".to_string(), expected_value: Some(UnitySerializedReplacementValue::Map(vec![ map_entry_replacement("jp", "hello"), map_entry_replacement("cn", "world"), ])), replacement: UnitySerializedReplacementValue::Map(vec![ map_entry_replacement("jp", "こんにちは"), map_entry_replacement("cn", "老師"), map_entry_replacement("tw", "文本"), ]), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-map-story")) .unwrap(); let fields = serialized.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 patches_key_value_string_map_schema_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path( &synthetic_serialized_key_value_string_map(), "CAB-key-value-map", ); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-key-value-map".to_string(), path_id: 1, field_path: "texts".to_string(), expected_value: Some(UnitySerializedReplacementValue::Map(vec![ map_entry_replacement_with_names("key", "value", "jp", "hello"), map_entry_replacement_with_names("key", "value", "cn", "world"), ])), replacement: 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 = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-key-value-map")) .unwrap(); let fields = serialized.fields_for_object(1).unwrap(); let UnitySerializedValue::Map(items) = &fields[0].value else { panic!("expected key/value string map"); }; let UnitySerializedValue::Object(entry_fields) = &items[0].value else { panic!("expected key/value map entry object"); }; assert_eq!(items.len(), 3); assert_eq!(entry_fields[0].path, "texts[0].key"); assert_eq!(entry_fields[1].path, "texts[0].value"); 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 patches_scriptableobject_key_value_string_map_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path( &synthetic_serialized_scriptableobject_key_value_string_map(), "CAB-scriptable-map", ); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-scriptable-map".to_string(), path_id: 1, field_path: "texts".to_string(), expected_value: Some(UnitySerializedReplacementValue::Map(vec![ map_entry_replacement_with_names("key", "value", "jp", "hello"), map_entry_replacement_with_names("key", "value", "cn", "world"), ])), replacement: 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 = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-scriptable-map")) .unwrap(); assert_eq!(serialized.types[0].class_id, 115); assert_eq!(serialized.objects[0].class_id, 115); let fields = serialized.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 patches_integer_array_element_and_rebuilds_unityfs() { let bundle = synthetic_bundle_with_path(&synthetic_serialized_int_array(), "CAB-score-story"); let patched = patch_unityfs_field( &bundle, &FieldPatch { serialized_file_path: "CAB-score-story".to_string(), path_id: 1, field_path: "scores[1]".to_string(), expected_value: Some(UnitySerializedReplacementValue::Signed(20)), replacement: UnitySerializedReplacementValue::Signed(42), }, ) .unwrap(); let reparsed = UnityFsParser::new().parse(&patched).unwrap(); let serialized = reparsed .serialized_files .iter() .find(|file| file.source_path.as_deref() == Some("CAB-score-story")) .unwrap(); let fields = serialized.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)); } 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) } }