308 lines
11 KiB
Rust
308 lines
11 KiB
Rust
#![allow(clippy::expect_used)]
|
|
#![doc = "Black-box CLI framing and exit-status tests."]
|
|
|
|
use std::{
|
|
fs,
|
|
os::unix::fs::PermissionsExt as _,
|
|
process::{Command, Stdio},
|
|
thread,
|
|
time::Duration,
|
|
};
|
|
|
|
use rustix::process::{Pid, Signal, kill_process};
|
|
#[test]
|
|
fn doctor_reports_ready_with_closed_runtime_paths() {
|
|
let temp = tempfile::tempdir().expect("temp");
|
|
let runtime = temp.path().join("runtime");
|
|
let java_home = runtime.join("jdk");
|
|
let adapter = runtime.join("adapter");
|
|
fs::create_dir_all(java_home.join("bin")).expect("java directory");
|
|
fs::create_dir_all(&adapter).expect("adapter directory");
|
|
for path in [runtime.join("bwrap"), adapter.join("GhidrAdapter.java")] {
|
|
fs::write(path, b"fixture").expect("runtime fixture");
|
|
}
|
|
let java = java_home.join("bin/java");
|
|
fs::write(&java, b"#!/bin/sh\necho 'openjdk version \"21\"' >&2\n").expect("java fixture");
|
|
fs::set_permissions(&java, fs::Permissions::from_mode(0o755)).expect("java mode");
|
|
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
|
|
.args([
|
|
"--store",
|
|
temp.path().join("store").to_str().expect("UTF-8"),
|
|
"--sandbox",
|
|
"off",
|
|
"doctor",
|
|
])
|
|
.env("GHIDR_GHIDRA_VERSION", "12.1.2")
|
|
.env(
|
|
"GHIDR_ANALYZE_HEADLESS",
|
|
env!("CARGO_BIN_EXE_ghidr-fake-child"),
|
|
)
|
|
.env("GHIDR_JAVA_HOME", java_home)
|
|
.env("GHIDR_ADAPTER_PATH", adapter)
|
|
.env("GHIDR_BUBBLEWRAP", runtime.join("bwrap"))
|
|
.output()
|
|
.expect("run ghidr");
|
|
assert_eq!(output.status.code(), Some(0), "{:?}", output.stderr);
|
|
assert!(output.stderr.is_empty());
|
|
let success: serde_json::Value =
|
|
serde_json::from_slice(&output.stdout).expect("valid JSON success");
|
|
assert_eq!(success["kind"], "doctor");
|
|
assert_eq!(success["data"]["ready"], true);
|
|
assert!(
|
|
!temp
|
|
.path()
|
|
.join("store/staging/doctor-write-probe")
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn clean_dry_run_and_execution_are_real_store_transactions() {
|
|
let temp = tempfile::tempdir().expect("temp");
|
|
let store = temp.path().join("store");
|
|
let sample = "a".repeat(64);
|
|
let profile = "b".repeat(64);
|
|
let artifact = store
|
|
.join("artifacts")
|
|
.join(&sample)
|
|
.join(&profile)
|
|
.join(format!("{}.json", "c".repeat(64)));
|
|
fs::create_dir_all(artifact.parent().expect("parent")).expect("artifact directory");
|
|
fs::write(&artifact, b"{}\n").expect("artifact");
|
|
|
|
let invoke = |dry_run: bool| {
|
|
let mut command = Command::new(env!("CARGO_BIN_EXE_ghidr"));
|
|
command.args([
|
|
"--store",
|
|
store.to_str().expect("UTF-8"),
|
|
"clean",
|
|
"--digest",
|
|
&sample,
|
|
]);
|
|
if dry_run {
|
|
command.arg("--dry-run");
|
|
}
|
|
command.output().expect("run clean")
|
|
};
|
|
let dry_run = invoke(true);
|
|
assert_eq!(dry_run.status.code(), Some(0), "{:?}", dry_run.stderr);
|
|
let report: serde_json::Value = serde_json::from_slice(&dry_run.stdout).expect("dry-run JSON");
|
|
assert_eq!(report["data"]["mode"], "dry_run");
|
|
assert_eq!(report["data"]["matched"]["artifacts"], 1);
|
|
assert!(artifact.exists());
|
|
|
|
let executed = invoke(false);
|
|
assert_eq!(executed.status.code(), Some(0), "{:?}", executed.stderr);
|
|
let report: serde_json::Value =
|
|
serde_json::from_slice(&executed.stdout).expect("execution JSON");
|
|
assert_eq!(report["data"]["mode"], "executed");
|
|
assert_eq!(report["data"]["removed"]["artifacts"], 1);
|
|
assert!(!artifact.exists());
|
|
|
|
let confirmation = Command::new(env!("CARGO_BIN_EXE_ghidr"))
|
|
.args(["--store", store.to_str().expect("UTF-8"), "clean", "--all"])
|
|
.output()
|
|
.expect("run clean all");
|
|
assert_eq!(confirmation.status.code(), Some(1));
|
|
assert!(confirmation.stdout.is_empty());
|
|
let error: serde_json::Value =
|
|
serde_json::from_slice(&confirmation.stderr).expect("confirmation JSON");
|
|
assert_eq!(error["error"]["code"], "confirmation_required");
|
|
assert!(error["error"]["details"]["matched"]["usage"].is_object());
|
|
}
|
|
|
|
#[test]
|
|
fn fake_worker_drives_inspect_reuse_functions_decompile_and_typed_error() {
|
|
let temp = tempfile::tempdir().expect("temp");
|
|
let store = temp.path().join("store");
|
|
let adapter = temp.path().join("adapter");
|
|
fs::create_dir(&adapter).expect("adapter");
|
|
fs::write(adapter.join("GhidrAdapter.java"), b"fixture").expect("adapter source");
|
|
let sample = temp.path().join("sample");
|
|
fs::write(&sample, b"controllable sample").expect("sample");
|
|
|
|
let invoke = |arguments: &[&str], error: Option<&str>| {
|
|
let mut command = Command::new(env!("CARGO_BIN_EXE_ghidr"));
|
|
command
|
|
.args([
|
|
"--sandbox",
|
|
"off",
|
|
"--store",
|
|
store.to_str().expect("UTF-8"),
|
|
])
|
|
.args(arguments)
|
|
.env("GHIDR_GHIDRA_VERSION", "12.1.2")
|
|
.env(
|
|
"GHIDR_ANALYZE_HEADLESS",
|
|
env!("CARGO_BIN_EXE_ghidr-fake-child"),
|
|
)
|
|
.env("GHIDR_ADAPTER_PATH", &adapter);
|
|
if let Some(code) = error {
|
|
command.env("GHIDR_FAKE_WORKER_ERROR", code);
|
|
}
|
|
command.output().expect("run Query")
|
|
};
|
|
|
|
let inspected = invoke(&["inspect", sample.to_str().expect("UTF-8")], None);
|
|
assert_eq!(inspected.status.code(), Some(0), "{:?}", inspected.stderr);
|
|
let inspection: serde_json::Value =
|
|
serde_json::from_slice(&inspected.stdout).expect("inspection");
|
|
assert_eq!(inspection["data"]["analysis"]["disposition"], "created");
|
|
assert_eq!(inspection["provenance"]["sandbox"]["backend"], "off");
|
|
let profile = inspection["provenance"]["analysis_profile_sha256"].clone();
|
|
|
|
let functions = invoke(
|
|
&["functions", sample.to_str().expect("UTF-8"), "--limit", "1"],
|
|
None,
|
|
);
|
|
assert_eq!(functions.status.code(), Some(0), "{:?}", functions.stderr);
|
|
let functions: serde_json::Value =
|
|
serde_json::from_slice(&functions.stdout).expect("functions");
|
|
assert_eq!(functions["data"]["items"][0]["name"], "main");
|
|
assert_eq!(functions["provenance"]["analysis_profile_sha256"], profile);
|
|
assert_eq!(
|
|
fs::read_dir(store.join("analyses"))
|
|
.expect("analyses")
|
|
.flat_map(|entry| fs::read_dir(entry.expect("sample entry").path()).expect("profiles"))
|
|
.count(),
|
|
1
|
|
);
|
|
|
|
let decompiled = invoke(
|
|
&[
|
|
"decompile",
|
|
sample.to_str().expect("UTF-8"),
|
|
"--name",
|
|
"main",
|
|
],
|
|
None,
|
|
);
|
|
assert_eq!(decompiled.status.code(), Some(0), "{:?}", decompiled.stderr);
|
|
let decompiled: serde_json::Value =
|
|
serde_json::from_slice(&decompiled.stdout).expect("decompilation");
|
|
assert_eq!(decompiled["data"]["requested_selector"]["value"], "main");
|
|
assert!(
|
|
decompiled["data"]["decompilation"]["text"]
|
|
.as_str()
|
|
.expect("text")
|
|
.contains("return 0")
|
|
);
|
|
|
|
let spilled = invoke(
|
|
&[
|
|
"decompile",
|
|
sample.to_str().expect("UTF-8"),
|
|
"--name",
|
|
"large",
|
|
"--max-inline-bytes",
|
|
"4096",
|
|
],
|
|
None,
|
|
);
|
|
assert_eq!(spilled.status.code(), Some(0), "{:?}", spilled.stderr);
|
|
let spilled: serde_json::Value =
|
|
serde_json::from_slice(&spilled.stdout).expect("spill descriptor");
|
|
assert_eq!(spilled["data"]["spilled"], true);
|
|
let artifact = spilled["data"]["artifact"]["path"]["value"]
|
|
.as_str()
|
|
.expect("Artifact path");
|
|
let complete = fs::read(artifact).expect("complete Artifact");
|
|
assert_eq!(complete.last(), Some(&b'\n'));
|
|
let complete: serde_json::Value =
|
|
serde_json::from_slice(&complete).expect("complete success response");
|
|
assert_eq!(
|
|
complete["data"]["decompilation"]["text"]
|
|
.as_str()
|
|
.expect("large text")
|
|
.len(),
|
|
100_000
|
|
);
|
|
|
|
let other = temp.path().join("other");
|
|
fs::copy("tests/fixtures/fake-worker-error.sample", &other).expect("other");
|
|
let failed = invoke(
|
|
&["inspect", other.to_str().expect("UTF-8")],
|
|
Some("analysis_failed"),
|
|
);
|
|
assert_eq!(failed.status.code(), Some(1));
|
|
assert!(failed.stdout.is_empty());
|
|
let error: serde_json::Value = serde_json::from_slice(&failed.stderr).expect("error JSON");
|
|
assert_eq!(error["error"]["code"], "analysis_failed");
|
|
let diagnostic = error["error"]["details"]["diagnostic_log"]["path"]["value"]
|
|
.as_str()
|
|
.expect("diagnostic path");
|
|
assert!(std::path::Path::new(diagnostic).is_file());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_invocation_is_one_json_document_on_stderr() {
|
|
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
|
|
.args(["functions", "sample", "--limit", "0"])
|
|
.output()
|
|
.expect("run ghidr");
|
|
assert_eq!(output.status.code(), Some(2));
|
|
assert!(output.stdout.is_empty());
|
|
let error: serde_json::Value =
|
|
serde_json::from_slice(&output.stderr).expect("valid JSON error");
|
|
assert_eq!(error["error"]["code"], "invalid_arguments");
|
|
assert_eq!(output.stderr.last(), Some(&b'\n'));
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_human_parse_failure_is_not_json() {
|
|
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
|
|
.args(["--format", "human", "decompile", "sample"])
|
|
.output()
|
|
.expect("run ghidr");
|
|
assert_eq!(output.status.code(), Some(2));
|
|
assert!(output.stdout.is_empty());
|
|
assert!(serde_json::from_slice::<serde_json::Value>(&output.stderr).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn sigint_interrupts_the_actual_cli_and_never_promotes_analysis() {
|
|
let temp = tempfile::tempdir().expect("temp");
|
|
let store = temp.path().join("store");
|
|
let adapter = temp.path().join("adapter");
|
|
fs::create_dir(&adapter).expect("adapter");
|
|
fs::write(adapter.join("GhidrAdapter.java"), b"fixture").expect("adapter source");
|
|
let sample = temp.path().join("sample");
|
|
fs::write(&sample, b"interruptible sample").expect("sample");
|
|
|
|
let child = Command::new(env!("CARGO_BIN_EXE_ghidr"))
|
|
.args([
|
|
"--sandbox",
|
|
"off",
|
|
"--store",
|
|
store.to_str().expect("UTF-8"),
|
|
"inspect",
|
|
sample.to_str().expect("UTF-8"),
|
|
])
|
|
.env("GHIDR_GHIDRA_VERSION", "12.1.2")
|
|
.env(
|
|
"GHIDR_ANALYZE_HEADLESS",
|
|
env!("CARGO_BIN_EXE_ghidr-fake-child"),
|
|
)
|
|
.env("GHIDR_ADAPTER_PATH", &adapter)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.expect("spawn ghidr");
|
|
thread::sleep(Duration::from_millis(200));
|
|
let raw_pid = i32::try_from(child.id()).expect("PID fits i32");
|
|
let pid = Pid::from_raw(raw_pid).expect("positive child PID");
|
|
kill_process(pid, Signal::INT).expect("send SIGINT");
|
|
let output = child.wait_with_output().expect("wait for interrupted CLI");
|
|
|
|
assert_eq!(output.status.code(), Some(130), "{:?}", output.stderr);
|
|
assert!(output.stdout.is_empty());
|
|
let error: serde_json::Value =
|
|
serde_json::from_slice(&output.stderr).expect("interrupted JSON");
|
|
assert_eq!(error["error"]["code"], "interrupted");
|
|
assert_eq!(
|
|
fs::read_dir(store.join("analyses"))
|
|
.expect("analyses")
|
|
.count(),
|
|
0
|
|
);
|
|
}
|