#![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::(&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 ); }