This commit is contained in:
Alexander Kiselev
2026-02-04 21:38:09 -08:00
parent 3293e1a545
commit 2447137ffc
8 changed files with 519 additions and 51 deletions
Generated
+1 -1
View File
@@ -750,7 +750,7 @@ dependencies = [
[[package]]
name = "ghidra-cli"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"anyhow",
"assert_cmd",
+32 -11
View File
@@ -34,19 +34,19 @@ pub enum Commands {
Project(ProjectArgs),
/// Program/binary management commands
#[command(subcommand)]
#[command(subcommand, alias = "prog", alias = "programs")]
Program(ProgramCommands),
/// Function operations
#[command(subcommand, alias = "fn")]
#[command(subcommand, alias = "fn", alias = "func", alias = "functions")]
Function(FunctionCommands),
/// String operations
#[command(subcommand)]
#[command(subcommand, alias = "string", alias = "str")]
Strings(StringsCommands),
/// Symbol operations
#[command(subcommand, alias = "sym")]
#[command(subcommand, alias = "sym", alias = "symbols")]
Symbol(SymbolCommands),
/// Memory operations
@@ -54,29 +54,31 @@ pub enum Commands {
Memory(MemoryCommands),
/// Cross-reference operations
#[command(subcommand)]
#[command(subcommand, alias = "xrefs", alias = "xref", alias = "crossref", alias = "crossrefs")]
XRef(XRefCommands),
/// Type operations
#[command(subcommand)]
#[command(subcommand, alias = "types")]
Type(TypeCommands),
/// Comment operations
#[command(subcommand)]
#[command(subcommand, alias = "comments")]
Comment(CommentCommands),
/// Search operations
#[command(subcommand)]
#[command(subcommand, alias = "search")]
Find(FindCommands),
/// Graph operations
#[command(subcommand)]
#[command(subcommand, alias = "callgraph", alias = "cg")]
Graph(GraphCommands),
/// Decompile function
#[command(alias = "decomp", alias = "dec")]
Decompile(DecompileArgs),
/// Disassemble code
#[command(alias = "disassemble", alias = "dis")]
Disasm(DisasmArgs),
/// Diff operations
@@ -84,7 +86,7 @@ pub enum Commands {
Diff(DiffCommands),
/// Dump/export data
#[command(subcommand)]
#[command(subcommand, alias = "export")]
Dump(DumpCommands),
/// Patch binary
@@ -92,7 +94,7 @@ pub enum Commands {
Patch(PatchCommands),
/// Script execution
#[command(subcommand)]
#[command(subcommand, alias = "scripts")]
Script(ScriptCommands),
/// Batch operations
@@ -106,6 +108,7 @@ pub enum Commands {
SetDefault(SetDefaultArgs),
/// Program summary
#[command(alias = "info")]
Summary(SummaryArgs),
/// Program statistics
@@ -124,6 +127,7 @@ pub enum Commands {
Import(ImportArgs),
/// Analyze a program
#[command(alias = "analysis")]
Analyze(AnalyzeArgs),
/// Start the bridge
@@ -238,6 +242,7 @@ pub enum ProjectCommands {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum ProgramCommands {
/// List all programs in the project
#[command(alias = "ls")]
List(ProgramTargetArgs),
/// Open/switch to a program
Open(ProgramTargetArgs),
@@ -275,16 +280,21 @@ pub struct ExportArgs {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum FunctionCommands {
/// List all functions
#[command(alias = "ls")]
List(QueryOptions),
/// Get function details
#[command(alias = "show", alias = "detail")]
Get(FunctionGetArgs),
/// Decompile function
#[command(alias = "decomp")]
Decompile(FunctionGetArgs),
/// Disassemble function
#[command(alias = "disassemble", alias = "dis")]
Disasm(FunctionGetArgs),
/// Get function calls
Calls(FunctionGetArgs),
/// Get cross-references to function
#[command(alias = "xrefs", alias = "crossrefs", alias = "references")]
XRefs(FunctionGetArgs),
/// Rename function
Rename(RenameArgs),
@@ -325,8 +335,10 @@ pub struct CreateFunctionArgs {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum StringsCommands {
/// List all strings
#[command(alias = "ls")]
List(QueryOptions),
/// Get references to a string
#[command(alias = "references", alias = "xrefs")]
Refs(StringRefsArgs),
}
@@ -340,6 +352,7 @@ pub struct StringRefsArgs {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum SymbolCommands {
/// List all symbols
#[command(alias = "ls")]
List(QueryOptions),
/// Get symbol details
Get(SymbolGetArgs),
@@ -425,6 +438,7 @@ pub struct XRefArgs {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum TypeCommands {
/// List data types
#[command(alias = "ls")]
List(QueryOptions),
/// Get type definition
Get(TypeGetArgs),
@@ -463,6 +477,7 @@ pub struct ApplyTypeArgs {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum CommentCommands {
/// List all comments
#[command(alias = "ls")]
List(QueryOptions),
/// Get comment at address
Get(CommentGetArgs),
@@ -494,16 +509,20 @@ pub struct CommentSetArgs {
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
pub enum FindCommands {
/// Find strings
#[command(alias = "str", alias = "strings")]
String(FindStringArgs),
/// Find byte patterns
Bytes(FindBytesArgs),
/// Find functions
#[command(alias = "func", alias = "fn", alias = "functions")]
Function(FindFunctionArgs),
/// Find calls to function
Calls(FindCallsArgs),
/// Find crypto constants
#[command(alias = "encryption")]
Crypto(QueryOptions),
/// Find interesting functions
#[command(alias = "suspicious", alias = "notable")]
Interesting(QueryOptions),
}
@@ -540,8 +559,10 @@ pub enum GraphCommands {
/// Call graph
Calls(QueryOptions),
/// Get callers of function
#[command(alias = "called-by", alias = "incoming")]
Callers(GraphFunctionArgs),
/// Get callees of function
#[command(alias = "calls-to", alias = "outgoing")]
Callees(GraphFunctionArgs),
/// Export graph
Export(GraphExportArgs),
+1 -2
View File
@@ -1,5 +1,3 @@
#![allow(dead_code)]
pub mod evaluator;
pub mod parser;
@@ -73,6 +71,7 @@ pub enum Value {
Hex(u64),
}
#[allow(dead_code)]
impl Value {
pub fn as_str(&self) -> Option<&str> {
match self {
+190 -3
View File
@@ -1,11 +1,10 @@
#![allow(dead_code)]
use crate::error::{GhidraError, Result};
use comfy_table::{presets::UTF8_FULL, Table};
use serde::Serialize;
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum OutputFormat {
Full,
Compact,
@@ -24,6 +23,7 @@ pub enum OutputFormat {
C,
}
#[allow(dead_code)]
impl OutputFormat {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
@@ -82,7 +82,8 @@ impl Formatter for DefaultFormatter {
OutputFormat::Table => format_table(data),
OutputFormat::Csv => format_csv(data, ','),
OutputFormat::Tsv => format_csv(data, '\t'),
OutputFormat::Compact => format_minimal(data),
OutputFormat::Compact => format_compact(data),
OutputFormat::Full => format_full(data),
OutputFormat::Minimal | OutputFormat::Ids => format_minimal(data),
_ => {
// For other formats, default to JSON
@@ -183,6 +184,192 @@ fn format_csv<T: Serialize>(data: &[T], delimiter: char) -> Result<String> {
Ok(result)
}
/// Compact human-readable format: one line per item with key fields.
fn format_compact<T: Serialize>(data: &[T]) -> Result<String> {
let json_data: Vec<JsonValue> = data
.iter()
.map(serde_json::to_value)
.collect::<std::result::Result<Vec<_>, _>>()?;
if json_data.is_empty() {
return Ok("No results".to_string());
}
let mut result = String::new();
for item in &json_data {
match item {
JsonValue::Object(map) => {
// Special case: decompile response with "code" key
if let Some(code) = map.get("code").and_then(|v| v.as_str()) {
if let Some(sig) = map.get("signature").and_then(|v| v.as_str()) {
result.push_str(sig);
result.push('\n');
}
result.push_str(code);
if !code.ends_with('\n') {
result.push('\n');
}
continue;
}
// Special case: disasm instruction with mnemonic
if let (Some(addr), Some(mnem)) = (
map.get("address").and_then(|v| v.as_str()),
map.get("mnemonic").and_then(|v| v.as_str()),
) {
let bytes = map
.get("bytes")
.and_then(|v| v.as_str())
.unwrap_or("");
let operands = match map.get("operands") {
Some(JsonValue::Array(ops)) => ops
.iter()
.map(|o| format_json_value(o))
.collect::<Vec<_>>()
.join(", "),
_ => String::new(),
};
result.push_str(&format!(
"{:<12} {:<16} {} {}\n",
addr, bytes, mnem, operands
));
continue;
}
// General object: render primary fields in a compact line
let address = map.get("address").and_then(|v| v.as_str());
let name = map.get("name").and_then(|v| v.as_str());
let size = map.get("size").and_then(|v| v.as_u64());
let value_str = map.get("value").and_then(|v| v.as_str());
// Build compact line from available fields
let mut parts: Vec<String> = Vec::new();
if let Some(addr) = address {
parts.push(addr.to_string());
}
if let Some(n) = name {
parts.push(n.to_string());
}
if let Some(s) = size {
parts.push(format!("({})", s));
}
if let Some(v) = value_str {
// Truncate long strings
if v.len() > 80 {
parts.push(format!("\"{}...\"", &v[..77]));
} else {
parts.push(format!("\"{}\"", v));
}
}
// If we only have unknown fields, render as key=value pairs
if parts.is_empty() {
let kv: Vec<String> = map
.iter()
.map(|(k, v)| format!("{}={}", k, format_json_value(v)))
.collect();
result.push_str(&kv.join(" "));
} else {
result.push_str(&parts.join(" "));
}
// Add extra context from secondary fields
let secondary: Vec<String> = map
.iter()
.filter(|(k, _)| {
!matches!(
k.as_str(),
"address" | "name" | "size" | "value" | "mnemonic" | "bytes" | "operands" | "code" | "signature"
)
})
.filter_map(|(k, v)| {
let s = format_json_value(v);
if s.is_empty() || s == "null" || s == "\"\"" {
None
} else {
Some(format!("{}={}", k, s))
}
})
.collect();
if !secondary.is_empty() {
result.push_str(" ");
result.push_str(&secondary.join(" "));
}
result.push('\n');
}
_ => {
result.push_str(&format_json_value(item));
result.push('\n');
}
}
}
Ok(result)
}
/// Full human-readable format: multi-line labeled blocks per item.
fn format_full<T: Serialize>(data: &[T]) -> Result<String> {
let json_data: Vec<JsonValue> = data
.iter()
.map(serde_json::to_value)
.collect::<std::result::Result<Vec<_>, _>>()?;
if json_data.is_empty() {
return Ok("No results".to_string());
}
let mut result = String::new();
for (i, item) in json_data.iter().enumerate() {
if i > 0 {
result.push_str("---\n");
}
match item {
JsonValue::Object(map) => {
// Special case: decompile response
if let Some(code) = map.get("code").and_then(|v| v.as_str()) {
if let Some(sig) = map.get("signature").and_then(|v| v.as_str()) {
result.push_str(&format!("Signature: {}\n", sig));
}
if let Some(name) = map.get("name").and_then(|v| v.as_str()) {
result.push_str(&format!("Function: {}\n", name));
}
result.push('\n');
result.push_str(code);
if !code.ends_with('\n') {
result.push('\n');
}
continue;
}
// Calculate max key width for alignment
let max_key = map.keys().map(|k| k.len()).max().unwrap_or(0);
for (key, val) in map {
let formatted = format_json_value(val);
result.push_str(&format!(
"{:width$} {}\n",
format!("{}:", key),
formatted,
width = max_key + 1
));
}
}
_ => {
result.push_str(&format_json_value(item));
result.push('\n');
}
}
}
Ok(result)
}
fn format_minimal<T: Serialize>(data: &[T]) -> Result<String> {
let json_data: Vec<JsonValue> = data
.iter()
+54 -17
View File
@@ -210,7 +210,7 @@ public class GhidraCliBridge extends GhidraScript {
case "symbol_delete": return handleSymbolDelete(args);
case "symbol_rename": return handleSymbolRename(args);
// Type commands
case "type_list": return handleTypeList();
case "type_list": return handleTypeList(args);
case "type_get": return handleTypeGet(args);
case "type_create": return handleTypeCreate(args);
case "type_apply": return handleTypeApply(args);
@@ -276,13 +276,40 @@ public class GhidraCliBridge extends GhidraScript {
return null;
}
// Try as hex address first
// Try as hex address first (with and without 0x prefix)
Address addr = currentProgram.getAddressFactory().getAddress(addrStr);
if (addr != null) {
return addr;
}
if (addrStr.startsWith("0x") || addrStr.startsWith("0X")) {
addr = currentProgram.getAddressFactory().getAddress(addrStr.substring(2));
if (addr != null) {
return addr;
}
}
// Try as function name
// Try as symbol/function name via SymbolTable
SymbolTable st = currentProgram.getSymbolTable();
SymbolIterator syms = st.getSymbols(addrStr);
while (syms.hasNext()) {
Symbol sym = syms.next();
Address symAddr = sym.getAddress();
// Skip external/fake addresses - prefer real addresses
if (symAddr != null && !symAddr.isExternalAddress()) {
return symAddr;
}
}
// Try global symbols (may include exports)
List<Symbol> globalSyms = st.getGlobalSymbols(addrStr);
for (Symbol sym : globalSyms) {
Address symAddr = sym.getAddress();
if (symAddr != null && !symAddr.isExternalAddress()) {
return symAddr;
}
}
// Fallback: scan functions by name (O(n) but handles edge cases)
FunctionManager fm = currentProgram.getFunctionManager();
FunctionIterator iter = fm.getFunctions(true);
while (iter.hasNext()) {
@@ -1562,21 +1589,25 @@ public class GhidraCliBridge extends GhidraScript {
// --- Type Handlers ---
private JsonObject handleTypeList() {
private JsonObject handleTypeList(JsonObject args) {
if (currentProgram == null) return errorResult("No program loaded");
int limit = getArgInt(args, "limit", 0);
DataTypeManager dtm = currentProgram.getDataTypeManager();
JsonArray types = new JsonArray();
Iterator<DataType> dtIter = dtm.getAllDataTypes();
int count = 0;
while (dtIter.hasNext()) {
DataType dt = dtIter.next();
if (limit > 0 && count >= limit) break;
JsonObject typeData = new JsonObject();
typeData.addProperty("name", dt.getName());
typeData.addProperty("path", dt.getPathName());
typeData.addProperty("category", dt.getCategoryPath().toString());
typeData.addProperty("size", dt.getLength());
types.add(typeData);
count++;
}
JsonObject result = new JsonObject();
@@ -2371,23 +2402,29 @@ public class GhidraCliBridge extends GhidraScript {
}
try {
// Strip 0x prefix if present
String cleanAddr = addressStr;
if (cleanAddr.startsWith("0x") || cleanAddr.startsWith("0X")) {
cleanAddr = cleanAddr.substring(2);
}
Address addr = currentProgram.getAddressFactory().getAddress(cleanAddr);
if (addr == null) {
// Try with the original string (might be a function name)
addr = resolveAddress(addressStr);
}
// Use resolveAddress which handles 0x prefix and symbol lookup
Address addr = resolveAddress(addressStr);
if (addr == null) return errorResult("Invalid address: " + addressStr);
Listing listing = currentProgram.getListing();
Instruction instruction = listing.getInstructionAt(addr);
// If no instruction at exact address, try containing instruction (mid-instruction)
if (instruction == null) {
return errorResult("No instruction at address: " + addressStr);
instruction = listing.getInstructionContaining(addr);
}
// If still null, try starting from containing function's entry point
if (instruction == null) {
Function func = currentProgram.getFunctionManager().getFunctionContaining(addr);
if (func != null) {
instruction = listing.getInstructionAt(func.getEntryPoint());
}
}
if (instruction == null) {
return errorResult("No instruction at address " + addressStr +
". Address may be data or unanalyzed code.");
}
JsonArray results = new JsonArray();
@@ -2419,7 +2456,7 @@ public class GhidraCliBridge extends GhidraScript {
}
JsonObject result = new JsonObject();
result.add("results", results);
result.add("instructions", results);
result.addProperty("count", results.size());
return result;
} catch (Exception e) {
+2 -2
View File
@@ -206,8 +206,8 @@ impl BridgeClient {
)
}
pub fn type_list(&self) -> Result<serde_json::Value> {
self.send_command("type_list", None)
pub fn type_list(&self, limit: Option<usize>) -> Result<serde_json::Value> {
self.send_command("type_list", Some(json!({"limit": limit})))
}
pub fn type_get(&self, name: &str) -> Result<serde_json::Value> {
+198 -13
View File
@@ -9,13 +9,14 @@ mod ipc;
mod query;
use clap::Parser;
use cli::{Cli, Commands};
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::path::PathBuf;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
@@ -325,6 +326,78 @@ fn extract_program_from_command(command: &Commands) -> Option<String> {
}
}
/// 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()?;
@@ -452,8 +525,22 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> {
}
};
// Determine output format based on flags and TTY detection
let format = if cli.pretty {
// Check for .NET decompilation and warn
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 {
OutputFormat::JsonCompact
@@ -461,11 +548,19 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> {
auto_detect_format(atty::is(atty::Stream::Stdout))
};
// Detect if result is already an array before wrapping
let values = match result {
serde_json::Value::Array(arr) => arr,
single => vec![single],
};
// 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)?;
@@ -495,7 +590,7 @@ fn execute_via_bridge(
}))
}
Commands::Query(args) => match args.data_type.as_str() {
"functions" => client.list_functions(args.limit, args.filter.clone()),
"functions" => client.list_functions(args.limit, None),
"strings" => client.list_strings(args.limit),
"imports" => client.list_imports(),
"exports" => client.list_exports(),
@@ -507,7 +602,7 @@ fn execute_via_bridge(
use cli::FunctionCommands;
match cmd {
FunctionCommands::List(opts) => {
client.list_functions(opts.limit, opts.filter.clone())
client.list_functions(opts.limit, None)
}
FunctionCommands::Decompile(args) => client.decompile(args.target.clone()),
FunctionCommands::Get(args) => {
@@ -577,7 +672,7 @@ fn execute_via_bridge(
DumpCommands::Imports(_) => client.list_imports(),
DumpCommands::Exports(_) => client.list_exports(),
DumpCommands::Functions(opts) => {
client.list_functions(opts.limit, opts.filter.clone())
client.list_functions(opts.limit, None)
}
DumpCommands::Strings(opts) => client.list_strings(opts.limit),
}
@@ -618,7 +713,7 @@ fn execute_via_bridge(
Commands::Symbol(cmd) => {
use cli::SymbolCommands;
match cmd {
SymbolCommands::List(opts) => client.symbol_list(opts.filter.as_deref()),
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),
@@ -630,7 +725,7 @@ fn execute_via_bridge(
Commands::Type(cmd) => {
use cli::TypeCommands;
match cmd {
TypeCommands::List(_) => client.type_list(),
TypeCommands::List(opts) => client.type_list(opts.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),
@@ -1128,6 +1223,96 @@ fn handle_project_command(cmd: cli::ProjectCommands) -> anyhow::Result<()> {
}
/// 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()? {
+41 -2
View File
@@ -1,11 +1,11 @@
#![allow(dead_code)]
use crate::cli::QueryOptions;
use crate::error::{GhidraError, Result};
use crate::filter::Filter;
use crate::format::{DefaultFormatter, Formatter, OutputFormat};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum DataType {
Functions,
Strings,
@@ -24,6 +24,7 @@ pub enum DataType {
References,
}
#[allow(dead_code)]
impl DataType {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
@@ -51,6 +52,7 @@ impl DataType {
}
pub struct Query {
#[allow(dead_code)]
pub data_type: DataType,
pub filter: Option<Filter>,
pub fields: Option<FieldSelector>,
@@ -61,6 +63,7 @@ pub struct Query {
pub count_only: bool,
}
#[allow(dead_code)]
impl Query {
pub fn new(data_type: DataType) -> Self {
Self {
@@ -75,6 +78,42 @@ impl Query {
}
}
/// Build a Query from CLI QueryOptions. Returns None if no query processing is needed.
pub fn from_options(opts: &QueryOptions, format: OutputFormat) -> Result<Option<Self>> {
let has_filter = opts.filter.is_some();
let has_fields = opts.fields.is_some();
let has_sort = opts.sort.is_some();
let has_count = opts.count;
// No query processing needed if no filter/fields/sort/count
if !has_filter && !has_fields && !has_sort && !has_count {
return Ok(None);
}
let filter = opts
.filter
.as_ref()
.map(|f| Filter::parse(f))
.transpose()?;
let fields = opts
.fields
.as_ref()
.map(|f| FieldSelector::parse(f))
.transpose()?;
let sort = opts.sort.as_ref().map(|s| SortKey::parse(s));
Ok(Some(Self {
data_type: DataType::Functions, // placeholder, not used in process_results
filter,
fields,
format,
limit: None, // limit/offset already handled by bridge
offset: None,
sort,
count_only: has_count,
}))
}
pub fn with_filter(mut self, filter: Filter) -> Self {
self.filter = Some(filter);
self