Files
BlueArchiveToolkit/crates/bat-assetbundle/src/patch.rs
T
nyaKazuha 2079c6a307 feat(sync): 接入解析缓存与汉化发布前置
补齐官方 release 解析缓存、TextUnit 明细索引、资源变更集、Crowdin handoff 预留、ResourceRepository 导入元数据和 localized release patch 前置链路。

同时开放文件级 patch.apply 与 UnityFS TextAsset/string/semantic field patch CLI/RPC 入口,并保留官方原版资源与汉化产物双目录发布状态。

验证:cargo test -p bat-assetbundle --locked;cargo clippy -p bat-assetbundle --all-targets --locked -- -D warnings;cargo test -p bat-infrastructure --locked。
2026-07-31 00:38:45 +08:00

1980 lines
74 KiB
Rust

//! 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<String>,
/// Replacement raw bytes.
pub replacement: Vec<u8>,
}
impl TextAssetPatch {
/// Creates a TextAsset replacement specification.
pub fn new(
serialized_file_path: impl Into<String>,
path_id: i64,
replacement: impl Into<Vec<u8>>,
) -> 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<String>,
/// Replacement string.
pub replacement: String,
}
impl StringFieldPatch {
/// Creates a string-field replacement specification.
pub fn new(
serialized_file_path: impl Into<String>,
path_id: i64,
field_path: impl Into<String>,
replacement: impl Into<String>,
) -> 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<UnitySerializedReplacementValue>,
/// 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<String>,
path_id: i64,
field_path: impl Into<String>,
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<u8>, value: &str) {
output.extend_from_slice(value.as_bytes());
output.push(0);
}
fn push_i32_be(output: &mut Vec<u8>, value: i32) {
output.extend_from_slice(&value.to_be_bytes());
}
fn push_u16_be(output: &mut Vec<u8>, value: u16) {
output.extend_from_slice(&value.to_be_bytes());
}
fn push_u32_be(output: &mut Vec<u8>, value: u32) {
output.extend_from_slice(&value.to_be_bytes());
}
fn push_u64_be(output: &mut Vec<u8>, value: u64) {
output.extend_from_slice(&value.to_be_bytes());
}
fn align_vec(output: &mut Vec<u8>, 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<u8> {
synthetic_bundle_with_path(payload, "CAB-test")
}
fn synthetic_bundle_with_path(payload: &[u8], path: &str) -> Vec<u8> {
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<u8>, value: i16) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_i32_le(data: &mut Vec<u8>, value: i32) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u32_le(data: &mut Vec<u8>, value: u32) {
data.extend_from_slice(&value.to_le_bytes());
}
fn push_u64_le(data: &mut Vec<u8>, value: u64) {
data.extend_from_slice(&value.to_le_bytes());
}
fn synthetic_serialized_text_asset(bytes: &[u8]) -> Vec<u8> {
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<u8> {
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<u8> {
synthetic_serialized_managed_reference_registry_with_payload_name(b"data")
}
fn synthetic_serialized_managed_reference_registry_with_managed_reference_data() -> Vec<u8> {
synthetic_serialized_managed_reference_registry_with_payload_name(b"managedReferenceData")
}
fn synthetic_serialized_managed_reference_registry_with_payload_name(
payload_field_name: &[u8],
) -> Vec<u8> {
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<u8> {
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<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, 2);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let enum_type = strings.len();
strings.extend_from_slice(b"ScenarioDifficulty\0");
let enum_name = strings.len();
strings.extend_from_slice(b"difficulty\0");
let value_type = strings.len();
strings.extend_from_slice(b"int\0");
let value_name = strings.len();
strings.extend_from_slice(b"value__\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 3);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset, byte_size) in [
(0u8, root_type as i32, root_name as i32, -1),
(1u8, enum_type as i32, enum_name as i32, -1),
(2u8, value_type as i32, value_name as i32, 4),
] {
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<u8> {
let mut object_data = Vec::new();
push_u32_le(&mut object_data, 5);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let bitfield_type = strings.len();
strings.extend_from_slice(b"LayerMask\0");
let bitfield_name = strings.len();
strings.extend_from_slice(b"target_layers\0");
let bits_type = strings.len();
strings.extend_from_slice(b"UInt32\0");
let bits_name = strings.len();
strings.extend_from_slice(b"m_Bits\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 3);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset, byte_size) in [
(0u8, root_type as i32, root_name as i32, -1),
(1u8, bitfield_type as i32, bitfield_name as i32, -1),
(2u8, bits_type as i32, bits_name as i32, 4),
] {
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<u8> {
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<u8> {
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<u8> {
let mut object_data = Vec::new();
push_i32_le(&mut object_data, 2);
push_i32_le(&mut object_data, 10);
push_i32_le(&mut object_data, 20);
let mut strings = Vec::new();
let root_type = strings.len();
strings.extend_from_slice(b"MonoBehaviour\0");
let root_name = strings.len();
strings.push(0);
let array_type = strings.len();
strings.extend_from_slice(b"Array\0");
let array_name = strings.len();
strings.extend_from_slice(b"scores\0");
let size_type = strings.len();
strings.extend_from_slice(b"int\0");
let size_name = strings.len();
strings.extend_from_slice(b"size\0");
let data_type = strings.len();
strings.extend_from_slice(b"int\0");
let data_name = strings.len();
strings.extend_from_slice(b"data\0");
let mut metadata = Vec::new();
metadata.extend_from_slice(b"2021.3.56f2\0");
push_i32_le(&mut metadata, 19);
metadata.push(1);
push_i32_le(&mut metadata, 1);
push_i32_le(&mut metadata, 114);
metadata.push(0);
push_i16_le(&mut metadata, 0);
metadata.extend_from_slice(&[0; 16]);
metadata.extend_from_slice(&[0; 16]);
push_i32_le(&mut metadata, 4);
push_i32_le(&mut metadata, strings.len() as i32);
for (level, type_offset, name_offset) in [
(0u8, root_type as i32, root_name as i32),
(1u8, array_type as i32, array_name as i32),
(2u8, size_type as i32, size_name as i32),
(2u8, data_type as i32, data_name as i32),
] {
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<u8> {
synthetic_serialized_string_map_with_entry_names(b"first", b"second")
}
fn synthetic_serialized_key_value_string_map() -> Vec<u8> {
synthetic_serialized_string_map_with_entry_names(b"key", b"value")
}
fn synthetic_serialized_scriptableobject_key_value_string_map() -> Vec<u8> {
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<u8> {
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<u8> {
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)
}
}