diff --git a/Cargo.lock b/Cargo.lock index be359f9..dc1cc9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "ghidra-cli" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "assert_cmd", diff --git a/src/cli.rs b/src/cli.rs index 480ec62..c3b62a4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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), diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 645564e..dac403a 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -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 { diff --git a/src/format/mod.rs b/src/format/mod.rs index 553b728..3d3a0d7 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -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 { 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(data: &[T], delimiter: char) -> Result { Ok(result) } +/// Compact human-readable format: one line per item with key fields. +fn format_compact(data: &[T]) -> Result { + let json_data: Vec = data + .iter() + .map(serde_json::to_value) + .collect::, _>>()?; + + 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::>() + .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 = 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 = 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 = 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(data: &[T]) -> Result { + let json_data: Vec = data + .iter() + .map(serde_json::to_value) + .collect::, _>>()?; + + 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(data: &[T]) -> Result { let json_data: Vec = data .iter() diff --git a/src/ghidra/scripts/GhidraCliBridge.java b/src/ghidra/scripts/GhidraCliBridge.java index e53a94d..46e1d15 100644 --- a/src/ghidra/scripts/GhidraCliBridge.java +++ b/src/ghidra/scripts/GhidraCliBridge.java @@ -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 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 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) { diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 5cdf3c7..2427cb6 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -206,8 +206,8 @@ impl BridgeClient { ) } - pub fn type_list(&self) -> Result { - self.send_command("type_list", None) + pub fn type_list(&self, limit: Option) -> Result { + self.send_command("type_list", Some(json!({"limit": limit}))) } pub fn type_get(&self, name: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index 753aaa7..2a5b97c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { } } +/// Extract QueryOptions from a command, if it has them. +fn extract_query_options(command: &Commands) -> Option { + 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 { + // 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()? { diff --git a/src/query/mod.rs b/src/query/mod.rs index fd32617..3bde34b 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -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 { 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, pub fields: Option, @@ -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> { + 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