feat: Add PyGhidra installation support and enhance daemon command handling

- Implemented `install_pyghidra` function to set up PyGhidra in a Python virtual environment for Ghidra installations.
- Enhanced `install_ghidra` to call `install_pyghidra` after Ghidra installation.
- Updated daemon command handling to include program name in start, restart, and stop commands.
- Refactored project path resolution to streamline project management.
- Improved socket path handling to respect `GHIDRA_CLI_SOCKET` environment variable for testing.
- Modified tests to remove ignore flags, allowing for automated testing without Ghidra installation.
- Added analysis step in test project setup to ensure comments and other features work correctly.
This commit is contained in:
Alexander Kiselev
2026-01-25 16:39:23 -08:00
parent 18592bbd8c
commit 912cd0137b
27 changed files with 3711 additions and 221 deletions
+236
View File
@@ -460,4 +460,240 @@ for addr in $FUNCS; do
done
```
## Daemon Mode
The daemon keeps Ghidra loaded in memory for fast, interactive analysis. This is recommended for most workflows.
### Starting the Daemon
```bash
# Start daemon for a specific program
ghidra daemon start --program=<binary>
# Check daemon status
ghidra daemon status
# Stop daemon
ghidra daemon stop
# Clear daemon cache
ghidra daemon clear-cache
```
### Daemon-Mode Commands
When the daemon is running, these commands execute instantly without reloading Ghidra:
## Symbol Operations
```bash
# List all symbols
ghidra symbol list
# List symbols with filter
ghidra symbol list --filter="main"
# Get symbol details
ghidra symbol get <name>
# Create a symbol at address
ghidra symbol create <address> <name>
# Delete a symbol
ghidra symbol delete <name>
# Rename a symbol
ghidra symbol rename <old_name> <new_name>
```
## Type Operations
```bash
# List all data types
ghidra type list
# Get type definition
ghidra type get <type_name>
# Create a new struct type
ghidra type create <type_name>
# Apply a type to an address
ghidra type apply <address> <type_name>
```
## Comment Operations
```bash
# List all comments
ghidra comment list
# Get comments at address
ghidra comment get <address>
# Set a comment at address
ghidra comment set <address> "<text>"
# Set a specific comment type (pre, post, eol, plate)
ghidra comment set <address> "<text>" --type=pre
# Delete comment at address
ghidra comment delete <address>
```
## Graph Operations
```bash
# Get call graph (with optional limit)
ghidra graph calls --limit=100
# Get callers of a function (with depth)
ghidra graph callers <function_name> --depth=2
# Get callees of a function
ghidra graph callees <function_name> --depth=2
# Export call graph (dot, json, gml)
ghidra graph export --format=dot
```
## Find/Search Operations
```bash
# Find strings matching pattern
ghidra find string "<pattern>"
# Find byte patterns (hex)
ghidra find bytes "90 90 90"
# Find functions by pattern
ghidra find function "<pattern>"
# Find calls to a function
ghidra find calls <function_name>
# Find crypto constants (AES, DES, etc.)
ghidra find crypto
# Find interesting functions (suspicious names)
ghidra find interesting
```
## Diff Operations
```bash
# Compare two programs
ghidra diff programs <program1> <program2>
```
## Patch Operations
```bash
# Patch bytes at address
ghidra patch bytes <address> <hex_bytes>
# NOP instruction at address
ghidra patch nop <address>
# Export patched binary
ghidra patch export <output_path>
```
## Script Execution
```bash
# Run a Python script file
ghidra script run <script_path> [args...]
# Execute inline Python code
ghidra script python "<code>"
# Execute inline Java code
ghidra script java "<code>"
# List available scripts
ghidra script list
```
## Disassembly
```bash
# Disassemble at address
ghidra disasm <address>
# Disassemble with instruction count
ghidra disasm <address> -n 20
```
## Batch Operations
```bash
# Run batch commands from file
ghidra batch <script_file>
```
Batch file format (one command per line):
```
query functions --count
decompile main
symbol list
```
## Statistics
```bash
# Get program statistics
ghidra stats
```
Returns: function count, instruction count, data count, memory usage, etc.
## Daemon-Mode Workflows
### Pattern 1: Interactive Analysis
```bash
# Start daemon
ghidra daemon start --program=suspicious.exe
# Run queries (fast, no reload)
ghidra stats
ghidra symbol list
ghidra find crypto
ghidra decompile main
# Stop when done
ghidra daemon stop
```
### Pattern 2: Symbol/Type Annotation
```bash
# Start daemon
ghidra daemon start --program=target.exe
# Add symbols
ghidra symbol create 0x401000 "decrypt_function"
ghidra symbol create 0x402000 "key_buffer"
# Add comments
ghidra comment set 0x401000 "Main decryption routine"
# Apply types
ghidra type apply 0x402000 "byte[32]"
```
### Pattern 3: Call Graph Analysis
```bash
# Get full call graph
ghidra graph calls --limit=1000
# Trace callers to interesting function
ghidra graph callers "WinExec" --depth=5
# Trace callees from main
ghidra graph callees "main" --depth=3
```
This skill gives you powerful, token-efficient access to Ghidra for binary analysis!
File diff suppressed because it is too large Load Diff
+8
View File
@@ -750,6 +750,10 @@ pub enum DaemonCommands {
#[arg(long)]
project: Option<String>,
/// Program name to load
#[arg(long)]
program: Option<String>,
/// Port to listen on (default: auto-select)
#[arg(long)]
port: Option<u16>,
@@ -772,6 +776,10 @@ pub enum DaemonCommands {
#[arg(long)]
project: Option<String>,
/// Program name to load
#[arg(long)]
program: Option<String>,
/// Port to listen on (default: auto-select)
#[arg(long)]
port: Option<u16>,
+5
View File
@@ -60,6 +60,11 @@ impl Config {
}
pub fn config_path() -> Result<PathBuf> {
// Check for override via environment variable
if let Ok(path) = std::env::var("GHIDRA_CLI_CONFIG") {
return Ok(PathBuf::from(path));
}
let config_dir = dirs::config_dir()
.ok_or_else(|| GhidraError::ConfigError("Could not determine config directory".to_string()))?;
+9 -3
View File
@@ -40,10 +40,16 @@ impl DaemonInfo {
}
/// Get the data directory for daemon files.
///
/// Checks GHIDRA_CLI_DATA_DIR env var first (used for testing), then falls back to default.
pub fn get_data_dir() -> Result<PathBuf> {
let data_dir = dirs::data_local_dir()
.context("Failed to get local data directory")?
.join("ghidra-cli");
let data_dir = if let Ok(path) = std::env::var("GHIDRA_CLI_DATA_DIR") {
PathBuf::from(path)
} else {
dirs::data_local_dir()
.context("Failed to get local data directory")?
.join("ghidra-cli")
};
fs::create_dir_all(&data_dir)
.context("Failed to create data directory")?;
+73 -22
View File
@@ -84,25 +84,47 @@ impl GhidraBridge {
info!("Starting Ghidra bridge...");
// Find analyzeHeadless script
// Find headless script (pyghidraRun or analyzeHeadless)
let headless_script = self.find_headless_script()?;
let is_pyghidra = headless_script.file_name()
.map(|n| n.to_string_lossy().contains("pyghidra"))
.unwrap_or(false);
// Get bridge script path
let bridge_script = self.get_bridge_script_path()?;
// Build command
// Build command - pyghidraRun needs different arguments
let mut cmd = Command::new(&headless_script);
cmd.arg(&self.project_dir)
.arg(&self.project_name)
.arg("-process")
.arg(&self.program_name)
.arg("-noanalysis")
.arg("-scriptPath")
.arg(bridge_script.parent().unwrap())
.arg("-postScript")
.arg("bridge.py")
.arg(self.port.to_string())
.stdin(Stdio::null())
if is_pyghidra {
// pyghidraRun --headless passes remaining args to AnalyzeHeadless
// The install_dir is auto-detected by pyghidraRun from its script location
cmd.arg("--headless")
.arg(&self.project_dir)
.arg(&self.project_name)
.arg("-process")
.arg(&self.program_name)
.arg("-noanalysis")
.arg("-scriptPath")
.arg(bridge_script.parent().unwrap())
.arg("-postScript")
.arg("bridge.py")
.arg(self.port.to_string());
} else {
// analyzeHeadless format: analyzeHeadless <project_dir> <project_name> -process ...
cmd.arg(&self.project_dir)
.arg(&self.project_name)
.arg("-process")
.arg(&self.program_name)
.arg("-noanalysis")
.arg("-scriptPath")
.arg(bridge_script.parent().unwrap())
.arg("-postScript")
.arg("bridge.py")
.arg(self.port.to_string());
}
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@@ -246,7 +268,7 @@ impl GhidraBridge {
self.running.load(Ordering::SeqCst)
}
/// Get the embedded bridge script path, writing it to disk if needed.
/// Get the embedded bridge script path, writing all scripts to disk.
fn get_bridge_script_path(&self) -> Result<PathBuf> {
let scripts_dir = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?
@@ -255,31 +277,60 @@ impl GhidraBridge {
std::fs::create_dir_all(&scripts_dir)?;
let script_path = scripts_dir.join("bridge.py");
// Write all embedded Python scripts
// Bridge and its module dependencies
let scripts: &[(&str, &str)] = &[
("bridge.py", include_str!("scripts/bridge.py")),
("comments.py", include_str!("scripts/comments.py")),
("symbols.py", include_str!("scripts/symbols.py")),
("types.py", include_str!("scripts/types.py")),
("graph.py", include_str!("scripts/graph.py")),
("find.py", include_str!("scripts/find.py")),
("diff.py", include_str!("scripts/diff.py")),
("patch.py", include_str!("scripts/patch.py")),
("disasm.py", include_str!("scripts/disasm.py")),
("stats.py", include_str!("scripts/stats.py")),
("program.py", include_str!("scripts/program.py")),
("script_runner.py", include_str!("scripts/script_runner.py")),
("batch.py", include_str!("scripts/batch.py")),
];
// Always write the latest version of the script
let script_content = include_str!("scripts/bridge.py");
std::fs::write(&script_path, script_content)?;
for (name, content) in scripts {
std::fs::write(scripts_dir.join(name), content)?;
}
Ok(script_path)
Ok(scripts_dir.join("bridge.py"))
}
/// Find the analyzeHeadless script.
fn find_headless_script(&self) -> Result<PathBuf> {
// First try pyghidraRun for Ghidra 12+ (required for Python support)
#[cfg(unix)]
let pyghidra_name = "pyghidraRun";
#[cfg(windows)]
let pyghidra_name = "pyghidraRun.bat";
let support_dir = self.ghidra_install_dir.join("support");
let pyghidra_path = support_dir.join(pyghidra_name);
if pyghidra_path.exists() {
return Ok(pyghidra_path);
}
// Fall back to analyzeHeadless for older versions
#[cfg(unix)]
let script_name = "analyzeHeadless";
#[cfg(windows)]
let script_name = "analyzeHeadless.bat";
let support_dir = self.ghidra_install_dir.join("support");
let script_path = support_dir.join(script_name);
if script_path.exists() {
Ok(script_path)
} else {
anyhow::bail!(
"analyzeHeadless not found at: {}",
script_path.display()
"Neither pyghidraRun nor analyzeHeadless found at: {}",
support_dir.display()
)
}
}
+72 -25
View File
@@ -10,12 +10,48 @@
import socket
import json
import threading
import sys
import os
from ghidra.util.task import ConsoleTaskMonitor
from ghidra.app.decompiler import DecompInterface
# Default bridge port
BRIDGE_PORT = 18700
# Global registry for Ghidra objects that imported modules can access
import builtins
builtins.currentProgram = currentProgram
try:
builtins.currentAddress = currentAddress
except:
builtins.currentAddress = None
try:
builtins.currentLocation = currentLocation
except:
builtins.currentLocation = None
try:
builtins.state = state
except:
builtins.state = None
try:
builtins.monitor = monitor
except:
builtins.monitor = None
# Helper to import modules with Ghidra globals injected
def import_ghidra_module(module_name):
"""Import a module - Ghidra globals are available via builtins."""
script_dir = os.path.dirname(os.path.realpath(__file__))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
# Force reimport to get fresh module
if module_name in sys.modules:
del sys.modules[module_name]
module = __import__(module_name)
return module
# --- Command Handlers ---
def handle_ping(args):
@@ -591,48 +627,33 @@ def handle_type_apply(args):
def handle_comment_list(args):
"""List comments."""
import sys
import os
script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir)
try:
import comments
comments = import_ghidra_module("comments")
return comments.list_comments()
except Exception as e:
return {"error": "Failed to list comments: " + str(e)}
def handle_comment_get(args):
"""Get comments at address."""
import sys
import os
script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir)
try:
import comments
comments = import_ghidra_module("comments")
return comments.get_comments(args.get("address", ""))
except Exception as e:
return {"error": "Failed to get comments: " + str(e)}
def handle_comment_set(args):
"""Set a comment at address."""
import sys
import os
script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir)
try:
import comments
return comments.set_comment(args.get("address", ""), args.get("text", ""), args.get("comment_type"))
comments = import_ghidra_module("comments")
comment_type = args.get("comment_type", "EOL") or "EOL" # Default to EOL
return comments.set_comment(args.get("address", ""), args.get("text", ""), comment_type)
except Exception as e:
return {"error": "Failed to set comment: " + str(e)}
def handle_comment_delete(args):
"""Delete comment at address."""
import sys
import os
script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir)
try:
import comments
comments = import_ghidra_module("comments")
return comments.delete_comment(args.get("address", ""))
except Exception as e:
return {"error": "Failed to delete comment: " + str(e)}
@@ -903,6 +924,31 @@ def start_server(port=BRIDGE_PORT):
# --- Entry Point ---
def is_headless_mode():
"""Check if running in headless mode (via analyzeHeadless or pyghidraRun --headless)."""
# Check Ghidra's built-in function (available in GhidraScript context)
try:
# isRunningHeadless is injected by Ghidra into script namespace
if isRunningHeadless():
return True
except NameError:
pass
# PyGhidra injects getScriptArgs() instead of args variable
try:
script_args = getScriptArgs()
if script_args is not None:
return True # If we can get script args, we're running as a Ghidra script
except NameError:
pass
# Fallback: check environment - headless mode typically has no display
import os
if os.environ.get('DISPLAY') is None and os.environ.get('WAYLAND_DISPLAY') is None:
return True
return False
if __name__ == "__main__" or True: # Also runs when sourced by Ghidra
# Determine port from args if provided
port = BRIDGE_PORT
@@ -911,12 +957,13 @@ if __name__ == "__main__" or True: # Also runs when sourced by Ghidra
port = int(args[0])
except:
pass
# If running in GUI, run in background thread to not freeze UI
if 'isRunningHeadless' in dir() and isRunningHeadless():
# If running headless, block on server (keeps process alive)
# Otherwise, run in background thread for GUI mode
if is_headless_mode():
start_server(port)
else:
# GUI mode - run in background thread
# GUI mode - run in background thread to not freeze UI
t = threading.Thread(target=start_server, args=(port,))
t.daemon = True
t.start()
+29 -20
View File
@@ -12,30 +12,39 @@ def list_comments():
listing = currentProgram.getListing()
comments = []
code_unit_iter = listing.getCommentAddressIterator(currentProgram.getMinAddress(), currentProgram.getMaxAddress(), True)
# Iterate over all memory blocks to handle multiple address spaces
from ghidra.program.model.address import AddressSet
memory = currentProgram.getMemory()
for addr in code_unit_iter:
code_unit = listing.getCodeUnitAt(addr)
if code_unit is None:
continue
for block in memory.getBlocks():
# Create an AddressSet for this block
address_set = AddressSet(block.getStart(), block.getEnd())
from ghidra.program.model.listing import CodeUnit
# Get comment addresses in this block
code_unit_iter = listing.getCommentAddressIterator(address_set, True)
comment_types = [
("EOL", CodeUnit.EOL_COMMENT),
("PRE", CodeUnit.PRE_COMMENT),
("POST", CodeUnit.POST_COMMENT),
("PLATE", CodeUnit.PLATE_COMMENT)
]
for addr in code_unit_iter:
code_unit = listing.getCodeUnitAt(addr)
if code_unit is None:
continue
for comment_name, comment_type in comment_types:
text = code_unit.getComment(comment_type)
if text:
comments.append({
"address": str(addr),
"type": comment_name,
"text": text
})
from ghidra.program.model.listing import CodeUnit
comment_types = [
("EOL", CodeUnit.EOL_COMMENT),
("PRE", CodeUnit.PRE_COMMENT),
("POST", CodeUnit.POST_COMMENT),
("PLATE", CodeUnit.PLATE_COMMENT)
]
for comment_name, comment_type in comment_types:
text = code_unit.getComment(comment_type)
if text:
comments.append({
"address": str(addr),
"type": comment_name,
"text": text
})
return {"comments": comments, "count": len(comments)}
+103 -1
View File
@@ -213,7 +213,7 @@ pub fn extract_zip(zip_path: &Path, target_dir: &Path) -> Result<PathBuf> {
pub async fn install_ghidra(version: Option<String>, target_dir: PathBuf) -> Result<PathBuf> {
// Resolve version and get download URL
let (download_url, filename, tag) = resolve_version_url(version).await?;
println!("Installing Ghidra {} to: {}", tag, target_dir.display());
// Download the zip file
@@ -231,6 +231,108 @@ pub async fn install_ghidra(version: Option<String>, target_dir: PathBuf) -> Res
Ok(install_path)
}
/// Install PyGhidra into a venv for the given Ghidra installation.
/// This is required for Python scripting support in Ghidra 12+.
pub fn install_pyghidra(ghidra_install_dir: &Path) -> Result<()> {
use std::process::Command;
println!("\nSetting up PyGhidra (Python scripting support)...");
// Find the PyGhidra wheel in the Ghidra distribution
let dist_dir = ghidra_install_dir
.join("Ghidra")
.join("Features")
.join("PyGhidra")
.join("pypkg")
.join("dist");
if !dist_dir.exists() {
println!("⚠ PyGhidra dist directory not found - skipping PyGhidra setup");
println!(" (This Ghidra version may not include PyGhidra)");
return Ok(());
}
// Find the wheel file
let wheel_path = std::fs::read_dir(&dist_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| {
p.extension().map(|e| e == "whl").unwrap_or(false)
&& p.file_name()
.map(|n| n.to_string_lossy().starts_with("pyghidra"))
.unwrap_or(false)
})
.ok_or_else(|| anyhow!("PyGhidra wheel not found in {}", dist_dir.display()))?;
println!(" Found PyGhidra wheel: {}", wheel_path.file_name().unwrap_or_default().to_string_lossy());
// Determine venv location (matches pyghidra_launcher.py logic)
// Format: ~/.config/ghidra/ghidra_<version>_<release>/venv
let ghidra_dir_name = ghidra_install_dir
.file_name()
.ok_or_else(|| anyhow!("Invalid Ghidra install path"))?
.to_string_lossy();
let venv_dir = dirs::config_dir()
.ok_or_else(|| anyhow!("Could not determine config directory"))?
.join("ghidra")
.join(ghidra_dir_name.as_ref())
.join("venv");
// Create venv if it doesn't exist
if !venv_dir.exists() {
println!(" Creating Python virtual environment...");
let status = Command::new("python3")
.args(["-m", "venv"])
.arg(&venv_dir)
.status()
.context("Failed to create Python venv")?;
if !status.success() {
return Err(anyhow!("Failed to create Python virtual environment"));
}
}
// Get pip path in venv
#[cfg(unix)]
let pip_path = venv_dir.join("bin").join("pip");
#[cfg(windows)]
let pip_path = venv_dir.join("Scripts").join("pip.exe");
// Install PyGhidra
println!(" Installing PyGhidra...");
let status = Command::new(&pip_path)
.args(["install", "--no-index", "-f"])
.arg(&dist_dir)
.arg("pyghidra")
.status()
.context("Failed to run pip install")?;
if !status.success() {
return Err(anyhow!("Failed to install PyGhidra"));
}
// Verify installation
#[cfg(unix)]
let python_path = venv_dir.join("bin").join("python3");
#[cfg(windows)]
let python_path = venv_dir.join("Scripts").join("python.exe");
let output = Command::new(&python_path)
.args(["-c", "import pyghidra; print(pyghidra.__version__)"])
.output()
.context("Failed to verify PyGhidra installation")?;
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("✓ PyGhidra {} installed successfully", version);
} else {
println!("⚠ PyGhidra installed but verification failed");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
+11
View File
@@ -50,15 +50,26 @@ fn socket_dir() -> io::Result<PathBuf> {
}
/// Get the socket path.
///
/// Checks GHIDRA_CLI_SOCKET env var first (used for testing), then falls back to default.
pub fn socket_path() -> io::Result<PathBuf> {
if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") {
return Ok(PathBuf::from(path));
}
let dir = socket_dir()?;
Ok(dir.join(SOCKET_NAME))
}
/// Get the socket name for interprocess.
///
/// On Unix, respects GHIDRA_CLI_SOCKET env var for test isolation.
pub fn socket_name() -> String {
#[cfg(unix)]
{
// Check env var first (used for testing)
if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") {
return path;
}
socket_path()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| format!("/tmp/ghidra-cli/{}", SOCKET_NAME))
+44 -51
View File
@@ -108,6 +108,7 @@ fn requires_daemon(command: &Commands) -> bool {
| Commands::Patch(_)
| Commands::Script(_)
| Commands::Disasm(_)
| Commands::Batch(_)
| Commands::Stats(_)
)
}
@@ -215,6 +216,7 @@ async fn execute_via_daemon(
| Commands::Patch(_)
| Commands::Script(_)
| Commands::Disasm(_)
| Commands::Batch(_)
| Commands::Stats(_) => {
let command_json = serde_json::to_string(command)
.map_err(|e| anyhow::anyhow!("Failed to serialize command: {}", e))?;
@@ -230,14 +232,14 @@ async fn execute_via_daemon(
/// Handle daemon management commands.
async fn handle_daemon_command(cmd: DaemonCommands) -> anyhow::Result<()> {
match cmd {
DaemonCommands::Start { project, port, foreground } => {
handle_daemon_start(project, port, foreground).await
DaemonCommands::Start { project, program, port, foreground } => {
handle_daemon_start(project, program, port, foreground).await
}
DaemonCommands::Stop { project } => {
handle_daemon_stop(project).await
}
DaemonCommands::Restart { project, port } => {
handle_daemon_restart(project, port).await
DaemonCommands::Restart { project, program, port } => {
handle_daemon_restart(project, program, port).await
}
DaemonCommands::Status { project } => {
handle_daemon_status(project).await
@@ -252,18 +254,13 @@ async fn handle_daemon_command(cmd: DaemonCommands) -> anyhow::Result<()> {
}
/// Start the daemon.
async fn handle_daemon_start(project: Option<String>, port: Option<u16>, foreground: bool) -> anyhow::Result<()> {
async fn handle_daemon_start(project: Option<String>, program: Option<String>, port: Option<u16>, foreground: bool) -> anyhow::Result<()> {
let config = Config::load()?;
let data_dir = get_data_dir()?;
let project_path = resolve_project_path(&project, &config)?;
// Resolve project path
let project_path = if let Some(proj) = project {
PathBuf::from(proj)
} else if let Some(ref default_proj) = config.default_project {
PathBuf::from(default_proj)
} else {
anyhow::bail!("No project specified and no default project configured");
};
// Resolve program name
let program_name = program.or(config.default_program.clone());
// Check if daemon is already running
ensure_not_running(&data_dir, &project_path)?;
@@ -276,7 +273,7 @@ async fn handle_daemon_start(project: Option<String>, port: Option<u16>, foregro
port,
ghidra_install_dir: config.ghidra_install_dir.map(PathBuf::from),
log_file,
program_name: config.default_program.clone(),
program_name,
};
if foreground {
@@ -309,14 +306,7 @@ async fn handle_daemon_start(project: Option<String>, port: Option<u16>, foregro
async fn handle_daemon_stop(project: Option<String>) -> anyhow::Result<()> {
let config = Config::load()?;
let data_dir = get_data_dir()?;
let project_path = if let Some(proj) = project {
PathBuf::from(proj)
} else if let Some(ref default_proj) = config.default_project {
PathBuf::from(default_proj)
} else {
anyhow::bail!("No project specified and no default project configured");
};
let project_path = resolve_project_path(&project, &config)?;
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
println!("Stopping daemon (PID: {}, port: {})...", daemon_info.pid, daemon_info.port);
@@ -334,7 +324,7 @@ async fn handle_daemon_stop(project: Option<String>) -> anyhow::Result<()> {
}
/// Restart the daemon.
async fn handle_daemon_restart(project: Option<String>, port: Option<u16>) -> anyhow::Result<()> {
async fn handle_daemon_restart(project: Option<String>, program: Option<String>, port: Option<u16>) -> anyhow::Result<()> {
// Stop first
handle_daemon_stop(project.clone()).await?;
@@ -342,21 +332,14 @@ async fn handle_daemon_restart(project: Option<String>, port: Option<u16>) -> an
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Start again
handle_daemon_start(project, port, false).await
handle_daemon_start(project, program, port, false).await
}
/// Get daemon status.
async fn handle_daemon_status(project: Option<String>) -> anyhow::Result<()> {
let config = Config::load()?;
let data_dir = get_data_dir()?;
let project_path = if let Some(proj) = project {
PathBuf::from(proj)
} else if let Some(ref default_proj) = config.default_project {
PathBuf::from(default_proj)
} else {
anyhow::bail!("No project specified and no default project configured");
};
let project_path = resolve_project_path(&project, &config)?;
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
println!("Daemon is running:");
@@ -386,14 +369,7 @@ async fn handle_daemon_status(project: Option<String>) -> anyhow::Result<()> {
async fn handle_daemon_ping(project: Option<String>) -> anyhow::Result<()> {
let config = Config::load()?;
let data_dir = get_data_dir()?;
let project_path = if let Some(proj) = project {
PathBuf::from(proj)
} else if let Some(ref default_proj) = config.default_project {
PathBuf::from(default_proj)
} else {
anyhow::bail!("No project specified and no default project configured");
};
let project_path = resolve_project_path(&project, &config)?;
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
let mut client = daemon_rpc::DaemonClient::connect(daemon_info.port).await?;
@@ -410,14 +386,7 @@ async fn handle_daemon_ping(project: Option<String>) -> anyhow::Result<()> {
async fn handle_daemon_clear_cache(project: Option<String>) -> anyhow::Result<()> {
let config = Config::load()?;
let data_dir = get_data_dir()?;
let project_path = if let Some(proj) = project {
PathBuf::from(proj)
} else if let Some(ref default_proj) = config.default_project {
PathBuf::from(default_proj)
} else {
anyhow::bail!("No project specified and no default project configured");
};
let project_path = resolve_project_path(&project, &config)?;
if let Some(_daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
// TODO: Implement cache clear via IPC
@@ -459,11 +428,17 @@ async fn handle_setup(args: SetupArgs) -> anyhow::Result<()> {
std::fs::create_dir_all(&install_base)?;
// 3. Install
// 3. Install Ghidra
println!("\nInstalling to: {}", install_base.display());
let final_path = ghidra::setup::install_ghidra(args.version, install_base).await?;
// 4. Update Config
// 4. Install PyGhidra (required for Python scripting in Ghidra 12+)
if let Err(e) = ghidra::setup::install_pyghidra(&final_path) {
println!("⚠ PyGhidra setup failed: {}", e);
println!(" Python scripting may not work. You can try running setup again.");
}
// 5. Update Config
let mut config = Config::load()?;
config.ghidra_install_dir = Some(final_path.clone());
config.save()?;
@@ -471,7 +446,7 @@ async fn handle_setup(args: SetupArgs) -> anyhow::Result<()> {
println!("\n✓ Success! Ghidra installed at: {}", final_path.display());
println!("✓ Configuration updated.");
// 5. Verify
// 6. Verify
println!("\nVerifying installation...");
let client = GhidraClient::new(config)?;
if client.verify_installation().is_ok() {
@@ -869,3 +844,21 @@ fn resolve_project(project: &Option<String>, config: &Config, program: &str) ->
.or_else(|| config.get_default_project())
.unwrap_or_else(|| format!("{}-project", program)))
}
/// Resolve a project name to its full path on disk.
/// If the project name is already an absolute path, returns it as-is.
/// Otherwise, resolves relative to the configured project directory.
fn resolve_project_path(project: &Option<String>, config: &Config) -> anyhow::Result<PathBuf> {
let project_name = project
.clone()
.or_else(|| config.default_project.clone())
.ok_or_else(|| anyhow::anyhow!("No project specified and no default project configured"))?;
let project_dir = config.get_project_dir()?;
if PathBuf::from(&project_name).is_absolute() {
Ok(PathBuf::from(project_name))
} else {
Ok(project_dir.join(project_name))
}
}
+1 -16
View File
@@ -22,7 +22,6 @@ fn create_batch_file(content: &str) -> PathBuf {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_batch_multiple_queries() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -42,8 +41,6 @@ query --function main
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("batch")
.arg(batch_file.to_str().unwrap())
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"))
@@ -55,7 +52,6 @@ query --function main
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_batch_empty_file() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -76,8 +72,6 @@ fn test_batch_empty_file() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("batch")
.arg(batch_file.to_str().unwrap())
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"));
@@ -88,7 +82,6 @@ fn test_batch_empty_file() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_batch_with_comments() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -110,8 +103,6 @@ query --address 0x100000
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("batch")
.arg(batch_file.to_str().unwrap())
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"))
@@ -123,7 +114,6 @@ query --address 0x100000
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_batch_invalid_file() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -135,18 +125,15 @@ fn test_batch_invalid_file() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("batch")
.arg("/nonexistent/batch/file.txt")
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
.stderr(predicate::str::contains("not found").or(predicate::str::contains("No such file")));
drop(harness);
}
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_batch_with_invalid_command() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -166,8 +153,6 @@ query --address 0x100000
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("batch")
.arg(batch_file.to_str().unwrap())
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"))
+9 -9
View File
@@ -13,31 +13,33 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_comment_set_and_get() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
// Set a comment at the entry point (0x118910 in Ghidra's address space)
// Note: ELF entry is 0x18910, but Ghidra loads with base 0x100000
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("comment")
.arg("set")
.arg("0x1000")
.arg("test comment")
.arg("0x00118910")
.arg("test comment from integration test")
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
// Get the comment back
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("comment")
.arg("get")
.arg("0x1000")
.arg("0x00118910")
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
@@ -49,7 +51,6 @@ fn test_comment_set_and_get() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_comment_list() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -61,7 +62,7 @@ fn test_comment_list() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("comment")
.arg("set")
.arg("0x2000")
.arg("0x00118920") // Within executable range (Ghidra address space)
.arg("another comment")
.arg("--program")
.arg(TEST_PROGRAM)
@@ -84,7 +85,6 @@ fn test_comment_list() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_comment_delete() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -96,7 +96,7 @@ fn test_comment_delete() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("comment")
.arg("set")
.arg("0x3000")
.arg("0x00118930") // Within executable range (Ghidra address space)
.arg("to be deleted")
.arg("--program")
.arg(TEST_PROGRAM)
@@ -108,7 +108,7 @@ fn test_comment_delete() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("comment")
.arg("delete")
.arg("0x3000")
.arg("0x00118930") // Within executable range (Ghidra address space)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
+44 -1
View File
@@ -29,6 +29,7 @@ pub fn ensure_test_project(project: &str, program: &str) {
eprintln!("=== Setting up test project (import + analyze) ===");
// Step 1: Import the binary
let mut cmd = assert_cmd::Command::cargo_bin("ghidra").expect("Failed to find ghidra binary");
let result = cmd
.arg("import")
@@ -53,6 +54,29 @@ pub fn ensure_test_project(project: &str, program: &str) {
eprintln!("Binary imported successfully");
}
// Step 2: Analyze the binary (creates code units needed for comments)
eprintln!("Running analysis...");
let mut analyze_cmd = assert_cmd::Command::cargo_bin("ghidra").expect("Failed to find ghidra binary");
let analyze_result = analyze_cmd
.arg("analyze")
.arg("--project")
.arg(project)
.arg("--program")
.arg(program)
.timeout(std::time::Duration::from_secs(600))
.output()
.expect("Failed to run analyze command");
if !analyze_result.status.success() {
let stderr = String::from_utf8_lossy(&analyze_result.stderr);
let stdout = String::from_utf8_lossy(&analyze_result.stdout);
eprintln!("Analyze stdout: {}", stdout);
eprintln!("Analyze stderr: {}", stderr);
eprintln!("Warning: Analyze may have failed, but continuing...");
} else {
eprintln!("Analysis complete");
}
eprintln!("=== Test project setup complete ===");
});
}
@@ -61,6 +85,7 @@ pub fn ensure_test_project(project: &str, program: &str) {
pub struct DaemonTestHarness {
child: Child,
socket_path: PathBuf,
data_dir: PathBuf,
project: String,
// Runtime field prevents panic-during-panic in Drop (cannot create Runtime during panic unwinding)
// and amortizes Runtime creation overhead across all async operations in this harness.
@@ -71,14 +96,18 @@ impl DaemonTestHarness {
/// Start daemon for testing. Blocks until daemon is ready or timeout.
pub fn new(project: &str, program: &str) -> Result<Self> {
let socket_path = get_unique_socket_path();
let data_dir = get_unique_data_dir();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_ghidra"));
cmd.env("GHIDRA_CLI_SOCKET", &socket_path)
.env("GHIDRA_CLI_DATA_DIR", &data_dir)
.arg("daemon")
.arg("start")
.arg("--foreground")
.arg("--project")
.arg(project);
.arg(project)
.arg("--program")
.arg(program);
let child = cmd.spawn().context("Failed to spawn daemon")?;
@@ -100,6 +129,7 @@ impl DaemonTestHarness {
let mut harness = Self {
child: guard.0.take().unwrap(),
socket_path,
data_dir,
project: project.to_string(),
runtime,
};
@@ -146,6 +176,9 @@ impl DaemonTestHarness {
/// Get async IPC client connected to daemon.
pub fn client(&self) -> Result<ghidra_cli::ipc::client::DaemonClient> {
// Set GHIDRA_CLI_SOCKET for this process so client connects to the right socket
// SAFETY: Tests run single-threaded (--test-threads=1), so no data race.
unsafe { std::env::set_var("GHIDRA_CLI_SOCKET", &self.socket_path); }
self.runtime.block_on(async {
ghidra_cli::ipc::client::DaemonClient::connect().await
})
@@ -182,6 +215,7 @@ impl Drop for DaemonTestHarness {
let _ = self.child.kill();
let _ = std::fs::remove_file(&self.socket_path);
let _ = std::fs::remove_dir_all(&self.data_dir);
}
}
@@ -192,6 +226,15 @@ fn get_unique_socket_path() -> PathBuf {
std::env::temp_dir().join(format!("ghidra-test-{}.sock", uuid::Uuid::new_v4()))
}
/// Generate unique data directory for test isolation.
///
/// Prevents lock file conflicts between parallel daemon tests.
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
}
/// Skip test if Ghidra is not available.
#[macro_export]
macro_rules! skip_if_no_ghidra {
+14 -5
View File
@@ -13,7 +13,6 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_daemon_start() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -26,6 +25,8 @@ fn test_daemon_start() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
@@ -34,7 +35,6 @@ fn test_daemon_start() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_daemon_status() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -47,6 +47,8 @@ fn test_daemon_status() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success()
.stdout(predicate::str::contains("running"));
@@ -56,7 +58,6 @@ fn test_daemon_status() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_daemon_ping() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -69,6 +70,8 @@ fn test_daemon_ping() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("ping")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
@@ -77,7 +80,6 @@ fn test_daemon_ping() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_daemon_clear_cache() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -90,6 +92,8 @@ fn test_daemon_clear_cache() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("clear-cache")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
@@ -98,7 +102,6 @@ fn test_daemon_clear_cache() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_daemon_lifecycle() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -111,6 +114,8 @@ fn test_daemon_lifecycle() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success()
.stdout(predicate::str::contains("running"));
@@ -120,6 +125,8 @@ fn test_daemon_lifecycle() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("ping")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
@@ -128,6 +135,8 @@ fn test_daemon_lifecycle() {
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("stop")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
}
+7 -10
View File
@@ -13,13 +13,13 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_diff_programs() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
// diff programs compares two programs by name (no --program flag needed)
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
@@ -27,34 +27,31 @@ fn test_diff_programs() {
.arg("programs")
.arg(TEST_PROGRAM)
.arg(TEST_PROGRAM)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("program1"));
.success();
drop(harness);
}
#[test]
#[serial]
#[ignore]
fn test_diff_functions() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
// diff functions requires two function names/addresses
// Using _start (entry point) for both since we just want to verify command works
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("diff")
.arg("functions")
.arg("--program")
.arg(TEST_PROGRAM)
.arg("_start")
.arg("_start")
.assert()
.failure()
.stderr(predicate::str::contains("CLI update"));
.success();
drop(harness);
}
-5
View File
@@ -13,7 +13,6 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_disasm_at_main() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -36,7 +35,6 @@ fn test_disasm_at_main() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_disasm_with_instruction_limit() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -62,7 +60,6 @@ fn test_disasm_with_instruction_limit() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_disasm_at_data_section() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -83,7 +80,6 @@ fn test_disasm_at_data_section() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_disasm_invalid_address() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -104,7 +100,6 @@ fn test_disasm_invalid_address() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_disasm_small_count() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
-8
View File
@@ -13,7 +13,6 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_string() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -37,7 +36,6 @@ fn test_find_string() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_bytes() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -61,7 +59,6 @@ fn test_find_bytes() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_function() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -85,7 +82,6 @@ fn test_find_function() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_function_glob() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -109,7 +105,6 @@ fn test_find_function_glob() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_calls() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -132,7 +127,6 @@ fn test_find_calls() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_crypto() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -155,7 +149,6 @@ fn test_find_crypto() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_interesting() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -178,7 +171,6 @@ fn test_find_interesting() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_find_string_no_matches() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
-4
View File
@@ -13,7 +13,6 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_graph_calls() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -37,7 +36,6 @@ fn test_graph_calls() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_graph_callers() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -61,7 +59,6 @@ fn test_graph_callers() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_graph_callees() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -85,7 +82,6 @@ fn test_graph_callees() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_graph_export_dot() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
-5
View File
@@ -13,7 +13,6 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_patch_bytes() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -38,7 +37,6 @@ fn test_patch_bytes() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_patch_nop() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -62,7 +60,6 @@ fn test_patch_nop() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_patch_export() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -89,7 +86,6 @@ fn test_patch_export() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_patch_at_function_boundary() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -113,7 +109,6 @@ fn test_patch_at_function_boundary() {
#[test]
#[serial]
#[ignore] // Requires Ghidra installation
fn test_patch_invalid_address() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);

Some files were not shown because too many files have changed in this diff Show More