use std::{
env,
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use ghidra::{
AddressRange, AnalysisMode, AnalysisOptions, DecompileOptions, DecompileStatus, Error,
FunctionDescriptor, Ghidra, LaunchOptions, ProgramLoadOptions, ProgramOpenOptions, ProgramPath,
SymbolKind,
};
use tempfile::TempDir;
#[test]
fn project_program_lifecycle_exposes_typed_function_data_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project_location = temp.path().join("ghidra-projects").join("lifecycle");
{
let project = ghidra.open_or_create_project(&project_location, "lifecycle")?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let functions = program.functions()?;
let checksum = functions
.iter()
.find(|function| function.name == "checksum8")
.expect("checksum8 function exists");
let decompiled = program.decompile_function(checksum, 60)?;
assert_eq!(program.name()?, "tiny_codec");
assert_eq!(checksum.name, "checksum8");
assert_eq!(decompiled.status, DecompileStatus::Completed);
assert!(program.basic_block_count(checksum)? > 0);
assert!(program.instruction_count(checksum)? > 0);
program.save()?;
}
let project = ghidra.open_or_create_project(&project_location, "lifecycle")?;
let program = project.open_or_import_program(&binary)?;
assert!(
program
.functions()?
.iter()
.any(|function| function.name == "parse_header")
);
Ok(())
}
#[test]
fn committed_transaction_persists_function_plate_comment_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project_location = temp.path().join("ghidra-projects").join("commit");
{
let project = ghidra.open_or_create_project(&project_location, "commit")?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let checksum = checksum_function(&program)?;
let transaction = program.start_transaction("set checksum8 comment")?;
transaction.set_function_plate_comment(&checksum, "committed checksum comment")?;
transaction.commit()?;
assert_eq!(
program.function_plate_comment(&checksum)?.as_deref(),
Some("committed checksum comment")
);
program.save()?;
}
let project = ghidra.open_or_create_project(&project_location, "commit")?;
let program = project.open_or_import_program(&binary)?;
let checksum = checksum_function(&program)?;
assert_eq!(
program.function_plate_comment(&checksum)?.as_deref(),
Some("committed checksum comment")
);
Ok(())
}
#[test]
fn dropped_transaction_rolls_back_function_plate_comment_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("drop-rollback"),
"drop-rollback",
)?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let checksum = checksum_function(&program)?;
let original = program.function_plate_comment(&checksum)?;
{
let transaction = program.start_transaction("rolled back checksum8 comment")?;
transaction.set_function_plate_comment(&checksum, "dropped transaction comment")?;
}
assert_eq!(program.function_plate_comment(&checksum)?, original);
Ok(())
}
#[test]
fn explicit_transaction_rollback_discards_function_plate_comment_when_requested()
-> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path()
.join("ghidra-projects")
.join("explicit-rollback"),
"explicit-rollback",
)?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let checksum = checksum_function(&program)?;
let original = program.function_plate_comment(&checksum)?;
let transaction = program.start_transaction("explicit rollback checksum8 comment")?;
transaction.set_function_plate_comment(&checksum, "rolled back transaction comment")?;
transaction.rollback()?;
assert_eq!(program.function_plate_comment(&checksum)?, original);
Ok(())
}
#[test]
fn program_navigation_returns_typed_model_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("navigation"),
"navigation",
)?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let checksum = checksum_function(&program)?;
let parsed = program.parse_address(checksum.entry.as_str())?;
assert_eq!(parsed, checksum.entry);
assert_eq!(
program.function_at(&checksum.entry)?.as_ref(),
Some(&checksum)
);
assert_eq!(
program.function_containing(&checksum.entry)?.as_ref(),
Some(&checksum)
);
assert!(program.symbols()?.iter().any(|symbol| {
symbol.name == "checksum8"
&& symbol.address == checksum.entry
&& symbol.kind == SymbolKind::Function
}));
assert!(
program
.symbols_at(&checksum.entry)?
.iter()
.any(|symbol| symbol.name == "checksum8")
);
assert!(
program
.find_symbols("checksum8", true)?
.iter()
.any(|symbol| symbol.address == checksum.entry)
);
assert!(
program
.references_to(&checksum.entry)?
.iter()
.any(|reference| reference.call)
);
let metadata = program.metadata()?;
assert!(
metadata
.functions
.iter()
.any(|function| function.name == "checksum8")
);
assert!(
metadata
.symbols
.iter()
.any(|symbol| symbol.name == "checksum8")
);
assert!(!metadata.types.is_empty());
assert!(program.memory_blocks()?.iter().any(|block| {
block.execute
&& block.range.start.space == checksum.entry.space
&& block.range.start.offset <= checksum.entry.offset
&& checksum.entry.offset <= block.range.end.offset
}));
Ok(())
}
#[test]
fn program_decompiler_returns_direct_results_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("decompiler"),
"decompiler",
)?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let checksum = checksum_function(&program)?;
let decompiler = program.open_decompiler()?;
let result = decompiler.decompile(&checksum, 60)?;
assert_eq!(result.status, DecompileStatus::Completed);
assert!(result.c.as_deref().is_some_and(|c| c.contains("checksum8")));
assert!(
result
.signature
.as_deref()
.is_some_and(|signature| signature.contains("checksum8"))
);
assert!(result.prototype.is_some());
assert!(
result
.pcode
.as_ref()
.is_some_and(|pcode| pcode.op_count > 0)
);
assert!(
result
.high_pcode
.as_ref()
.is_some_and(|graph| !graph.ops.is_empty() && !graph.blocks.is_empty())
);
assert!(
result
.data_flow
.as_ref()
.is_some_and(|graph| !graph.ops.is_empty()
&& !graph.varnodes.is_empty()
&& !graph.variables.is_empty()
&& !graph.edges.is_empty())
);
let one_shot = program.decompile_function(&checksum, 60)?;
assert_eq!(one_shot.status, DecompileStatus::Completed);
assert_eq!(one_shot.signature, result.signature);
let option_based = program.decompile_function_with_options(
&checksum,
DecompileOptions::new(60).with_monitor_timeout(Duration::from_secs(10))?,
)?;
assert_eq!(option_based.status, DecompileStatus::Completed);
assert_eq!(option_based.signature, result.signature);
Ok(())
}
#[test]
fn program_listing_returns_typed_instructions_and_data_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("listing"),
"listing",
)?;
let program = project.open_or_import_program(&binary)?;
program.analyze()?;
let checksum = checksum_function(&program)?;
let instructions = program.instructions_for_function(&checksum)?;
let first = instructions.first().expect("checksum8 has instructions");
let last = instructions.last().expect("checksum8 has instructions");
assert_eq!(first.address, checksum.entry);
assert_eq!(first.length as usize, first.bytes.len());
assert!(
instructions
.iter()
.any(|instruction| !instruction.pcode.is_empty())
);
assert!(
instructions
.iter()
.any(|instruction| !instruction.operands.is_empty())
);
assert_eq!(
program.instruction_at(&checksum.entry)?.as_ref(),
Some(first)
);
let ranged = program.instructions_in_range(&AddressRange {
start: first.address.clone(),
end: last.address.clone(),
})?;
assert!(
ranged
.iter()
.any(|instruction| instruction.address == first.address)
);
let graph = program.control_flow_graph(&checksum)?;
assert!(!graph.blocks.is_empty());
assert!(graph.blocks.iter().any(|block| {
block
.instruction_addresses
.iter()
.any(|address| address == &checksum.entry)
}));
let single_graph = program.control_flow_graphs(std::slice::from_ref(&checksum))?;
assert_eq!(single_graph.len(), 1);
assert_eq!(single_graph[0].function, checksum);
assert_eq!(single_graph[0].graph, graph);
let functions = program.functions()?;
let graphs = program.control_flow_graphs(&functions)?;
assert_eq!(graphs.len(), functions.len());
for (function, graph) in functions.iter().zip(graphs.iter()) {
assert_eq!(&graph.function, function);
}
assert!(program.control_flow_graphs(&[])?.is_empty());
let call_graph = program.call_graph()?;
assert!(call_graph.nodes.iter().any(|node| node.name == "checksum8"));
assert!(call_graph.nodes.iter().any(|node| node.name == "main"));
assert!(call_graph.edges.iter().any(|edge| {
edge.source_id.contains("main") || edge.destination_id.contains(&checksum.entry.to_string())
}));
let main = program
.functions()?
.into_iter()
.find(|function| function.name == "main")
.expect("main function exists");
let data_refs = program.data_refs(&main)?;
let data_ref = data_refs.first().expect("main has fixture data references");
let data = program
.data_containing(data_ref)?
.expect("data exists for fixture data reference");
assert!(data.length > 0);
assert!(!data.display.is_empty());
Ok(())
}
#[test]
fn java_exceptions_are_structured_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("structured-error"),
"structured-error",
)?;
let program = project.open_or_import_program(&binary)?;
let error = program
.parse_address("not-an-address")
.expect_err("invalid address should fail");
let Error::JavaException {
class_name,
message,
stack_trace,
..
} = error
else {
panic!("expected structured Java exception");
};
assert_eq!(class_name, "java.lang.IllegalArgumentException");
assert!(message.contains("invalid address not-an-address"));
assert!(stack_trace.contains("ProgramModel.parseAddress"));
Ok(())
}
#[test]
fn typed_lifecycle_collects_function_summaries_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("extract"),
"extract",
)?;
let program = project
.open_or_import_program_with_options(ProgramLoadOptions::new(binary)?)?
.program;
program.analyze_with_options(AnalysisOptions::if_needed())?;
let functions = program.functions()?;
let checksum = functions
.iter()
.find(|function| function.name == "checksum8")
.expect("checksum8 function exists");
let single = program.function_summary(checksum)?;
assert!(
functions
.iter()
.any(|function| function.name == "checksum8")
);
assert!(
functions
.iter()
.any(|function| function.name == "parse_header")
);
assert_eq!(&single.function, checksum);
assert!(single.instruction_count > 0);
assert_eq!(
single.basic_block_count,
program.basic_block_count(checksum)?
);
assert_eq!(
single.instruction_count,
program.instruction_count(checksum)?
);
assert_eq!(single.callers, program.callers(checksum)?);
assert_eq!(single.callees, program.callees(checksum)?);
assert_eq!(single.strings, program.strings(checksum)?);
assert_eq!(single.constants, program.constants(checksum)?);
assert_eq!(single.data_refs, program.data_refs(checksum)?);
assert_eq!(single.imports, program.imports(checksum)?);
assert_eq!(single.exports, program.exports(checksum)?);
assert_eq!(single.source_map, program.source_map(checksum)?);
let summaries = program.function_summaries(&functions)?;
assert_eq!(summaries.len(), functions.len());
for (function, summary) in functions.iter().zip(summaries.iter()) {
assert_eq!(&summary.function, function);
}
assert_eq!(
summaries
.iter()
.find(|summary| &summary.function == checksum)
.expect("checksum8 summary exists"),
&single
);
assert!(program.function_summaries(&[])?.is_empty());
program.save()?;
Ok(())
}
#[test]
fn program_loading_options_control_project_path_and_reuse_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("load-options"),
"load-options",
)?;
let path = ProgramPath::new("/firmware/samples", "custom-tiny")?;
let options = ProgramLoadOptions::new(&binary)?
.with_path(path.clone())
.with_timeout(Duration::from_secs(10))?;
let loaded = project.open_or_import_program_with_options(options.clone())?;
assert!(!loaded.info.opened_existing);
assert_eq!(loaded.info.project_path, "/firmware/samples/custom-tiny");
assert_eq!(loaded.info.folder, "/firmware/samples");
assert_eq!(loaded.info.name, "custom-tiny");
assert_eq!(loaded.info.monitor.timeout_seconds, Some(10));
assert!(!loaded.info.monitor.cancelled);
assert_eq!(loaded.program.name()?, "tiny_codec");
loaded.program.save()?;
loaded.program.close()?;
let reopened = project.open_or_import_program_with_options(options)?;
assert!(reopened.info.opened_existing);
assert_eq!(reopened.info.name, "custom-tiny");
assert_eq!(reopened.info.monitor.timeout_seconds, Some(10));
assert!(!reopened.info.monitor.cancelled);
assert_eq!(reopened.program.name()?, "tiny_codec");
reopened.program.close()?;
let opened = project.open_program(ProgramOpenOptions::new(path))?;
assert_eq!(opened.name()?, "tiny_codec");
Ok(())
}
#[test]
fn analysis_modes_return_typed_reports_when_requested() -> anyhow::Result<()> {
if !live_ghidra_requested() {
return Ok(());
}
let temp = TempDir::new()?;
let binary = compile_tiny_codec(temp.path())?;
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(
temp.path().join("ghidra-projects").join("analysis-options"),
"analysis-options",
)?;
let program = project.open_or_import_program(&binary)?;
let skipped = program.analyze_with_options(AnalysisOptions::skip())?;
assert_eq!(skipped.mode, AnalysisMode::Skip);
assert!(!skipped.analyzed);
assert_eq!(skipped.monitor.timeout_seconds, None);
assert!(!skipped.monitor.cancelled);
let if_needed = program.analyze_with_options(AnalysisOptions::if_needed())?;
assert_eq!(if_needed.mode, AnalysisMode::IfNeeded);
assert_eq!(if_needed.monitor.timeout_seconds, None);
let force = program
.analyze_with_options(AnalysisOptions::force().with_timeout(Duration::from_secs(10))?)?;
assert_eq!(force.mode, AnalysisMode::Force);
assert!(force.analyzed);
assert_eq!(force.monitor.timeout_seconds, Some(10));
assert!(!force.monitor.cancelled);
Ok(())
}
fn checksum_function(program: &ghidra::Program<'_>) -> anyhow::Result<FunctionDescriptor> {
Ok(program
.functions()?
.into_iter()
.find(|function| function.name == "checksum8")
.expect("checksum8 function exists"))
}
fn live_ghidra_requested() -> bool {
env::var("GHIDRA_RUN_LIVE_TESTS").ok().as_deref() == Some("1")
}
fn compile_tiny_codec(temp: &Path) -> anyhow::Result<PathBuf> {
let binary = temp.join("tiny_codec");
let compiler = env::var_os("CC").unwrap_or_else(|| "cc".into());
let status = Command::new(compiler)
.arg("-std=c11")
.arg("-Wall")
.arg("-Wextra")
.arg("-Og")
.arg("-g")
.arg("-fno-inline")
.arg("-o")
.arg(&binary)
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tiny_codec.c"))
.status()?;
if !status.success() {
anyhow::bail!("C compiler failed with status {status}");
}
Ok(binary)
}