#![allow(dead_code, unused_imports)]
pub mod helpers;
pub mod schemas;
pub use helpers::{
get_function_address, get_function_addresses, ghidra, normalize_json, normalize_output,
GhidraCommand, GhidraResult,
};
pub use schemas::Validate;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::sync::Once;
use std::time::Duration;
pub fn fixture_binary() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("sample_binary")
}
pub fn ensure_test_project(project: &str, program: &str) {
static SETUP: Once = Once::new();
SETUP.call_once(|| {
let binary = fixture_binary();
if !binary.exists() {
panic!(
"Test fixture not found: {:?}\nRun: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs",
binary
);
}
let projects_dir = dirs::cache_dir()
.expect("Could not determine cache directory")
.join("ghidra-cli")
.join("projects");
let gpr_file = projects_dir.join(format!("{}.gpr", project));
let rep_dir = projects_dir.join(format!("{}.rep", project));
let idata_dir = rep_dir.join("idata");
let idata_has_data = idata_dir.is_dir()
&& std::fs::read_dir(&idata_dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.file_name() != "~index.dat")
})
.unwrap_or(false);
let project_valid = gpr_file.exists()
&& gpr_file.metadata().map(|m| m.len() > 0).unwrap_or(false)
&& idata_has_data;
if project_valid {
eprintln!("=== Using cached test project: {:?} ===", gpr_file);
return;
}
if gpr_file.exists() {
eprintln!("=== Project cache invalid (missing program data), re-importing ===");
let _ = std::fs::remove_file(&gpr_file);
let _ = std::fs::remove_dir_all(&rep_dir);
}
eprintln!("=== Setting up test project (import + analyze) ===");
eprintln!("Project dir: {:?}", projects_dir);
eprintln!("Step 1: Importing binary {:?} ...", binary);
let ghidra_bin = assert_cmd::cargo::cargo_bin!("ghidra");
let import_status = run_cli_with_timeout(
ghidra_bin,
&[
"import",
binary.to_str().unwrap(),
"--project",
project,
"--program",
program,
],
Duration::from_secs(300),
);
match import_status {
Ok(status) => {
eprintln!("Import finished with status: {}", status);
if !status.success() {
eprintln!("Warning: Import may have failed, but continuing...");
} else {
eprintln!("Binary imported successfully");
}
}
Err(e) => eprintln!("Import error: {}", e),
}
eprintln!("Step 2: Running analysis...");
let analyze_status = run_cli_with_timeout(
ghidra_bin,
&[
"analyze",
"--project",
project,
"--program",
program,
],
Duration::from_secs(600),
);
match analyze_status {
Ok(status) => {
eprintln!("Analyze finished with status: {}", status);
if !status.success() {
eprintln!("Warning: Analyze may have failed, but continuing...");
} else {
eprintln!("Analysis complete");
}
}
Err(e) => eprintln!("Analyze error: {}", e),
}
eprintln!("=== Test project setup complete ===");
});
}
pub struct DaemonTestHarness {
port: u16,
pid: Option<u32>,
data_dir: PathBuf,
project: String,
project_path: PathBuf,
}
impl DaemonTestHarness {
pub fn new(project: &str, program: &str) -> Result<Self> {
let data_dir = get_unique_data_dir();
let project_path = dirs::cache_dir()
.context("Could not determine cache directory")?
.join("ghidra-cli")
.join("projects")
.join(project);
let config = ghidra_cli::config::Config::load().context("Failed to load config")?;
let ghidra_install_dir = config
.ghidra_install_dir
.clone()
.or_else(|| config.get_ghidra_install_dir().ok())
.context("Ghidra installation directory not configured")?;
let port = ghidra_cli::ghidra::bridge::ensure_bridge_running(
&project_path,
&ghidra_install_dir,
ghidra_cli::ghidra::bridge::BridgeStartMode::Process {
program_name: program.to_string(),
},
)?;
let pid = ghidra_cli::ghidra::bridge::read_pid_file(&project_path)
.ok()
.flatten();
Ok(Self {
port,
pid,
data_dir,
project: project.to_string(),
project_path,
})
}
pub fn client(&self) -> Result<ghidra_cli::ipc::client::BridgeClient> {
Ok(ghidra_cli::ipc::client::BridgeClient::new(self.port))
}
pub fn data_dir(&self) -> &PathBuf {
&self.data_dir
}
pub fn project(&self) -> &str {
&self.project
}
pub fn port(&self) -> u16 {
self.port
}
}
impl Drop for DaemonTestHarness {
fn drop(&mut self) {
let file_pid = ghidra_cli::ghidra::bridge::read_pid_file(&self.project_path)
.ok()
.flatten();
let _ = ghidra_cli::ghidra::bridge::stop_bridge(&self.project_path);
let mut pids_to_wait: Vec<u32> = Vec::new();
if let Some(pid) = file_pid {
pids_to_wait.push(pid);
}
if let Some(pid) = self.pid {
if !pids_to_wait.contains(&pid) {
pids_to_wait.push(pid);
}
}
let max_wait = if cfg!(windows) {
Duration::from_secs(30)
} else {
Duration::from_secs(15)
};
for pid in &pids_to_wait {
let start = std::time::Instant::now();
while start.elapsed() < max_wait {
if !ghidra_cli::ghidra::bridge::is_pid_alive(*pid) {
break;
}
std::thread::sleep(Duration::from_millis(500));
}
}
let _ = ghidra_cli::ghidra::bridge::cleanup_stale_files(&self.project_path);
let _ = std::fs::remove_dir_all(&self.data_dir);
}
}
fn get_unique_data_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!("ghidra-data-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("Failed to create test data dir");
dir
}
pub fn run_cli_with_timeout(
bin: &std::path::Path,
args: &[&str],
timeout: Duration,
) -> Result<std::process::ExitStatus> {
use std::process::{Command, Stdio};
let mut child = Command::new(bin)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.context("Failed to spawn CLI command")?;
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => return Ok(status),
Ok(None) => {
if start.elapsed() > timeout {
eprintln!("Command timed out after {}s, killing...", timeout.as_secs());
let _ = child.kill();
let _ = child.wait();
anyhow::bail!("Command timed out after {}s", timeout.as_secs());
}
std::thread::sleep(Duration::from_secs(1));
}
Err(e) => anyhow::bail!("Error waiting for command: {}", e),
}
}
}
#[macro_export]
macro_rules! require_ghidra {
() => {
let doctor = assert_cmd::cargo::cargo_bin_cmd!("ghidra")
.arg("doctor")
.output()
.expect("Failed to run ghidra doctor");
let output = String::from_utf8_lossy(&doctor.stdout);
if !output.contains("OK") || output.contains("NOT FOUND") || output.contains("FAILED") {
panic!(
"Ghidra not properly installed — tests MUST fail without Ghidra.\n\
Doctor output: {}",
output
);
}
};
}