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