forked from Crockan/MercuryToolbox
853 lines
26 KiB
Rust
853 lines
26 KiB
Rust
//! Integration tests for the `unityasset` command.
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::*;
|
|
use serde::de::DeserializeOwned;
|
|
use serde_json::Value;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
fn cargo_command() -> Command {
|
|
Command::cargo_bin("unityasset").expect("binary")
|
|
}
|
|
|
|
const ALIGN_BYTES_FLAG: i32 = 0x4000;
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct PPtrValue {
|
|
file_id: i32,
|
|
path_id: i64,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum FixtureObject {
|
|
GameObject {
|
|
name: String,
|
|
},
|
|
TextAsset {
|
|
name: String,
|
|
script: String,
|
|
},
|
|
MonoScript {
|
|
name: String,
|
|
class_name: String,
|
|
namespace: String,
|
|
assembly_name: String,
|
|
},
|
|
MonoBehaviour {
|
|
name: String,
|
|
script: PPtrValue,
|
|
text_asset: PPtrValue,
|
|
},
|
|
StrippedMonoBehaviour {
|
|
game_object: PPtrValue,
|
|
script: PPtrValue,
|
|
extra_refs: Vec<PPtrValue>,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct FixtureEntry {
|
|
path_id: i64,
|
|
class_id: i32,
|
|
object: FixtureObject,
|
|
}
|
|
|
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
let suffix = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("clock")
|
|
.as_nanos();
|
|
let path = std::env::temp_dir().join(format!("unityasset-{label}-{suffix}"));
|
|
fs::create_dir_all(&path).expect("temp dir");
|
|
path
|
|
}
|
|
|
|
fn to_i32(value: usize) -> i32 {
|
|
i32::try_from(value).expect("fixture value fits in i32")
|
|
}
|
|
|
|
fn to_u32(value: usize) -> u32 {
|
|
u32::try_from(value).expect("fixture value fits in u32")
|
|
}
|
|
|
|
fn to_i64(value: usize) -> i64 {
|
|
i64::try_from(value).expect("fixture value fits in i64")
|
|
}
|
|
|
|
fn standalone_entries() -> Vec<FixtureEntry> {
|
|
vec![
|
|
FixtureEntry {
|
|
path_id: 1001,
|
|
class_id: 49,
|
|
object: FixtureObject::TextAsset {
|
|
name: "BinaryText".to_string(),
|
|
script: "Hello from binary text asset\n".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 1002,
|
|
class_id: 115,
|
|
object: FixtureObject::MonoScript {
|
|
name: "SampleBehaviour".to_string(),
|
|
class_name: "SampleBehaviour".to_string(),
|
|
namespace: "Mercury.Sample".to_string(),
|
|
assembly_name: "Assembly-CSharp".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 1003,
|
|
class_id: 114,
|
|
object: FixtureObject::MonoBehaviour {
|
|
name: "BinaryBehaviour".to_string(),
|
|
script: PPtrValue {
|
|
file_id: 0,
|
|
path_id: 1002,
|
|
},
|
|
text_asset: PPtrValue {
|
|
file_id: 0,
|
|
path_id: 1001,
|
|
},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
fn bundle_entries() -> Vec<FixtureEntry> {
|
|
vec![
|
|
FixtureEntry {
|
|
path_id: 2001,
|
|
class_id: 49,
|
|
object: FixtureObject::TextAsset {
|
|
name: "BundledText".to_string(),
|
|
script: "Hello from bundled text asset\n".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 2002,
|
|
class_id: 115,
|
|
object: FixtureObject::MonoScript {
|
|
name: "BundledBehaviour".to_string(),
|
|
class_name: "BundledBehaviour".to_string(),
|
|
namespace: "Mercury.Bundle".to_string(),
|
|
assembly_name: "Assembly-CSharp".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 2003,
|
|
class_id: 114,
|
|
object: FixtureObject::MonoBehaviour {
|
|
name: "BundledMonoBehaviour".to_string(),
|
|
script: PPtrValue {
|
|
file_id: 0,
|
|
path_id: 2002,
|
|
},
|
|
text_asset: PPtrValue {
|
|
file_id: 0,
|
|
path_id: 2001,
|
|
},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
fn stripped_entries() -> Vec<FixtureEntry> {
|
|
vec![
|
|
FixtureEntry {
|
|
path_id: 29,
|
|
class_id: 1,
|
|
object: FixtureObject::GameObject {
|
|
name: "StrippedWindow".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 30,
|
|
class_id: 1,
|
|
object: FixtureObject::GameObject {
|
|
name: "LinkedChild".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 1182,
|
|
class_id: 115,
|
|
object: FixtureObject::MonoScript {
|
|
name: "StrippedWindowBehaviour".to_string(),
|
|
class_name: "StrippedWindowBehaviour".to_string(),
|
|
namespace: "Mercury.Stripped".to_string(),
|
|
assembly_name: "Assembly-CSharp".to_string(),
|
|
},
|
|
},
|
|
FixtureEntry {
|
|
path_id: 2000,
|
|
class_id: 114,
|
|
object: FixtureObject::StrippedMonoBehaviour {
|
|
game_object: PPtrValue {
|
|
file_id: 0,
|
|
path_id: 29,
|
|
},
|
|
script: PPtrValue {
|
|
file_id: 0,
|
|
path_id: 1182,
|
|
},
|
|
extra_refs: vec![PPtrValue {
|
|
file_id: 0,
|
|
path_id: 30,
|
|
}],
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
fn write_fixture_workspace(root: &Path) {
|
|
fs::write(
|
|
root.join("sample.assets"),
|
|
build_serialized_file(&standalone_entries()),
|
|
)
|
|
.expect("standalone asset");
|
|
fs::write(
|
|
root.join("sample.bundle"),
|
|
build_unityfs_bundle("bundled.assets", &build_serialized_file(&bundle_entries())),
|
|
)
|
|
.expect("bundle");
|
|
fs::write(
|
|
root.join("stripped.assets"),
|
|
build_serialized_file(&stripped_entries()),
|
|
)
|
|
.expect("stripped asset");
|
|
fs::write(root.join("scene.unity"), sample_yaml_fixture()).expect("yaml fixture");
|
|
}
|
|
|
|
fn sample_yaml_fixture() -> String {
|
|
r"--- !u!1 &1000
|
|
GameObject:
|
|
m_Name: SampleYamlGameObject
|
|
m_Component:
|
|
- component: {fileID: 2000}
|
|
--- !u!114 &2000
|
|
MonoBehaviour:
|
|
m_Name: SampleYamlBehaviour
|
|
m_GameObject: {fileID: 1000}
|
|
m_Script: {fileID: 11500000, guid: 1234567890abcdef1234567890abcdef, type: 3}
|
|
m_TextAsset: {fileID: 3000}
|
|
--- !u!49 &3000
|
|
TextAsset:
|
|
m_Name: YamlText
|
|
m_Script: Hello from YAML text asset
|
|
"
|
|
.to_string()
|
|
}
|
|
|
|
fn build_serialized_file(entries: &[FixtureEntry]) -> Vec<u8> {
|
|
let type_descriptors = entries
|
|
.iter()
|
|
.map(|entry| (entry.class_id, type_tree_blob_for(&entry.object)))
|
|
.collect::<Vec<_>>();
|
|
let object_payloads = entries
|
|
.iter()
|
|
.map(|entry| object_bytes(&entry.object))
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut metadata = Vec::new();
|
|
metadata.extend_from_slice(b"2019.4.0f1\0");
|
|
metadata.extend_from_slice(&0i32.to_le_bytes());
|
|
metadata.push(1u8);
|
|
metadata.extend_from_slice(&to_i32(type_descriptors.len()).to_le_bytes());
|
|
|
|
for (class_id, tree_blob) in &type_descriptors {
|
|
metadata.extend_from_slice(&class_id.to_le_bytes());
|
|
metadata.push(0u8);
|
|
metadata.extend_from_slice(&(-1i16).to_le_bytes());
|
|
if *class_id == 114 {
|
|
metadata.extend_from_slice(&[0xAB; 16]);
|
|
}
|
|
metadata.extend_from_slice(&[0xCD; 16]);
|
|
metadata.extend_from_slice(tree_blob);
|
|
}
|
|
|
|
metadata.extend_from_slice(&to_i32(entries.len()).to_le_bytes());
|
|
let mut relative_offset = 0u32;
|
|
for (index, entry) in entries.iter().enumerate() {
|
|
while metadata.len() % 4 != 0 {
|
|
metadata.push(0u8);
|
|
}
|
|
metadata.extend_from_slice(&entry.path_id.to_le_bytes());
|
|
metadata.extend_from_slice(&relative_offset.to_le_bytes());
|
|
metadata.extend_from_slice(&to_u32(object_payloads[index].len()).to_le_bytes());
|
|
metadata.extend_from_slice(&to_i32(index).to_le_bytes());
|
|
relative_offset = relative_offset
|
|
.checked_add(to_u32(object_payloads[index].len()))
|
|
.expect("relative offset overflow");
|
|
}
|
|
|
|
metadata.extend_from_slice(&0i32.to_le_bytes());
|
|
metadata.extend_from_slice(&0i32.to_le_bytes());
|
|
metadata.push(0u8);
|
|
|
|
let header_size = 20u32;
|
|
let data_offset = header_size
|
|
.checked_add(to_u32(metadata.len()))
|
|
.expect("data offset overflow");
|
|
let file_size = data_offset
|
|
.checked_add(
|
|
object_payloads
|
|
.iter()
|
|
.map(|bytes| to_u32(bytes.len()))
|
|
.sum::<u32>(),
|
|
)
|
|
.expect("file size overflow");
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&data_offset.to_be_bytes());
|
|
bytes.extend_from_slice(&file_size.to_be_bytes());
|
|
bytes.extend_from_slice(&19u32.to_be_bytes());
|
|
bytes.extend_from_slice(&data_offset.to_be_bytes());
|
|
bytes.push(0u8);
|
|
bytes.extend_from_slice(&[0u8; 3]);
|
|
bytes.extend_from_slice(&metadata);
|
|
for payload in object_payloads {
|
|
bytes.extend_from_slice(&payload);
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn build_unityfs_bundle(entry_name: &str, file_bytes: &[u8]) -> Vec<u8> {
|
|
let mut blocks_info = vec![0u8; 16];
|
|
blocks_info.extend_from_slice(&1i32.to_be_bytes());
|
|
blocks_info.extend_from_slice(&to_u32(file_bytes.len()).to_be_bytes());
|
|
blocks_info.extend_from_slice(&to_u32(file_bytes.len()).to_be_bytes());
|
|
blocks_info.extend_from_slice(&0u16.to_be_bytes());
|
|
blocks_info.extend_from_slice(&1i32.to_be_bytes());
|
|
blocks_info.extend_from_slice(&0i64.to_be_bytes());
|
|
blocks_info.extend_from_slice(&to_i64(file_bytes.len()).to_be_bytes());
|
|
blocks_info.extend_from_slice(&0x4u32.to_be_bytes());
|
|
blocks_info.extend_from_slice(entry_name.as_bytes());
|
|
blocks_info.push(0u8);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(b"UnityFS\0");
|
|
bytes.extend_from_slice(&7u32.to_be_bytes());
|
|
bytes.extend_from_slice(b"2019.4.0f1\0");
|
|
bytes.extend_from_slice(b"2019.4.0f1\0");
|
|
let size_offset = bytes.len();
|
|
bytes.extend_from_slice(&0i64.to_be_bytes());
|
|
bytes.extend_from_slice(&to_u32(blocks_info.len()).to_be_bytes());
|
|
bytes.extend_from_slice(&to_u32(blocks_info.len()).to_be_bytes());
|
|
bytes.extend_from_slice(&0u32.to_be_bytes());
|
|
while bytes.len() % 16 != 0 {
|
|
bytes.push(0u8);
|
|
}
|
|
bytes.extend_from_slice(&blocks_info);
|
|
bytes.extend_from_slice(file_bytes);
|
|
let total_size = to_i64(bytes.len());
|
|
bytes[size_offset..size_offset + 8].copy_from_slice(&total_size.to_be_bytes());
|
|
bytes
|
|
}
|
|
|
|
fn type_tree_blob_for(object: &FixtureObject) -> Vec<u8> {
|
|
let nodes = match object {
|
|
FixtureObject::GameObject { .. } => vec![
|
|
node(0, "GameObject", "Base", -1, 0),
|
|
node(1, "string", "m_Name", -1, ALIGN_BYTES_FLAG),
|
|
],
|
|
FixtureObject::TextAsset { .. } => vec![
|
|
node(0, "TextAsset", "Base", -1, 0),
|
|
node(1, "string", "m_Name", -1, ALIGN_BYTES_FLAG),
|
|
node(1, "string", "m_Script", -1, ALIGN_BYTES_FLAG),
|
|
],
|
|
FixtureObject::MonoScript { .. } => vec![
|
|
node(0, "MonoScript", "Base", -1, 0),
|
|
node(1, "string", "m_Name", -1, ALIGN_BYTES_FLAG),
|
|
node(1, "string", "m_ClassName", -1, ALIGN_BYTES_FLAG),
|
|
node(1, "string", "m_Namespace", -1, ALIGN_BYTES_FLAG),
|
|
node(1, "string", "m_AssemblyName", -1, ALIGN_BYTES_FLAG),
|
|
],
|
|
FixtureObject::MonoBehaviour { .. } => vec![
|
|
node(0, "MonoBehaviour", "Base", -1, 0),
|
|
node(1, "string", "m_Name", -1, ALIGN_BYTES_FLAG),
|
|
node(1, "PPtr<MonoScript>", "m_Script", -1, 0),
|
|
node(2, "int", "m_FileID", 4, 0),
|
|
node(2, "long long", "m_PathID", 8, 0),
|
|
node(1, "PPtr<TextAsset>", "m_TextAsset", -1, 0),
|
|
node(2, "int", "m_FileID", 4, 0),
|
|
node(2, "long long", "m_PathID", 8, 0),
|
|
],
|
|
FixtureObject::StrippedMonoBehaviour { .. } => return empty_type_tree_blob(),
|
|
};
|
|
|
|
let mut string_buffer = Vec::new();
|
|
let mut records = Vec::new();
|
|
for (index, (level, type_name, field_name, byte_size, meta_flags)) in nodes.iter().enumerate() {
|
|
let type_offset = push_cstring(&mut string_buffer, type_name);
|
|
let name_offset = push_cstring(&mut string_buffer, field_name);
|
|
records.push((
|
|
*level,
|
|
type_offset,
|
|
name_offset,
|
|
*byte_size,
|
|
to_i32(index),
|
|
*meta_flags,
|
|
));
|
|
}
|
|
|
|
let mut blob = Vec::new();
|
|
blob.extend_from_slice(&to_i32(records.len()).to_le_bytes());
|
|
blob.extend_from_slice(&to_i32(string_buffer.len()).to_le_bytes());
|
|
for (level, type_offset, name_offset, byte_size, index, meta_flags) in records {
|
|
blob.extend_from_slice(&1u16.to_le_bytes());
|
|
blob.push(level);
|
|
blob.push(0u8);
|
|
blob.extend_from_slice(&type_offset.to_le_bytes());
|
|
blob.extend_from_slice(&name_offset.to_le_bytes());
|
|
blob.extend_from_slice(&byte_size.to_le_bytes());
|
|
blob.extend_from_slice(&index.to_le_bytes());
|
|
blob.extend_from_slice(&meta_flags.to_le_bytes());
|
|
blob.extend_from_slice(&0u64.to_le_bytes());
|
|
}
|
|
blob.extend_from_slice(&string_buffer);
|
|
blob
|
|
}
|
|
|
|
fn empty_type_tree_blob() -> Vec<u8> {
|
|
let mut blob = Vec::new();
|
|
blob.extend_from_slice(&0i32.to_le_bytes());
|
|
blob.extend_from_slice(&0i32.to_le_bytes());
|
|
blob
|
|
}
|
|
|
|
const fn node(
|
|
level: u8,
|
|
type_name: &'static str,
|
|
field_name: &'static str,
|
|
byte_size: i32,
|
|
meta_flags: i32,
|
|
) -> (u8, &'static str, &'static str, i32, i32) {
|
|
(level, type_name, field_name, byte_size, meta_flags)
|
|
}
|
|
|
|
fn push_cstring(buffer: &mut Vec<u8>, value: &str) -> u32 {
|
|
let offset = to_u32(buffer.len());
|
|
buffer.extend_from_slice(value.as_bytes());
|
|
buffer.push(0u8);
|
|
offset
|
|
}
|
|
|
|
fn object_bytes(object: &FixtureObject) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
match object {
|
|
FixtureObject::GameObject { name } => {
|
|
push_aligned_string(&mut bytes, name);
|
|
}
|
|
FixtureObject::TextAsset { name, script } => {
|
|
push_aligned_string(&mut bytes, name);
|
|
push_aligned_string(&mut bytes, script);
|
|
}
|
|
FixtureObject::MonoScript {
|
|
name,
|
|
class_name,
|
|
namespace,
|
|
assembly_name,
|
|
} => {
|
|
push_aligned_string(&mut bytes, name);
|
|
push_aligned_string(&mut bytes, class_name);
|
|
push_aligned_string(&mut bytes, namespace);
|
|
push_aligned_string(&mut bytes, assembly_name);
|
|
}
|
|
FixtureObject::MonoBehaviour {
|
|
name,
|
|
script,
|
|
text_asset,
|
|
} => {
|
|
push_aligned_string(&mut bytes, name);
|
|
push_pptr(&mut bytes, *script);
|
|
push_pptr(&mut bytes, *text_asset);
|
|
}
|
|
FixtureObject::StrippedMonoBehaviour {
|
|
game_object,
|
|
script,
|
|
extra_refs,
|
|
} => {
|
|
push_pptr(&mut bytes, *game_object);
|
|
bytes.extend_from_slice(&1i32.to_le_bytes());
|
|
push_pptr(&mut bytes, *script);
|
|
for value in extra_refs {
|
|
push_pptr(&mut bytes, *value);
|
|
}
|
|
}
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn push_aligned_string(bytes: &mut Vec<u8>, value: &str) {
|
|
bytes.extend_from_slice(&to_i32(value.len()).to_le_bytes());
|
|
bytes.extend_from_slice(value.as_bytes());
|
|
while bytes.len() % 4 != 0 {
|
|
bytes.push(0u8);
|
|
}
|
|
}
|
|
|
|
fn push_pptr(bytes: &mut Vec<u8>, value: PPtrValue) {
|
|
bytes.extend_from_slice(&value.file_id.to_le_bytes());
|
|
bytes.extend_from_slice(&value.path_id.to_le_bytes());
|
|
}
|
|
|
|
fn assert_json<T: DeserializeOwned>(assert: &assert_cmd::assert::Assert, label: &str) -> T {
|
|
serde_json::from_slice(&assert.get_output().stdout).expect(label)
|
|
}
|
|
|
|
#[test]
|
|
fn help_includes_subcommands_and_examples() {
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg("--help")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("unityasset index"))
|
|
.stdout(predicate::str::contains("unityasset refs"))
|
|
.stdout(predicate::str::contains("--direction <MODE>"))
|
|
.stdout(predicate::str::contains("--max-refs <COUNT>"))
|
|
.stdout(predicate::str::contains("unityasset dump"))
|
|
.stdout(predicate::str::contains("unityasset extract"));
|
|
}
|
|
|
|
#[test]
|
|
fn indexes_serialized_bundle_and_yaml_objects_as_json() {
|
|
let root = unique_temp_dir("index");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut command = cargo_command();
|
|
let assert = command
|
|
.arg("--json")
|
|
.arg("index")
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let rows = assert_json::<Vec<Value>>(&assert, "json");
|
|
|
|
assert!(rows.iter().any(|row| {
|
|
row.get("id").and_then(Value::as_str) == Some("sample.assets#1001")
|
|
&& row.get("class_name").and_then(Value::as_str) == Some("TextAsset")
|
|
}));
|
|
assert!(rows.iter().any(|row| {
|
|
row.get("id").and_then(Value::as_str) == Some("sample.assets#1002")
|
|
&& row.get("script_class").and_then(Value::as_str) == Some("SampleBehaviour")
|
|
&& row.get("script_namespace").and_then(Value::as_str) == Some("Mercury.Sample")
|
|
&& row.get("script_assembly").and_then(Value::as_str) == Some("Assembly-CSharp")
|
|
}));
|
|
assert!(rows.iter().any(|row| {
|
|
row.get("id").and_then(Value::as_str) == Some("sample.bundle::bundled.assets#2001")
|
|
&& row.get("source_kind").and_then(Value::as_str) == Some("asset_bundle")
|
|
}));
|
|
assert!(rows.iter().any(|row| {
|
|
row.get("id").and_then(Value::as_str) == Some("scene.unity#3000")
|
|
&& row.get("format").and_then(Value::as_str) == Some("yaml")
|
|
}));
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn index_filters_by_script_metadata_for_mono_behaviour_instances() {
|
|
let root = unique_temp_dir("index-script-filter");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut command = cargo_command();
|
|
let assert = command
|
|
.args([
|
|
"--json",
|
|
"index",
|
|
"--class",
|
|
"MonoBehaviour",
|
|
"--script",
|
|
"SampleBehaviour",
|
|
&root.display().to_string(),
|
|
])
|
|
.assert()
|
|
.success();
|
|
let rows = assert_json::<Vec<Value>>(&assert, "json");
|
|
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(
|
|
rows[0].get("id").and_then(Value::as_str),
|
|
Some("sample.assets#1003")
|
|
);
|
|
assert_eq!(
|
|
rows[0].get("script_class").and_then(Value::as_str),
|
|
Some("SampleBehaviour")
|
|
);
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn index_empty_matches_still_exit_success_with_json_output() {
|
|
let root = unique_temp_dir("index-empty-success");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut command = cargo_command();
|
|
let assert = command
|
|
.args([
|
|
"--json",
|
|
"index",
|
|
"--class",
|
|
"^VideoClip$",
|
|
&root.display().to_string(),
|
|
])
|
|
.assert()
|
|
.success();
|
|
let rows = assert_json::<Vec<Value>>(&assert, "json");
|
|
assert!(rows.is_empty());
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn reports_binary_incoming_and_outgoing_references_as_json() {
|
|
let root = unique_temp_dir("refs-bin");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut command = cargo_command();
|
|
let assert = command
|
|
.args(["--json", "refs", "sample.assets#1001"])
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let refs = assert_json::<Value>(&assert, "json");
|
|
|
|
assert_eq!(
|
|
refs.get("target")
|
|
.and_then(|target| target.get("id"))
|
|
.and_then(Value::as_str),
|
|
Some("sample.assets#1001")
|
|
);
|
|
assert_eq!(
|
|
refs.get("incoming")
|
|
.and_then(Value::as_array)
|
|
.expect("incoming")
|
|
.iter()
|
|
.filter_map(|entry| entry.get("id").and_then(Value::as_str))
|
|
.collect::<Vec<_>>(),
|
|
vec!["sample.assets#1003"]
|
|
);
|
|
assert!(
|
|
refs.get("outgoing")
|
|
.and_then(Value::as_array)
|
|
.expect("outgoing")
|
|
.is_empty()
|
|
);
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn reports_yaml_references_as_json() {
|
|
let root = unique_temp_dir("refs-yaml");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut command = cargo_command();
|
|
let assert = command
|
|
.args(["--json", "refs", "scene.unity#1000"])
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let refs = assert_json::<Value>(&assert, "json");
|
|
|
|
assert_eq!(
|
|
refs.get("outgoing")
|
|
.and_then(Value::as_array)
|
|
.expect("outgoing")
|
|
.iter()
|
|
.filter_map(|entry| entry.get("id").and_then(Value::as_str))
|
|
.collect::<Vec<_>>(),
|
|
vec!["scene.unity#2000"]
|
|
);
|
|
assert_eq!(
|
|
refs.get("incoming")
|
|
.and_then(Value::as_array)
|
|
.expect("incoming")
|
|
.iter()
|
|
.filter_map(|entry| entry.get("id").and_then(Value::as_str))
|
|
.collect::<Vec<_>>(),
|
|
vec!["scene.unity#2000"]
|
|
);
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn reports_stripped_mono_behaviour_refs_and_metadata_only_dump() {
|
|
let root = unique_temp_dir("refs-stripped");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut refs_command = cargo_command();
|
|
let refs_assert = refs_command
|
|
.args(["--json", "refs", "stripped.assets#2000"])
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let refs = assert_json::<Value>(&refs_assert, "json");
|
|
|
|
let outgoing = refs
|
|
.get("outgoing")
|
|
.and_then(Value::as_array)
|
|
.expect("outgoing");
|
|
assert!(outgoing.iter().any(|entry| {
|
|
entry.get("path").and_then(Value::as_str) == Some("m_GameObject")
|
|
&& entry.get("id").and_then(Value::as_str) == Some("stripped.assets#29")
|
|
}));
|
|
assert!(outgoing.iter().any(|entry| {
|
|
entry.get("path").and_then(Value::as_str) == Some("m_Script")
|
|
&& entry.get("id").and_then(Value::as_str) == Some("stripped.assets#1182")
|
|
}));
|
|
assert!(outgoing.iter().any(|entry| {
|
|
entry
|
|
.get("path")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|path| path.starts_with("_raw_pptr@"))
|
|
&& entry.get("id").and_then(Value::as_str) == Some("stripped.assets#30")
|
|
}));
|
|
|
|
let mut dump_command = cargo_command();
|
|
let dump_assert = dump_command
|
|
.args(["--json", "dump", "stripped.assets#2000"])
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let dump = assert_json::<Value>(&dump_assert, "json");
|
|
|
|
assert_eq!(
|
|
dump.get("field_mode").and_then(Value::as_str),
|
|
Some("metadata_only")
|
|
);
|
|
assert_eq!(
|
|
dump.get("properties")
|
|
.and_then(|value| value.get("m_GameObject"))
|
|
.and_then(|value| value.get("resolved_id"))
|
|
.and_then(Value::as_str),
|
|
Some("stripped.assets#29")
|
|
);
|
|
assert_eq!(
|
|
dump.get("properties")
|
|
.and_then(|value| value.get("m_Script"))
|
|
.and_then(|value| value.get("resolved_id"))
|
|
.and_then(Value::as_str),
|
|
Some("stripped.assets#1182")
|
|
);
|
|
assert!(
|
|
dump.get("properties")
|
|
.and_then(|value| value.get("_raw_resolved_references"))
|
|
.and_then(Value::as_array)
|
|
.is_some_and(|items| !items.is_empty())
|
|
);
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn refs_json_supports_direction_and_max_refs() {
|
|
let root = unique_temp_dir("refs-direction");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut command = cargo_command();
|
|
let assert = command
|
|
.args([
|
|
"--json",
|
|
"refs",
|
|
"--direction",
|
|
"outgoing",
|
|
"--max-refs",
|
|
"2",
|
|
"stripped.assets#2000",
|
|
])
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let refs = assert_json::<Value>(&assert, "json");
|
|
|
|
assert_eq!(
|
|
refs.get("direction").and_then(Value::as_str),
|
|
Some("outgoing")
|
|
);
|
|
assert_eq!(refs.get("max_refs").and_then(Value::as_u64), Some(2));
|
|
assert_eq!(refs.get("outgoing_total").and_then(Value::as_u64), Some(3));
|
|
assert_eq!(refs.get("outgoing_shown").and_then(Value::as_u64), Some(2));
|
|
assert_eq!(
|
|
refs.get("outgoing_truncated").and_then(Value::as_bool),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
refs.get("incoming_filter_reason").and_then(Value::as_str),
|
|
Some("excluded_by_direction")
|
|
);
|
|
assert!(
|
|
refs.get("incoming")
|
|
.and_then(Value::as_array)
|
|
.expect("incoming")
|
|
.is_empty()
|
|
);
|
|
assert_eq!(
|
|
refs.get("outgoing")
|
|
.and_then(Value::as_array)
|
|
.expect("outgoing")
|
|
.len(),
|
|
2
|
|
);
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|
|
|
|
#[test]
|
|
fn dumps_bundle_objects_and_extracts_text_payloads() {
|
|
let root = unique_temp_dir("dump-extract");
|
|
write_fixture_workspace(&root);
|
|
|
|
let mut dump_command = cargo_command();
|
|
let dump_assert = dump_command
|
|
.args(["--json", "dump", "sample.bundle::bundled.assets#2002"])
|
|
.arg(&root)
|
|
.assert()
|
|
.success();
|
|
let dump = assert_json::<Value>(&dump_assert, "dump json");
|
|
assert_eq!(
|
|
dump.get("properties")
|
|
.and_then(|properties| properties.get("m_ClassName"))
|
|
.and_then(Value::as_str),
|
|
Some("BundledBehaviour")
|
|
);
|
|
|
|
let out_dir = root.join("out");
|
|
fs::create_dir_all(&out_dir).expect("out dir");
|
|
let mut extract_command = cargo_command();
|
|
extract_command
|
|
.args([
|
|
"extract",
|
|
"sample.bundle::bundled.assets#2001",
|
|
&root.display().to_string(),
|
|
"--output-dir",
|
|
&out_dir.display().to_string(),
|
|
])
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("BundledText"));
|
|
|
|
let bundled_text = fs::read_to_string(out_dir.join("BundledText.txt")).expect("bundled text");
|
|
assert_eq!(bundled_text, "Hello from bundled text asset\n");
|
|
|
|
let mut yaml_extract = cargo_command();
|
|
yaml_extract
|
|
.args([
|
|
"extract",
|
|
"scene.unity#3000",
|
|
&root.display().to_string(),
|
|
"--output-dir",
|
|
&out_dir.display().to_string(),
|
|
])
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("YamlText"));
|
|
let yaml_text = fs::read_to_string(out_dir.join("YamlText.txt")).expect("yaml text");
|
|
assert_eq!(yaml_text, "Hello from YAML text asset");
|
|
|
|
fs::remove_dir_all(root).expect("cleanup");
|
|
}
|