mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
fixes
This commit is contained in:
Generated
+1
-1
@@ -750,7 +750,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ghidra-cli"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
+32
-11
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user