chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "unityasset"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
readme.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Inspect Unity assets, references, and extractable objects with AI-friendly output."
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common", default-features = false }
|
||||
lexopt.workspace = true
|
||||
regex-lite.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
unitysupport = { path = "../unitysupport" }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd.workspace = true
|
||||
predicates.workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
//! Public entry point for the `unityasset` command crate.
|
||||
#![allow(clippy::multiple_crate_versions)]
|
||||
|
||||
mod cli;
|
||||
|
||||
pub use cli::main_entry;
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Binary entry point for `unityasset`.
|
||||
#![allow(clippy::multiple_crate_versions)]
|
||||
|
||||
fn main() {
|
||||
std::process::exit(unityasset::main_entry());
|
||||
}
|
||||
@@ -0,0 +1,852 @@
|
||||
//! 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");
|
||||
}
|
||||
Reference in New Issue
Block a user