mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
The diff subcommands were missing --project fields, causing clap to reject the argument when tests passed --project to diff commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1386 lines
54 KiB
Rust
1386 lines
54 KiB
Rust
mod cli;
|
|
mod config;
|
|
mod error;
|
|
mod filter;
|
|
mod format;
|
|
mod ghidra;
|
|
mod ipc;
|
|
mod query;
|
|
|
|
use clap::Parser;
|
|
use cli::{Cli, Commands, QueryOptions};
|
|
use config::Config;
|
|
use error::GhidraError;
|
|
use format::{auto_detect_format, DefaultFormatter, Formatter, OutputFormat};
|
|
use ghidra::bridge::{self, BridgeStartMode, BridgeStatus};
|
|
use ghidra::GhidraClient;
|
|
use ipc::client::BridgeClient;
|
|
use query::Query;
|
|
use std::io::IsTerminal;
|
|
use std::path::PathBuf;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
use tracing_subscriber::Layer;
|
|
|
|
fn main() {
|
|
let cli = Cli::parse();
|
|
|
|
// --- Logging setup ---
|
|
// File layer: always writes at debug level
|
|
let log_dir = dirs::data_local_dir()
|
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
|
.join("ghidra-cli");
|
|
let _ = std::fs::create_dir_all(&log_dir);
|
|
let file_appender = tracing_appender::rolling::daily(&log_dir, "ghidra-cli.log");
|
|
let file_layer = tracing_subscriber::fmt::layer()
|
|
.with_writer(file_appender)
|
|
.with_ansi(false)
|
|
.with_filter(tracing_subscriber::EnvFilter::new("debug"));
|
|
|
|
// Stdout layer: only if -v/-vv/-vvv is specified
|
|
let stdout_layer = match cli.verbose {
|
|
1 => Some("warn"),
|
|
2 => Some("info"),
|
|
3.. => Some("debug"),
|
|
_ => None,
|
|
}
|
|
.map(|level| {
|
|
tracing_subscriber::fmt::layer()
|
|
.with_writer(std::io::stderr)
|
|
.with_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level)),
|
|
)
|
|
});
|
|
|
|
tracing_subscriber::registry()
|
|
.with(file_layer)
|
|
.with(stdout_layer)
|
|
.init();
|
|
|
|
let result = match &cli.command {
|
|
Commands::Setup(_) => {
|
|
// Setup needs async for downloading
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
rt.block_on(run_setup(cli))
|
|
}
|
|
Commands::Start { .. }
|
|
| Commands::Stop { .. }
|
|
| Commands::Restart { .. }
|
|
| Commands::Status { .. }
|
|
| Commands::Ping { .. } => handle_bridge_command(cli),
|
|
_ => run_command(cli),
|
|
};
|
|
|
|
if let Err(e) = result {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
/// Run a command, starting the bridge if needed.
|
|
fn run_command(cli: Cli) -> anyhow::Result<()> {
|
|
match &cli.command {
|
|
// Non-bridge commands
|
|
Commands::Init => handle_init(),
|
|
Commands::Doctor => handle_doctor(),
|
|
Commands::Version => handle_version(),
|
|
Commands::Config(cmd) => handle_config_command(cmd.clone()),
|
|
Commands::SetDefault(args) => handle_set_default(args.clone()),
|
|
Commands::Project(args) => handle_project_command(args.command.clone()),
|
|
// Commands requiring bridge
|
|
_ if requires_bridge(&cli.command) => run_with_bridge(cli),
|
|
_ => {
|
|
println!("Command not yet implemented");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Determines if a command requires the bridge to be running.
|
|
fn requires_bridge(command: &Commands) -> bool {
|
|
matches!(
|
|
command,
|
|
Commands::Import(_)
|
|
| Commands::Analyze(_)
|
|
| Commands::Query(_)
|
|
| Commands::Decompile(_)
|
|
| Commands::Function(_)
|
|
| Commands::Strings(_)
|
|
| Commands::Memory(_)
|
|
| Commands::Dump(_)
|
|
| Commands::Summary(_)
|
|
| Commands::XRef(_)
|
|
| Commands::Symbol(_)
|
|
| Commands::Type(_)
|
|
| Commands::Comment(_)
|
|
| Commands::Graph(_)
|
|
| Commands::Find(_)
|
|
| Commands::Diff(_)
|
|
| Commands::Patch(_)
|
|
| Commands::Script(_)
|
|
| Commands::Disasm(_)
|
|
| Commands::Batch(_)
|
|
| Commands::Stats(_)
|
|
| Commands::Program(_)
|
|
)
|
|
}
|
|
|
|
/// Extract the project name from a command's args (if present).
|
|
fn extract_project_from_command(command: &Commands) -> Option<String> {
|
|
match command {
|
|
Commands::Import(args) => args.project.clone(),
|
|
Commands::Analyze(args) => args.project.clone(),
|
|
Commands::Query(args) => args.project.clone(),
|
|
Commands::Summary(args) => args.options.project.clone(),
|
|
Commands::Decompile(args) => args.options.project.clone(),
|
|
Commands::Function(cmd) => match cmd {
|
|
cli::FunctionCommands::List(opts) => opts.project.clone(),
|
|
cli::FunctionCommands::Decompile(args) => args.options.project.clone(),
|
|
cli::FunctionCommands::Get(args) => args.options.project.clone(),
|
|
cli::FunctionCommands::Disasm(args) => args.options.project.clone(),
|
|
cli::FunctionCommands::Calls(args) => args.options.project.clone(),
|
|
cli::FunctionCommands::XRefs(args) => args.options.project.clone(),
|
|
cli::FunctionCommands::Rename(args) => args.project.clone(),
|
|
cli::FunctionCommands::Create(args) => args.project.clone(),
|
|
cli::FunctionCommands::Delete(args) => args.options.project.clone(),
|
|
},
|
|
Commands::Strings(cmd) => match cmd {
|
|
cli::StringsCommands::List(opts) => opts.project.clone(),
|
|
cli::StringsCommands::Refs(args) => args.options.project.clone(),
|
|
},
|
|
Commands::Memory(cmd) => match cmd {
|
|
cli::MemoryCommands::Map(opts) => opts.project.clone(),
|
|
cli::MemoryCommands::Read(args) => args.options.project.clone(),
|
|
cli::MemoryCommands::Write(args) => args.project.clone(),
|
|
cli::MemoryCommands::Search(args) => args.options.project.clone(),
|
|
},
|
|
Commands::Dump(cmd) => match cmd {
|
|
cli::DumpCommands::Imports(opts) => opts.project.clone(),
|
|
cli::DumpCommands::Exports(opts) => opts.project.clone(),
|
|
cli::DumpCommands::Functions(opts) => opts.project.clone(),
|
|
cli::DumpCommands::Strings(opts) => opts.project.clone(),
|
|
},
|
|
Commands::XRef(cmd) => match cmd {
|
|
cli::XRefCommands::To(args) => args.options.project.clone(),
|
|
cli::XRefCommands::From(args) => args.options.project.clone(),
|
|
cli::XRefCommands::List(args) => args.options.project.clone(),
|
|
},
|
|
Commands::Stats(args) => args.options.project.clone(),
|
|
Commands::Disasm(args) => args.options.project.clone(),
|
|
Commands::Find(cmd) => match cmd {
|
|
cli::FindCommands::String(args) => args.options.project.clone(),
|
|
cli::FindCommands::Bytes(args) => args.options.project.clone(),
|
|
cli::FindCommands::Function(args) => args.options.project.clone(),
|
|
cli::FindCommands::Calls(args) => args.options.project.clone(),
|
|
cli::FindCommands::Crypto(opts) => opts.project.clone(),
|
|
cli::FindCommands::Interesting(opts) => opts.project.clone(),
|
|
},
|
|
Commands::Graph(cmd) => match cmd {
|
|
cli::GraphCommands::Calls(opts) => opts.project.clone(),
|
|
cli::GraphCommands::Callers(args) => args.options.project.clone(),
|
|
cli::GraphCommands::Callees(args) => args.options.project.clone(),
|
|
cli::GraphCommands::Export(args) => args.options.project.clone(),
|
|
},
|
|
Commands::Comment(cmd) => match cmd {
|
|
cli::CommentCommands::List(opts) => opts.project.clone(),
|
|
cli::CommentCommands::Get(args) => args.options.project.clone(),
|
|
cli::CommentCommands::Set(args) => args.project.clone(),
|
|
cli::CommentCommands::Delete(args) => args.options.project.clone(),
|
|
},
|
|
Commands::Symbol(cmd) => match cmd {
|
|
cli::SymbolCommands::List(opts) => opts.project.clone(),
|
|
cli::SymbolCommands::Get(args) => args.options.project.clone(),
|
|
cli::SymbolCommands::Create(args) => args.project.clone(),
|
|
cli::SymbolCommands::Delete(args) => args.options.project.clone(),
|
|
cli::SymbolCommands::Rename(args) => args.project.clone(),
|
|
},
|
|
Commands::Type(cmd) => match cmd {
|
|
cli::TypeCommands::List(opts) => opts.project.clone(),
|
|
cli::TypeCommands::Get(args) => args.options.project.clone(),
|
|
cli::TypeCommands::Create(args) => args.project.clone(),
|
|
cli::TypeCommands::Apply(args) => args.project.clone(),
|
|
},
|
|
Commands::Patch(cmd) => match cmd {
|
|
cli::PatchCommands::Bytes(args) => args.project.clone(),
|
|
cli::PatchCommands::Nop(args) => args.project.clone(),
|
|
cli::PatchCommands::Export(args) => args.project.clone(),
|
|
},
|
|
Commands::Script(cmd) => match cmd {
|
|
cli::ScriptCommands::Run(args) => args.project.clone(),
|
|
cli::ScriptCommands::Python(args) => args.project.clone(),
|
|
cli::ScriptCommands::Java(args) => args.project.clone(),
|
|
cli::ScriptCommands::List => None,
|
|
},
|
|
Commands::Program(cmd) => match cmd {
|
|
cli::ProgramCommands::List(args) => args.project.clone(),
|
|
cli::ProgramCommands::Open(args) => args.project.clone(),
|
|
cli::ProgramCommands::Close(args) => args.project.clone(),
|
|
cli::ProgramCommands::Delete(args) => args.project.clone(),
|
|
cli::ProgramCommands::Info(args) => args.project.clone(),
|
|
cli::ProgramCommands::Export(args) => args.project.clone(),
|
|
},
|
|
Commands::Diff(cmd) => match cmd {
|
|
cli::DiffCommands::Programs(args) => args.project.clone(),
|
|
cli::DiffCommands::Functions(args) => args.project.clone(),
|
|
},
|
|
Commands::Batch(args) => args.project.clone(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Extract the --program argument from a command's args, if present.
|
|
/// Enables program switching before query execution when the requested
|
|
/// program differs from the bridge's current program.
|
|
fn extract_program_from_command(command: &Commands) -> Option<String> {
|
|
match command {
|
|
Commands::Analyze(args) => args.program.clone(),
|
|
Commands::Query(args) => args.program.clone(),
|
|
Commands::Summary(args) => args.options.program.clone(),
|
|
Commands::Decompile(args) => args.options.program.clone(),
|
|
Commands::Function(cmd) => match cmd {
|
|
cli::FunctionCommands::List(opts) => opts.program.clone(),
|
|
cli::FunctionCommands::Decompile(args) => args.options.program.clone(),
|
|
cli::FunctionCommands::Get(args) => args.options.program.clone(),
|
|
cli::FunctionCommands::Disasm(args) => args.options.program.clone(),
|
|
cli::FunctionCommands::Calls(args) => args.options.program.clone(),
|
|
cli::FunctionCommands::XRefs(args) => args.options.program.clone(),
|
|
cli::FunctionCommands::Rename(args) => args.program.clone(),
|
|
cli::FunctionCommands::Create(args) => args.program.clone(),
|
|
cli::FunctionCommands::Delete(args) => args.options.program.clone(),
|
|
},
|
|
Commands::Strings(cmd) => match cmd {
|
|
cli::StringsCommands::List(opts) => opts.program.clone(),
|
|
cli::StringsCommands::Refs(args) => args.options.program.clone(),
|
|
},
|
|
Commands::Memory(cmd) => match cmd {
|
|
cli::MemoryCommands::Map(opts) => opts.program.clone(),
|
|
cli::MemoryCommands::Read(args) => args.options.program.clone(),
|
|
cli::MemoryCommands::Write(args) => args.program.clone(),
|
|
cli::MemoryCommands::Search(args) => args.options.program.clone(),
|
|
},
|
|
Commands::Dump(cmd) => match cmd {
|
|
cli::DumpCommands::Imports(opts) => opts.program.clone(),
|
|
cli::DumpCommands::Exports(opts) => opts.program.clone(),
|
|
cli::DumpCommands::Functions(opts) => opts.program.clone(),
|
|
cli::DumpCommands::Strings(opts) => opts.program.clone(),
|
|
},
|
|
Commands::XRef(cmd) => match cmd {
|
|
cli::XRefCommands::To(args) => args.options.program.clone(),
|
|
cli::XRefCommands::From(args) => args.options.program.clone(),
|
|
cli::XRefCommands::List(args) => args.options.program.clone(),
|
|
},
|
|
Commands::Stats(args) => args.options.program.clone(),
|
|
Commands::Disasm(args) => args.options.program.clone(),
|
|
Commands::Find(cmd) => match cmd {
|
|
cli::FindCommands::String(args) => args.options.program.clone(),
|
|
cli::FindCommands::Bytes(args) => args.options.program.clone(),
|
|
cli::FindCommands::Function(args) => args.options.program.clone(),
|
|
cli::FindCommands::Calls(args) => args.options.program.clone(),
|
|
cli::FindCommands::Crypto(opts) => opts.program.clone(),
|
|
cli::FindCommands::Interesting(opts) => opts.program.clone(),
|
|
},
|
|
Commands::Graph(cmd) => match cmd {
|
|
cli::GraphCommands::Calls(opts) => opts.program.clone(),
|
|
cli::GraphCommands::Callers(args) => args.options.program.clone(),
|
|
cli::GraphCommands::Callees(args) => args.options.program.clone(),
|
|
cli::GraphCommands::Export(args) => args.options.program.clone(),
|
|
},
|
|
Commands::Comment(cmd) => match cmd {
|
|
cli::CommentCommands::List(opts) => opts.program.clone(),
|
|
cli::CommentCommands::Get(args) => args.options.program.clone(),
|
|
cli::CommentCommands::Set(args) => args.program.clone(),
|
|
cli::CommentCommands::Delete(args) => args.options.program.clone(),
|
|
},
|
|
Commands::Symbol(cmd) => match cmd {
|
|
cli::SymbolCommands::List(opts) => opts.program.clone(),
|
|
cli::SymbolCommands::Get(args) => args.options.program.clone(),
|
|
cli::SymbolCommands::Create(args) => args.program.clone(),
|
|
cli::SymbolCommands::Delete(args) => args.options.program.clone(),
|
|
cli::SymbolCommands::Rename(args) => args.program.clone(),
|
|
},
|
|
Commands::Type(cmd) => match cmd {
|
|
cli::TypeCommands::List(opts) => opts.program.clone(),
|
|
cli::TypeCommands::Get(args) => args.options.program.clone(),
|
|
cli::TypeCommands::Create(args) => args.program.clone(),
|
|
cli::TypeCommands::Apply(args) => args.program.clone(),
|
|
},
|
|
Commands::Patch(cmd) => match cmd {
|
|
cli::PatchCommands::Bytes(args) => args.program.clone(),
|
|
cli::PatchCommands::Nop(args) => args.program.clone(),
|
|
cli::PatchCommands::Export(args) => args.program.clone(),
|
|
},
|
|
Commands::Script(cmd) => match cmd {
|
|
cli::ScriptCommands::Run(args) => args.program.clone(),
|
|
cli::ScriptCommands::Python(args) => args.program.clone(),
|
|
cli::ScriptCommands::Java(args) => args.program.clone(),
|
|
cli::ScriptCommands::List => None,
|
|
},
|
|
Commands::Program(cmd) => match cmd {
|
|
cli::ProgramCommands::List(args) => args.program.clone(),
|
|
cli::ProgramCommands::Open(args) => args.program.clone(),
|
|
cli::ProgramCommands::Close(args) => args.program.clone(),
|
|
cli::ProgramCommands::Delete(args) => args.program.clone(),
|
|
cli::ProgramCommands::Info(args) => args.program.clone(),
|
|
cli::ProgramCommands::Export(args) => args.program.clone(),
|
|
},
|
|
Commands::Batch(args) => args.program.clone(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Extract QueryOptions from a command, if it has them.
|
|
fn extract_query_options(command: &Commands) -> Option<QueryOptions> {
|
|
match command {
|
|
Commands::Summary(args) => Some(args.options.clone()),
|
|
Commands::Decompile(args) => Some(args.options.clone()),
|
|
Commands::Disasm(args) => Some(args.options.clone()),
|
|
Commands::Stats(args) => Some(args.options.clone()),
|
|
Commands::Function(cmd) => match cmd {
|
|
cli::FunctionCommands::List(opts) => Some(opts.clone()),
|
|
cli::FunctionCommands::Get(args) => Some(args.options.clone()),
|
|
cli::FunctionCommands::Decompile(args) => Some(args.options.clone()),
|
|
cli::FunctionCommands::Disasm(args) => Some(args.options.clone()),
|
|
cli::FunctionCommands::Calls(args) => Some(args.options.clone()),
|
|
cli::FunctionCommands::XRefs(args) => Some(args.options.clone()),
|
|
cli::FunctionCommands::Delete(args) => Some(args.options.clone()),
|
|
_ => None,
|
|
},
|
|
Commands::Strings(cmd) => match cmd {
|
|
cli::StringsCommands::List(opts) => Some(opts.clone()),
|
|
cli::StringsCommands::Refs(args) => Some(args.options.clone()),
|
|
},
|
|
Commands::Memory(cmd) => match cmd {
|
|
cli::MemoryCommands::Map(opts) => Some(opts.clone()),
|
|
cli::MemoryCommands::Read(args) => Some(args.options.clone()),
|
|
cli::MemoryCommands::Search(args) => Some(args.options.clone()),
|
|
_ => None,
|
|
},
|
|
Commands::Dump(cmd) => match cmd {
|
|
cli::DumpCommands::Imports(opts) => Some(opts.clone()),
|
|
cli::DumpCommands::Exports(opts) => Some(opts.clone()),
|
|
cli::DumpCommands::Functions(opts) => Some(opts.clone()),
|
|
cli::DumpCommands::Strings(opts) => Some(opts.clone()),
|
|
},
|
|
Commands::XRef(cmd) => match cmd {
|
|
cli::XRefCommands::To(args) => Some(args.options.clone()),
|
|
cli::XRefCommands::From(args) => Some(args.options.clone()),
|
|
cli::XRefCommands::List(args) => Some(args.options.clone()),
|
|
},
|
|
Commands::Symbol(cmd) => match cmd {
|
|
cli::SymbolCommands::List(opts) => Some(opts.clone()),
|
|
cli::SymbolCommands::Get(args) => Some(args.options.clone()),
|
|
cli::SymbolCommands::Delete(args) => Some(args.options.clone()),
|
|
_ => None,
|
|
},
|
|
Commands::Type(cmd) => match cmd {
|
|
cli::TypeCommands::List(opts) => Some(opts.clone()),
|
|
cli::TypeCommands::Get(args) => Some(args.options.clone()),
|
|
_ => None,
|
|
},
|
|
Commands::Comment(cmd) => match cmd {
|
|
cli::CommentCommands::List(opts) => Some(opts.clone()),
|
|
cli::CommentCommands::Get(args) => Some(args.options.clone()),
|
|
_ => None,
|
|
},
|
|
Commands::Graph(cmd) => match cmd {
|
|
cli::GraphCommands::Calls(opts) => Some(opts.clone()),
|
|
cli::GraphCommands::Callers(args) => Some(args.options.clone()),
|
|
cli::GraphCommands::Callees(args) => Some(args.options.clone()),
|
|
cli::GraphCommands::Export(args) => Some(args.options.clone()),
|
|
},
|
|
Commands::Find(cmd) => match cmd {
|
|
cli::FindCommands::String(args) => Some(args.options.clone()),
|
|
cli::FindCommands::Bytes(args) => Some(args.options.clone()),
|
|
cli::FindCommands::Function(args) => Some(args.options.clone()),
|
|
cli::FindCommands::Calls(args) => Some(args.options.clone()),
|
|
cli::FindCommands::Crypto(opts) => Some(opts.clone()),
|
|
cli::FindCommands::Interesting(opts) => Some(opts.clone()),
|
|
},
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Run a command that requires the bridge.
|
|
fn run_with_bridge(cli: Cli) -> anyhow::Result<()> {
|
|
let config = Config::load()?;
|
|
|
|
// Extract project from command args, fall back to config default
|
|
let project_from_cmd = extract_project_from_command(&cli.command);
|
|
let project_path = resolve_project_path(&project_from_cmd, &config)?;
|
|
|
|
let ghidra_install_dir = config
|
|
.ghidra_install_dir
|
|
.clone()
|
|
.or_else(|| config.get_ghidra_install_dir().ok())
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"Ghidra installation directory not configured. Run 'ghidra setup' first."
|
|
)
|
|
})?;
|
|
|
|
// Import and Quick produce their own result and don't need execute_via_bridge.
|
|
// Other commands (including Analyze) produce a result via execute_via_bridge.
|
|
use serde_json::json;
|
|
let result = match &cli.command {
|
|
Commands::Import(args) => {
|
|
let binary_path = PathBuf::from(&args.binary);
|
|
if !binary_path.exists() {
|
|
anyhow::bail!("Binary not found: {}", args.binary);
|
|
}
|
|
|
|
// Check if bridge is already running
|
|
if let Some(port) = bridge::is_bridge_running(&project_path) {
|
|
// Bridge running - import via bridge command
|
|
let client = BridgeClient::new(port);
|
|
verify_bridge(&client)?;
|
|
let result = client.import_binary(&args.binary, args.program.as_deref())?;
|
|
|
|
let program_name = args.program.clone().unwrap_or_else(|| {
|
|
result
|
|
.get("program")
|
|
.and_then(|p| p.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string()
|
|
});
|
|
|
|
// Switch to the newly imported program
|
|
client.open_program(&program_name)?;
|
|
if !cli.quiet {
|
|
eprintln!("Successfully imported as: {}", program_name);
|
|
}
|
|
json!({
|
|
"command": "import",
|
|
"program": program_name,
|
|
"status": "success",
|
|
"data": result
|
|
})
|
|
} else {
|
|
// No bridge running - start one in import mode
|
|
if !cli.quiet {
|
|
eprintln!("Starting Ghidra bridge...");
|
|
}
|
|
let port = bridge::ensure_bridge_running(
|
|
&project_path,
|
|
&ghidra_install_dir,
|
|
BridgeStartMode::Import {
|
|
binary_path: args.binary.clone(),
|
|
},
|
|
)?;
|
|
let client = BridgeClient::new(port);
|
|
let info = client.program_info()?;
|
|
let program_name = args.program.clone().unwrap_or_else(|| {
|
|
info.get("name")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string()
|
|
});
|
|
if !cli.quiet {
|
|
eprintln!("Successfully imported as: {}", program_name);
|
|
}
|
|
json!({
|
|
"command": "import",
|
|
"program": program_name,
|
|
"status": "success",
|
|
"data": info
|
|
})
|
|
}
|
|
}
|
|
|
|
_ => {
|
|
// For all bridge commands (including Analyze), ensure bridge is running
|
|
let client = if let Some(port) = bridge::is_bridge_running(&project_path) {
|
|
let client = BridgeClient::new(port);
|
|
verify_bridge(&client)?;
|
|
client
|
|
} else {
|
|
// Auto-start bridge - use specific program if available, otherwise project mode
|
|
let mode = if let Some(program) = extract_program_from_command(&cli.command)
|
|
.or_else(|| config.get_default_program())
|
|
{
|
|
BridgeStartMode::Process {
|
|
program_name: program,
|
|
}
|
|
} else {
|
|
BridgeStartMode::Project
|
|
};
|
|
|
|
if !cli.quiet {
|
|
eprintln!("Starting Ghidra bridge...");
|
|
}
|
|
let port = bridge::ensure_bridge_running(&project_path, &ghidra_install_dir, mode)?;
|
|
if !cli.quiet {
|
|
eprintln!("Bridge ready.");
|
|
}
|
|
BridgeClient::new(port)
|
|
};
|
|
|
|
// Switch to requested program if it differs from the bridge's current program
|
|
if let Some(requested_program) = extract_program_from_command(&cli.command) {
|
|
if let Ok(info) = client.program_info() {
|
|
let current = info.get("name").and_then(|n| n.as_str()).unwrap_or("");
|
|
if current != requested_program {
|
|
client.open_program(&requested_program)?;
|
|
}
|
|
} else {
|
|
client.open_program(&requested_program)?;
|
|
}
|
|
}
|
|
|
|
execute_via_bridge(&client, &cli.command, cli.quiet, config.default_limit)?
|
|
}
|
|
};
|
|
|
|
// Check for .NET decompilation and warn
|
|
if !cli.quiet {
|
|
check_dotnet_decompile_warning(&cli.command, &result);
|
|
}
|
|
|
|
// Determine output format: explicit -o flag > --json/--pretty > TTY detection
|
|
let opts = extract_query_options(&cli.command);
|
|
let explicit_format = opts
|
|
.as_ref()
|
|
.and_then(|o| o.format.as_ref())
|
|
.map(|f| OutputFormat::from_str(f))
|
|
.transpose()
|
|
.ok()
|
|
.flatten();
|
|
|
|
let format = if let Some(fmt) = explicit_format {
|
|
fmt
|
|
} else if cli.pretty {
|
|
OutputFormat::Json
|
|
} else if cli.json || opts.as_ref().is_some_and(|o| o.json) {
|
|
OutputFormat::JsonCompact
|
|
} else {
|
|
auto_detect_format(std::io::stdout().is_terminal())
|
|
};
|
|
|
|
// Unwrap bridge response envelopes before formatting
|
|
let values = unwrap_bridge_response(result);
|
|
|
|
// Apply Rust-side query processing (filter, fields, sort) if QueryOptions are present
|
|
if let Some(opts) = &opts {
|
|
if let Ok(Some(query)) = Query::from_options(opts, format) {
|
|
let output = query.process_results(values)?;
|
|
if !output.is_empty() {
|
|
println!("{}", output);
|
|
}
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
let formatter = DefaultFormatter;
|
|
let output = formatter.format(&values, format)?;
|
|
if !output.is_empty() {
|
|
println!("{}", output);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Execute a command via the bridge client.
|
|
fn execute_via_bridge(
|
|
client: &BridgeClient,
|
|
command: &Commands,
|
|
quiet: bool,
|
|
default_limit: Option<usize>,
|
|
) -> anyhow::Result<serde_json::Value> {
|
|
use serde_json::json;
|
|
|
|
match command {
|
|
// Analyze shares the generic dispatch path with all query commands
|
|
Commands::Analyze(_) => {
|
|
if !quiet {
|
|
eprintln!("Analyzing...");
|
|
}
|
|
let result = client.analyze()?;
|
|
if !quiet {
|
|
eprintln!("Analysis complete!");
|
|
}
|
|
Ok(json!({
|
|
"command": "analyze",
|
|
"status": "success",
|
|
"data": result
|
|
}))
|
|
}
|
|
Commands::Query(args) => match args.data_type.as_str() {
|
|
"functions" => client.list_functions(args.limit.or(default_limit), None),
|
|
"strings" => client.list_strings(args.limit.or(default_limit)),
|
|
"imports" => client.list_imports(),
|
|
"exports" => client.list_exports(),
|
|
"memory" => client.memory_map(),
|
|
other => anyhow::bail!("Query type '{}' not supported", other),
|
|
},
|
|
Commands::Decompile(args) => client.decompile(args.target.clone()),
|
|
Commands::Function(cmd) => {
|
|
use cli::FunctionCommands;
|
|
match cmd {
|
|
FunctionCommands::List(opts) => {
|
|
client.list_functions(opts.limit.or(default_limit), None)
|
|
}
|
|
FunctionCommands::Decompile(args) => client.decompile(args.target.clone()),
|
|
FunctionCommands::Get(args) => {
|
|
client.send_command("get_function", Some(json!({"address": args.target})))
|
|
}
|
|
FunctionCommands::Disasm(args) => client.disasm(&args.target, None),
|
|
FunctionCommands::Calls(args) => client.find_calls(&args.target),
|
|
FunctionCommands::XRefs(args) => client.xrefs_to(args.target.clone()),
|
|
FunctionCommands::Rename(args) => client.send_command(
|
|
"rename_function",
|
|
Some(json!({
|
|
"old_name": args.old_name,
|
|
"new_name": args.new_name,
|
|
})),
|
|
),
|
|
FunctionCommands::Create(args) => client.send_command(
|
|
"create_function",
|
|
Some(json!({
|
|
"address": args.address,
|
|
"name": args.name,
|
|
})),
|
|
),
|
|
FunctionCommands::Delete(args) => client.send_command(
|
|
"delete_function",
|
|
Some(json!({
|
|
"address": args.target,
|
|
})),
|
|
),
|
|
}
|
|
}
|
|
Commands::Strings(cmd) => {
|
|
use cli::StringsCommands;
|
|
match cmd {
|
|
StringsCommands::List(opts) => client.list_strings(opts.limit.or(default_limit)),
|
|
StringsCommands::Refs(args) => client.xrefs_to(args.string.clone()),
|
|
}
|
|
}
|
|
Commands::Memory(cmd) => {
|
|
use cli::MemoryCommands;
|
|
match cmd {
|
|
MemoryCommands::Map(_) => client.memory_map(),
|
|
MemoryCommands::Read(args) => client.send_command(
|
|
"read_memory",
|
|
Some(json!({
|
|
"address": args.address,
|
|
"size": args.size,
|
|
})),
|
|
),
|
|
MemoryCommands::Write(args) => client.send_command(
|
|
"write_memory",
|
|
Some(json!({
|
|
"address": args.address,
|
|
"bytes": args.bytes,
|
|
})),
|
|
),
|
|
MemoryCommands::Search(args) => client.send_command(
|
|
"search_memory",
|
|
Some(json!({
|
|
"pattern": args.pattern,
|
|
})),
|
|
),
|
|
}
|
|
}
|
|
Commands::Dump(cmd) => {
|
|
use cli::DumpCommands;
|
|
match cmd {
|
|
DumpCommands::Imports(_) => client.list_imports(),
|
|
DumpCommands::Exports(_) => client.list_exports(),
|
|
DumpCommands::Functions(opts) => {
|
|
client.list_functions(opts.limit.or(default_limit), None)
|
|
}
|
|
DumpCommands::Strings(opts) => client.list_strings(opts.limit.or(default_limit)),
|
|
}
|
|
}
|
|
Commands::Summary(_) => client.program_info(),
|
|
Commands::XRef(cmd) => {
|
|
use cli::XRefCommands;
|
|
match cmd {
|
|
XRefCommands::To(args) => client.xrefs_to(args.address.clone()),
|
|
XRefCommands::From(args) => client.xrefs_from(args.address.clone()),
|
|
XRefCommands::List(_) => client.send_command("xrefs_list", None),
|
|
}
|
|
}
|
|
Commands::Program(cmd) => {
|
|
use cli::ProgramCommands;
|
|
match cmd {
|
|
ProgramCommands::List(_) => client.list_programs(),
|
|
ProgramCommands::Open(args) => {
|
|
let program = args.program.as_ref().ok_or_else(|| {
|
|
anyhow::anyhow!("Program name required. Use --program <name>")
|
|
})?;
|
|
client.open_program(program)
|
|
}
|
|
ProgramCommands::Close(_) => client.program_close(),
|
|
ProgramCommands::Delete(args) => {
|
|
let program = args
|
|
.program
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow::anyhow!("Program name required"))?;
|
|
client.program_delete(program)
|
|
}
|
|
ProgramCommands::Info(_) => client.program_info(),
|
|
ProgramCommands::Export(args) => {
|
|
client.program_export(&args.format, args.output.as_deref())
|
|
}
|
|
}
|
|
}
|
|
Commands::Symbol(cmd) => {
|
|
use cli::SymbolCommands;
|
|
match cmd {
|
|
SymbolCommands::List(_) => client.symbol_list(None),
|
|
SymbolCommands::Get(args) => client.symbol_get(&args.name),
|
|
SymbolCommands::Create(args) => client.symbol_create(&args.address, &args.name),
|
|
SymbolCommands::Delete(args) => client.symbol_delete(&args.name),
|
|
SymbolCommands::Rename(args) => {
|
|
client.symbol_rename(&args.old_name, &args.new_name)
|
|
}
|
|
}
|
|
}
|
|
Commands::Type(cmd) => {
|
|
use cli::TypeCommands;
|
|
match cmd {
|
|
TypeCommands::List(opts) => client.type_list(opts.limit.or(default_limit)),
|
|
TypeCommands::Get(args) => client.type_get(&args.name),
|
|
TypeCommands::Create(args) => client.type_create(&args.definition),
|
|
TypeCommands::Apply(args) => client.type_apply(&args.address, &args.type_name),
|
|
}
|
|
}
|
|
Commands::Comment(cmd) => {
|
|
use cli::CommentCommands;
|
|
match cmd {
|
|
CommentCommands::List(_) => client.comment_list(),
|
|
CommentCommands::Get(args) => client.comment_get(&args.address),
|
|
CommentCommands::Set(args) => {
|
|
client.comment_set(&args.address, &args.text, args.comment_type.as_deref())
|
|
}
|
|
CommentCommands::Delete(args) => client.comment_delete(&args.address),
|
|
}
|
|
}
|
|
Commands::Graph(cmd) => {
|
|
use cli::GraphCommands;
|
|
match cmd {
|
|
GraphCommands::Calls(opts) => client.graph_calls(opts.limit.or(default_limit)),
|
|
GraphCommands::Callers(args) => client.graph_callers(&args.function, args.depth),
|
|
GraphCommands::Callees(args) => client.graph_callees(&args.function, args.depth),
|
|
GraphCommands::Export(args) => client.graph_export(&args.format),
|
|
}
|
|
}
|
|
Commands::Find(cmd) => {
|
|
use cli::FindCommands;
|
|
match cmd {
|
|
FindCommands::String(args) => client.find_string(&args.pattern),
|
|
FindCommands::Bytes(args) => client.find_bytes(&args.hex),
|
|
FindCommands::Function(args) => client.find_function(&args.pattern),
|
|
FindCommands::Calls(args) => client.find_calls(&args.function),
|
|
FindCommands::Crypto(_) => client.find_crypto(),
|
|
FindCommands::Interesting(_) => client.find_interesting(),
|
|
}
|
|
}
|
|
Commands::Diff(cmd) => {
|
|
use cli::DiffCommands;
|
|
match cmd {
|
|
DiffCommands::Programs(args) => {
|
|
client.diff_programs(&args.program1, &args.program2)
|
|
}
|
|
DiffCommands::Functions(args) => client.diff_functions(&args.func1, &args.func2),
|
|
}
|
|
}
|
|
Commands::Patch(cmd) => {
|
|
use cli::PatchCommands;
|
|
match cmd {
|
|
PatchCommands::Bytes(args) => client.patch_bytes(&args.address, &args.hex),
|
|
PatchCommands::Nop(args) => client.patch_nop(&args.address),
|
|
PatchCommands::Export(args) => client.patch_export(&args.output),
|
|
}
|
|
}
|
|
Commands::Script(cmd) => {
|
|
use cli::ScriptCommands;
|
|
match cmd {
|
|
ScriptCommands::Run(args) => client.script_run(&args.script_path, &args.args),
|
|
ScriptCommands::Python(args) => client.script_python(&args.code),
|
|
ScriptCommands::Java(args) => client.script_java(&args.code),
|
|
ScriptCommands::List => client.script_list(),
|
|
}
|
|
}
|
|
Commands::Disasm(args) => client.disasm(&args.address, args.num_instructions),
|
|
Commands::Batch(args) => {
|
|
// Read batch file and execute each command locally
|
|
let content = std::fs::read_to_string(&args.script_file)
|
|
.map_err(|e| anyhow::anyhow!("Failed to read batch file: {}", e))?;
|
|
let lines: Vec<&str> = content
|
|
.lines()
|
|
.filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#'))
|
|
.collect();
|
|
|
|
let mut results = Vec::new();
|
|
for line in &lines {
|
|
let words: Vec<&str> = std::iter::once("ghidra")
|
|
.chain(line.split_whitespace())
|
|
.collect();
|
|
let sub_result = match Cli::try_parse_from(&words) {
|
|
Ok(sub_cli) => {
|
|
execute_via_bridge(client, &sub_cli.command, true, default_limit)
|
|
}
|
|
Err(e) => Err(anyhow::anyhow!("{}", e)),
|
|
};
|
|
match sub_result {
|
|
Ok(val) => results.push(json!({"command": line.trim(), "result": val})),
|
|
Err(e) => results.push(json!({"command": line.trim(), "error": e.to_string()})),
|
|
}
|
|
}
|
|
|
|
Ok(json!({
|
|
"commands_parsed": lines.len(),
|
|
"results": results
|
|
}))
|
|
}
|
|
Commands::Stats(_) => client.stats(),
|
|
_ => anyhow::bail!("Command not supported"),
|
|
}
|
|
}
|
|
|
|
/// Dispatch bridge management commands (top-level start/stop/restart/status/ping).
|
|
fn handle_bridge_command(cli: Cli) -> anyhow::Result<()> {
|
|
match cli.command {
|
|
Commands::Start { project, program } => handle_bridge_start(project, program),
|
|
Commands::Stop { project } => handle_bridge_stop(project),
|
|
Commands::Restart { project, program } => {
|
|
handle_bridge_stop(project.clone())?;
|
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
|
handle_bridge_start(project, program)
|
|
}
|
|
Commands::Status { project } => handle_bridge_status(project),
|
|
Commands::Ping { project } => handle_bridge_ping(project),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Start the bridge for a project.
|
|
fn handle_bridge_start(project: Option<String>, program: Option<String>) -> anyhow::Result<()> {
|
|
let config = Config::load()?;
|
|
let project_path = resolve_project_path(&project, &config)?;
|
|
|
|
let ghidra_install_dir = config
|
|
.ghidra_install_dir
|
|
.clone()
|
|
.or_else(|| config.get_ghidra_install_dir().ok())
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"Ghidra installation directory not configured. Run 'ghidra setup' first."
|
|
)
|
|
})?;
|
|
|
|
// Check if bridge is already running
|
|
if bridge::is_bridge_running(&project_path).is_some() {
|
|
println!(
|
|
"Bridge is already running for project: {}",
|
|
project_path.display()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
// Determine start mode
|
|
let mode = if let Some(prog) = program {
|
|
BridgeStartMode::Process { program_name: prog }
|
|
} else if let Some(prog) = config.get_default_program() {
|
|
BridgeStartMode::Process { program_name: prog }
|
|
} else {
|
|
BridgeStartMode::Project
|
|
};
|
|
|
|
println!("Starting bridge for project: {}", project_path.display());
|
|
|
|
let port = bridge::ensure_bridge_running(&project_path, &ghidra_install_dir, mode)?;
|
|
|
|
println!("Bridge started on port {}", port);
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop the bridge for a project.
|
|
fn handle_bridge_stop(project: Option<String>) -> anyhow::Result<()> {
|
|
let config = Config::load()?;
|
|
let project_path = resolve_project_path(&project, &config)?;
|
|
|
|
if bridge::is_bridge_running(&project_path).is_some() {
|
|
println!("Stopping bridge...");
|
|
bridge::stop_bridge(&project_path)?;
|
|
println!("Bridge stopped");
|
|
} else {
|
|
println!("No bridge running for project: {}", project_path.display());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get bridge status for a project.
|
|
fn handle_bridge_status(project: Option<String>) -> anyhow::Result<()> {
|
|
let config = Config::load()?;
|
|
let project_path = resolve_project_path(&project, &config)?;
|
|
|
|
match bridge::bridge_status(&project_path)? {
|
|
BridgeStatus::Running { port, pid } => {
|
|
println!("Bridge is running:");
|
|
println!(" PID: {}", pid);
|
|
println!(" Port: {}", port);
|
|
println!(" Project: {}", project_path.display());
|
|
|
|
// Try to get extended info from the bridge
|
|
let client = BridgeClient::new(port);
|
|
if let Ok(info) = client.bridge_info() {
|
|
if let Some(prog) = info.get("current_program").and_then(|v| v.as_str()) {
|
|
println!(" Current program: {}", prog);
|
|
}
|
|
if let Some(count) = info.get("program_count").and_then(|v| v.as_u64()) {
|
|
println!(" Programs: {}", count);
|
|
}
|
|
}
|
|
}
|
|
BridgeStatus::Stopped => {
|
|
println!("No bridge running for project: {}", project_path.display());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Ping the bridge.
|
|
fn handle_bridge_ping(project: Option<String>) -> anyhow::Result<()> {
|
|
let config = Config::load()?;
|
|
let project_path = resolve_project_path(&project, &config)?;
|
|
|
|
if let Some(port) = bridge::is_bridge_running(&project_path) {
|
|
let client = BridgeClient::new(port);
|
|
if client.ping()? {
|
|
println!("Bridge is responsive");
|
|
} else {
|
|
println!("Bridge is not responding");
|
|
}
|
|
} else {
|
|
println!("No bridge running for project: {}", project_path.display());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle the setup command - download and install Ghidra.
|
|
async fn run_setup(cli: Cli) -> anyhow::Result<()> {
|
|
let args = match cli.command {
|
|
Commands::Setup(args) => args,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
println!("Ghidra Setup Wizard");
|
|
println!("===================\n");
|
|
|
|
// 1. Check Java
|
|
if !args.force {
|
|
if let Err(e) = ghidra::setup::check_java_requirement() {
|
|
eprintln!("Java prerequisite check failed: {}", e);
|
|
eprintln!("Ghidra requires JDK 17+. Use --force to continue anyway.");
|
|
std::process::exit(1);
|
|
}
|
|
} else {
|
|
println!("Skipping Java check (--force specified)");
|
|
}
|
|
|
|
// 2. Determine Install Directory
|
|
let install_base = if let Some(d) = args.dir {
|
|
PathBuf::from(d)
|
|
} else {
|
|
dirs::data_local_dir()
|
|
.ok_or(anyhow::anyhow!("Could not determine data directory"))?
|
|
.join("ghidra-cli")
|
|
.join("ghidra")
|
|
};
|
|
|
|
std::fs::create_dir_all(&install_base)?;
|
|
|
|
// 3. Install Ghidra
|
|
println!("\nInstalling to: {}", install_base.display());
|
|
let final_path = ghidra::setup::install_ghidra(args.version, install_base).await?;
|
|
|
|
// 4. Update Config
|
|
let mut config = Config::load()?;
|
|
config.ghidra_install_dir = Some(final_path.clone());
|
|
config.save()?;
|
|
|
|
println!("\nSuccess! Ghidra installed at: {}", final_path.display());
|
|
println!("Configuration updated.");
|
|
|
|
// 5. Verify
|
|
println!("\nVerifying installation...");
|
|
let client = GhidraClient::new(config)?;
|
|
if client.verify_installation().is_ok() {
|
|
println!("Verification passed!");
|
|
println!("\nYou can now run: ghidra import <binary> --project <name>");
|
|
} else {
|
|
println!("Verification failed - analyzeHeadless not found");
|
|
println!(" The installation may be incomplete.");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_init() -> anyhow::Result<()> {
|
|
println!("Ghidra CLI Initialization");
|
|
println!("========================\n");
|
|
|
|
let mut config = Config::default();
|
|
|
|
if config.ghidra_install_dir.is_none() {
|
|
println!("Ghidra installation not found automatically.");
|
|
println!("Please set GHIDRA_INSTALL_DIR environment variable or run 'ghidra setup'.");
|
|
}
|
|
|
|
// Set default project directory
|
|
let home = dirs::home_dir().ok_or_else(|| {
|
|
GhidraError::ConfigError("Could not determine home directory".to_string())
|
|
})?;
|
|
let project_dir = home.join(".ghidra-projects");
|
|
config.ghidra_project_dir = Some(project_dir.clone());
|
|
|
|
println!("\nProject directory: {}", project_dir.display());
|
|
|
|
// Save config
|
|
config.save()?;
|
|
|
|
println!(
|
|
"\nConfiguration saved to: {}",
|
|
Config::config_path()?.display()
|
|
);
|
|
println!("\nRun 'ghidra doctor' to verify your installation.");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_doctor() -> anyhow::Result<()> {
|
|
println!("Ghidra CLI Doctor");
|
|
println!("=================\n");
|
|
|
|
let config = Config::load()?;
|
|
|
|
// Check Ghidra installation
|
|
print!("Checking Ghidra installation... ");
|
|
match config.get_ghidra_install_dir() {
|
|
Ok(dir) => {
|
|
println!("OK");
|
|
println!(" Location: {}", dir.display());
|
|
|
|
let client = GhidraClient::new(config.clone());
|
|
match client {
|
|
Ok(c) => {
|
|
if c.verify_installation().is_ok() {
|
|
println!(" analyzeHeadless: OK");
|
|
} else {
|
|
println!(" analyzeHeadless: NOT FOUND");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!(" Error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!("FAILED");
|
|
println!(" Error: {}", e);
|
|
}
|
|
}
|
|
|
|
// Check Java
|
|
print!("\nChecking Java... ");
|
|
match ghidra::setup::check_java_requirement() {
|
|
Ok(()) => println!("OK (JDK 17+)"),
|
|
Err(e) => {
|
|
println!("FAILED");
|
|
println!(" Error: {}", e);
|
|
}
|
|
}
|
|
|
|
// Check project directory
|
|
print!("\nChecking project directory... ");
|
|
match config.get_project_dir() {
|
|
Ok(dir) => {
|
|
println!("OK");
|
|
println!(" Location: {}", dir.display());
|
|
println!(
|
|
" Exists: {}",
|
|
if dir.exists() {
|
|
"yes"
|
|
} else {
|
|
"no (will be created)"
|
|
}
|
|
);
|
|
}
|
|
Err(e) => {
|
|
println!("FAILED");
|
|
println!(" Error: {}", e);
|
|
}
|
|
}
|
|
|
|
// Check config file
|
|
print!("\nConfig file... ");
|
|
match Config::config_path() {
|
|
Ok(path) => {
|
|
println!("OK");
|
|
println!(" Location: {}", path.display());
|
|
println!(" Exists: {}", if path.exists() { "yes" } else { "no" });
|
|
}
|
|
Err(e) => {
|
|
println!("FAILED");
|
|
println!(" Error: {}", e);
|
|
}
|
|
}
|
|
|
|
println!("\nDone!");
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_version() -> anyhow::Result<()> {
|
|
println!("ghidra-cli {}", env!("CARGO_PKG_VERSION"));
|
|
println!("Rust CLI for Ghidra reverse engineering");
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_config_command(cmd: cli::ConfigCommands) -> anyhow::Result<()> {
|
|
use cli::ConfigCommands;
|
|
|
|
match cmd {
|
|
ConfigCommands::List => {
|
|
let config = Config::load()?;
|
|
println!("{}", serde_yaml::to_string(&config)?);
|
|
}
|
|
ConfigCommands::Get { key } => {
|
|
let config = Config::load()?;
|
|
let yaml = serde_yaml::to_value(&config)?;
|
|
if let Some(value) = yaml.get(&key) {
|
|
println!("{}", serde_yaml::to_string(value)?);
|
|
} else {
|
|
println!("Key not found: {}", key);
|
|
}
|
|
}
|
|
ConfigCommands::Set { key, value } => {
|
|
let mut config = Config::load()?;
|
|
match key.as_str() {
|
|
"default_output_format" => config.default_output_format = Some(value),
|
|
"timeout" => {
|
|
let timeout: u64 = value.parse().map_err(|_| {
|
|
GhidraError::ConfigError("Invalid timeout value".to_string())
|
|
})?;
|
|
config.timeout = Some(timeout);
|
|
}
|
|
"ghidra_install_dir" => config.ghidra_install_dir = Some(PathBuf::from(value)),
|
|
"ghidra_project_dir" => config.ghidra_project_dir = Some(PathBuf::from(value)),
|
|
"default_program" => config.default_program = Some(value),
|
|
"default_project" => config.default_project = Some(value),
|
|
"default_limit" => {
|
|
let limit: usize = value
|
|
.parse()
|
|
.map_err(|_| GhidraError::ConfigError("Invalid limit value".to_string()))?;
|
|
config.default_limit = Some(limit);
|
|
}
|
|
_ => {
|
|
anyhow::bail!("Unknown config key: {}", key);
|
|
}
|
|
}
|
|
config.save()?;
|
|
println!("Configuration updated");
|
|
}
|
|
ConfigCommands::Reset => {
|
|
let config = Config::default();
|
|
config.save()?;
|
|
println!("Configuration reset to defaults");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_set_default(args: cli::SetDefaultArgs) -> anyhow::Result<()> {
|
|
let mut config = Config::load()?;
|
|
|
|
match args.kind.as_str() {
|
|
"program" => {
|
|
config.default_program = Some(args.value.clone());
|
|
config.save()?;
|
|
println!("Default program set to: {}", args.value);
|
|
}
|
|
"project" => {
|
|
config.default_project = Some(args.value.clone());
|
|
config.save()?;
|
|
println!("Default project set to: {}", args.value);
|
|
}
|
|
_ => {
|
|
anyhow::bail!(format!("Unknown default kind: {}", args.kind));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_project_command(cmd: cli::ProjectCommands) -> anyhow::Result<()> {
|
|
use cli::ProjectCommands;
|
|
|
|
let config = Config::load()?;
|
|
let client = GhidraClient::new(config)?;
|
|
|
|
match cmd {
|
|
ProjectCommands::Create { name } => {
|
|
client.create_project(&name)?;
|
|
println!("Project '{}' created", name);
|
|
}
|
|
ProjectCommands::List => {
|
|
let project_dir = client.get_project_dir();
|
|
if !project_dir.exists() {
|
|
println!("No projects found");
|
|
return Ok(());
|
|
}
|
|
|
|
println!("Projects:");
|
|
for entry in std::fs::read_dir(project_dir)? {
|
|
let entry = entry?;
|
|
if entry.path().is_dir() {
|
|
if let Some(name) = entry.file_name().to_str() {
|
|
println!(" {}", name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ProjectCommands::Delete { name } => {
|
|
let project_path = client.get_project_path(&name);
|
|
if project_path.exists() {
|
|
std::fs::remove_dir_all(&project_path)?;
|
|
println!("Project '{}' deleted", name);
|
|
} else {
|
|
println!("Project '{}' not found", name);
|
|
}
|
|
}
|
|
ProjectCommands::Info { name } => {
|
|
let project_name = name.unwrap_or_else(|| "default".to_string());
|
|
let project_path = client.get_project_path(&project_name);
|
|
println!("Project: {}", project_name);
|
|
println!("Path: {}", project_path.display());
|
|
println!("Exists: {}", project_path.exists());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if a decompile result looks like .NET managed code and warn the user.
|
|
fn check_dotnet_decompile_warning(command: &Commands, result: &serde_json::Value) {
|
|
let is_decompile = matches!(
|
|
command,
|
|
Commands::Decompile(_) | Commands::Function(cli::FunctionCommands::Decompile(_))
|
|
);
|
|
if !is_decompile {
|
|
return;
|
|
}
|
|
|
|
if let Some(code) = result.get("code").and_then(|c| c.as_str()) {
|
|
if code.contains("halt_baddata()") || code.contains(".NET CLR Managed Code") {
|
|
eprintln!(
|
|
"Warning: This appears to be .NET managed code. Ghidra cannot decompile .NET IL bytecode.\n\
|
|
Consider using a .NET decompiler (e.g., ilspy-cli) for better results."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Unwrap bridge response envelopes into a flat array of objects.
|
|
///
|
|
/// Bridge returns envelopes like `{"count": N, "functions": [...]}`.
|
|
/// This extracts the inner array so formatters can render individual items.
|
|
fn unwrap_bridge_response(value: serde_json::Value) -> Vec<serde_json::Value> {
|
|
// Already an array - return as-is
|
|
if let serde_json::Value::Array(arr) = &value {
|
|
return arr.clone();
|
|
}
|
|
|
|
// Must be an object to unwrap
|
|
let obj = match value {
|
|
serde_json::Value::Object(ref map) => map,
|
|
other => return vec![other],
|
|
};
|
|
|
|
// Known array keys from bridge responses
|
|
const ARRAY_KEYS: &[&str] = &[
|
|
"functions",
|
|
"strings",
|
|
"imports",
|
|
"exports",
|
|
"blocks",
|
|
"xrefs",
|
|
"results",
|
|
"programs",
|
|
"types",
|
|
"comments",
|
|
"symbols",
|
|
"callers",
|
|
"callees",
|
|
"calls",
|
|
"instructions",
|
|
"sections",
|
|
"references",
|
|
];
|
|
|
|
// Metadata keys that accompany array keys (not data themselves)
|
|
const META_KEYS: &[&str] = &[
|
|
"count",
|
|
"target",
|
|
"function",
|
|
"command",
|
|
"status",
|
|
"current_program_name",
|
|
"has_current_program",
|
|
"data",
|
|
];
|
|
|
|
// Special case: decompile responses have a "code" key - return as-is for special rendering
|
|
if obj.contains_key("code") {
|
|
return vec![value];
|
|
}
|
|
|
|
// Look for a known array key
|
|
for &key in ARRAY_KEYS {
|
|
if let Some(serde_json::Value::Array(arr)) = obj.get(key) {
|
|
// Verify remaining keys are metadata
|
|
let all_meta = obj
|
|
.keys()
|
|
.all(|k| k == key || META_KEYS.contains(&k.as_str()));
|
|
if all_meta {
|
|
return arr.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
// No known array key found - return as single-item vec
|
|
vec![value]
|
|
}
|
|
|
|
/// Verify that a bridge is actually responding to commands.
|
|
fn verify_bridge(client: &BridgeClient) -> anyhow::Result<()> {
|
|
if !client.ping()? {
|
|
anyhow::bail!("Bridge not responding to ping");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Resolve a project name to its full path on disk.
|
|
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))
|
|
}
|
|
}
|