From ac1fb7b931eb32874a1fd8b46392dd6ed64da8d6 Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Wed, 4 Feb 2026 14:04:32 -0800 Subject: [PATCH] v2 wip --- Cargo.lock | 119 +- Cargo.toml | 14 +- src/daemon/cache.rs | 155 -- src/daemon/handler.rs | 427 ---- src/daemon/handlers/batch.rs | 54 - src/daemon/handlers/comments.rs | 86 - src/daemon/handlers/diff.rs | 61 - src/daemon/handlers/disasm.rs | 41 - src/daemon/handlers/find.rs | 111 - src/daemon/handlers/graph.rs | 94 - src/daemon/handlers/mod.rs | 14 - src/daemon/handlers/patch.rs | 73 - src/daemon/handlers/program.rs | 132 -- src/daemon/handlers/script.rs | 83 - src/daemon/handlers/stats.rs | 30 - src/daemon/handlers/symbols.rs | 127 -- src/daemon/handlers/types.rs | 82 - src/daemon/ipc_server.rs | 163 -- src/daemon/mod.rs | 217 +- src/daemon/process.rs | 246 --- src/daemon/queue.rs | 559 ----- src/daemon/state.rs | 82 - src/ghidra/bridge.rs | 800 +++---- src/ghidra/scripts/GhidraCliBridge.java | 2528 +++++++++++++++++++++++ src/ipc/client.rs | 398 ++-- src/ipc/mod.rs | 16 +- src/ipc/protocol.rs | 219 +- src/ipc/transport.rs | 243 +-- src/main.rs | 1071 +++++----- tests/batch_tests.rs | 10 +- tests/comment_tests.rs | 12 +- tests/common/helpers.rs | 5 +- tests/common/mod.rs | 184 +- tests/daemon_tests.rs | 36 +- tests/diff_tests.rs | 4 +- tests/find_tests.rs | 16 +- tests/graph_tests.rs | 8 +- tests/program_tests.rs | 8 +- tests/reliability_tests.rs | 74 +- tests/script_tests.rs | 8 +- tests/stats_tests.rs | 6 +- tests/symbol_tests.rs | 12 +- tests/type_tests.rs | 10 +- 43 files changed, 3988 insertions(+), 4650 deletions(-) delete mode 100644 src/daemon/cache.rs delete mode 100644 src/daemon/handler.rs delete mode 100644 src/daemon/handlers/batch.rs delete mode 100644 src/daemon/handlers/comments.rs delete mode 100644 src/daemon/handlers/diff.rs delete mode 100644 src/daemon/handlers/disasm.rs delete mode 100644 src/daemon/handlers/find.rs delete mode 100644 src/daemon/handlers/graph.rs delete mode 100644 src/daemon/handlers/mod.rs delete mode 100644 src/daemon/handlers/patch.rs delete mode 100644 src/daemon/handlers/program.rs delete mode 100644 src/daemon/handlers/script.rs delete mode 100644 src/daemon/handlers/stats.rs delete mode 100644 src/daemon/handlers/symbols.rs delete mode 100644 src/daemon/handlers/types.rs delete mode 100644 src/daemon/ipc_server.rs delete mode 100644 src/daemon/process.rs delete mode 100644 src/daemon/queue.rs delete mode 100644 src/daemon/state.rs create mode 100644 src/ghidra/scripts/GhidraCliBridge.java diff --git a/Cargo.lock b/Cargo.lock index 1398834..47b466d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,12 +498,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "doctest-file" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" - [[package]] name = "document-features" version = "0.2.12" @@ -640,30 +634,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fslock" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -671,7 +641,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -726,7 +695,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -785,12 +753,11 @@ dependencies = [ "dirs", "dunce", "env_logger", - "fslock", "futures-util", "indicatif", "insta", - "interprocess", "lazy_static", + "libc", "log", "md5", "once_cell", @@ -799,7 +766,6 @@ dependencies = [ "predicates", "proptest", "regex", - "remoc", "reqwest", "serde", "serde_json", @@ -1140,21 +1106,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "interprocess" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" -dependencies = [ - "doctest-file", - "futures-core", - "libc", - "recvmsg", - "tokio", - "widestring", - "windows-sys 0.52.0", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -1751,12 +1702,6 @@ dependencies = [ "rand_core 0.9.4", ] -[[package]] -name = "recvmsg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -1806,37 +1751,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "remoc" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b862dbacf3e0cad8d9031e799ba77db2f6f1f65a2480c3d3310fab8b3b1f499b" -dependencies = [ - "byteorder", - "bytes", - "futures", - "rand", - "remoc_macro", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "remoc_macro" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b929c6b6255d8d80205d79d80f41093adf0d14089e51c74ff5b850f2160b0677" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "reqwest" version = "0.11.27" @@ -2181,16 +2095,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -2423,25 +2327,11 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2 0.6.1", - "tokio-macros", "windows-sys 0.61.2", ] -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -2634,7 +2524,6 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde_core", "wasm-bindgen", ] @@ -2808,12 +2697,6 @@ dependencies = [ "winsafe", ] -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 1396862..b11dc29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,21 +34,12 @@ log = "0.4" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -# Async runtime -tokio = { version = "1.35", features = ["full"] } - -# RPC -remoc = { version = "0.16", features = ["full"] } - -# IPC (local sockets) -interprocess = { version = "2.2", features = ["tokio"] } +# Async runtime (only needed for setup command's HTTP downloads) +tokio = { version = "1.35", features = ["rt", "io-util", "time", "net"] } # Time chrono = { version = "0.4", features = ["serde"] } -# File locking -fslock = "0.2" - # Hashing md5 = "0.7" @@ -59,6 +50,7 @@ walkdir = "2.4" # Process management which = "6.0" +libc = "0.2" # Regex regex = "1.10" diff --git a/src/daemon/cache.rs b/src/daemon/cache.rs deleted file mode 100644 index f2ab3c6..0000000 --- a/src/daemon/cache.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Caching layer for common requests. -//! -//! Caches results of expensive Ghidra operations to speed up repeated queries. - -#![allow(dead_code)] - -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use tokio::sync::RwLock; -use tracing::debug; - -use crate::cli::Commands; - -/// A cached entry with timestamp. -struct CacheEntry { - value: String, - inserted_at: Instant, -} - -impl CacheEntry { - fn new(value: String) -> Self { - Self { - value, - inserted_at: Instant::now(), - } - } - - fn is_expired(&self, ttl: Duration) -> bool { - self.inserted_at.elapsed() > ttl - } -} - -/// Cache for command results. -pub struct Cache { - /// Cache storage - entries: Arc>>, - /// Time-to-live for cache entries - ttl: Duration, -} - -impl Cache { - /// Create a new cache with default TTL (5 minutes). - pub fn new() -> Self { - Self::with_ttl(Duration::from_secs(300)) - } - - /// Create a new cache with custom TTL. - pub fn with_ttl(ttl: Duration) -> Self { - Self { - entries: Arc::new(RwLock::new(HashMap::new())), - ttl, - } - } - - /// Get a cached value if it exists and hasn't expired. - pub async fn get(&self, command: &Commands) -> Option { - let key = self.cache_key(command)?; - - let entries = self.entries.read().await; - if let Some(entry) = entries.get(&key) { - if !entry.is_expired(self.ttl) { - debug!("Cache hit for key: {}", key); - return Some(entry.value.clone()); - } else { - debug!("Cache entry expired for key: {}", key); - } - } - - None - } - - /// Set a cached value. - pub async fn set(&self, command: &Commands, value: String) { - if let Some(key) = self.cache_key(command) { - let mut entries = self.entries.write().await; - entries.insert(key.clone(), CacheEntry::new(value)); - debug!("Cached result for key: {}", key); - } - } - - /// Clear all cached entries. - pub async fn clear(&self) { - let mut entries = self.entries.write().await; - entries.clear(); - debug!("Cache cleared"); - } - - /// Remove expired entries. - pub async fn cleanup(&self) { - let mut entries = self.entries.write().await; - let ttl = self.ttl; - entries.retain(|_, entry| !entry.is_expired(ttl)); - debug!("Cache cleanup completed"); - } - - /// Generate a cache key for a command. - /// Only cacheable commands return Some. - fn cache_key(&self, command: &Commands) -> Option { - // For now, generate a simple cache key based on debug representation - // TODO: Implement proper cache key generation for specific command types - Some(format!("{:?}", command)) - } -} - -impl Default for Cache { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_cache_operations() { - let cache = Cache::new(); - - // Create a test command (using Version since it's simple) - let command = Commands::Version; - - // Should be empty initially - assert!(cache.get(&command).await.is_none()); - - // Set a value - cache.set(&command, "test result".to_string()).await; - - // Should return the value - assert_eq!(cache.get(&command).await, Some("test result".to_string())); - - // Clear cache - cache.clear().await; - - // Should be empty again - assert!(cache.get(&command).await.is_none()); - } - - #[tokio::test] - async fn test_cache_expiration() { - let cache = Cache::with_ttl(Duration::from_millis(100)); - - let command = Commands::Version; - - cache.set(&command, "test".to_string()).await; - assert!(cache.get(&command).await.is_some()); - - // Wait for expiration - tokio::time::sleep(Duration::from_millis(150)).await; - - // Should be expired - assert!(cache.get(&command).await.is_none()); - } -} diff --git a/src/daemon/handler.rs b/src/daemon/handler.rs deleted file mode 100644 index 556922b..0000000 --- a/src/daemon/handler.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! Command handler for processing IPC requests. -//! -//! Translates IPC commands into Ghidra bridge operations. -//! Handles lazy bridge startup on Import/Analyze commands. - -use std::sync::Arc; - -use serde_json::json; -use tokio::sync::Mutex; -use tracing::{debug, info}; - -use crate::ghidra::bridge::{BridgeStartMode, GhidraBridge}; -use crate::ipc::protocol::{Command, Response}; - -use super::DaemonState; - -/// Handle an IPC command. -pub async fn handle_command( - state: &Arc, - id: u64, - command: Command, -) -> Response { - match handle_command_inner(state, command).await { - Ok(result) => Response::success(id, result), - Err(e) => Response::error(id, e.to_string()), - } -} - -/// Ensure the bridge is running, returning an error if not. -async fn require_bridge( - bridge: &Arc>>, -) -> anyhow::Result<()> { - let bridge_guard = bridge.lock().await; - let b = bridge_guard - .as_ref() - .ok_or_else(|| anyhow::anyhow!("No program loaded. Run 'ghidra import ' first."))?; - if !b.is_running() { - anyhow::bail!("Bridge is not running. Try restarting the daemon."); - } - Ok(()) -} - -/// Start the bridge in import mode (lazy start). -async fn start_bridge_for_import( - state: &Arc, - binary_path: &str, - project: &str, -) -> anyhow::Result<()> { - let ghidra_dir = state - .ghidra_install_dir - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Ghidra installation directory not configured"))?; - - // Ensure project directory exists (Ghidra requires it) - if !state.project_path.exists() { - std::fs::create_dir_all(&state.project_path) - .map_err(|e| anyhow::anyhow!("Failed to create project directory: {}", e))?; - } - - // Stop existing bridge if any - { - let mut bridge_guard = state.bridge.lock().await; - if let Some(mut b) = bridge_guard.take() { - info!("Stopping existing bridge before import"); - b.stop().ok(); - } - } - - let mut new_bridge = GhidraBridge::new( - ghidra_dir.clone(), - state.project_path.clone(), - project.to_string(), - ); - - info!("Starting bridge in import mode for: {}", binary_path); - new_bridge.start(BridgeStartMode::Import { - binary_path: binary_path.to_string(), - })?; - - let mut bridge_guard = state.bridge.lock().await; - *bridge_guard = Some(new_bridge); - - Ok(()) -} - -/// Start the bridge in process mode (for analyze/query after import). -async fn start_bridge_for_process( - state: &Arc, - project: &str, - program: &str, -) -> anyhow::Result<()> { - let ghidra_dir = state - .ghidra_install_dir - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Ghidra installation directory not configured"))?; - - // Ensure project directory exists - if !state.project_path.exists() { - std::fs::create_dir_all(&state.project_path) - .map_err(|e| anyhow::anyhow!("Failed to create project directory: {}", e))?; - } - - // Stop existing bridge if any - { - let mut bridge_guard = state.bridge.lock().await; - if let Some(mut b) = bridge_guard.take() { - info!("Stopping existing bridge before starting process mode"); - b.stop().ok(); - } - } - - let mut new_bridge = GhidraBridge::new( - ghidra_dir.clone(), - state.project_path.clone(), - project.to_string(), - ); - - info!("Starting bridge in process mode for: {}", program); - new_bridge.start(BridgeStartMode::Process { - program_name: program.to_string(), - })?; - - let mut bridge_guard = state.bridge.lock().await; - *bridge_guard = Some(new_bridge); - - Ok(()) -} - -async fn handle_command_inner( - state: &Arc, - command: Command, -) -> anyhow::Result { - match command { - Command::Ping => Ok(json!({"status": "ok"})), - - Command::Status => { - let bridge_guard = state.bridge.lock().await; - let bridge_running = bridge_guard - .as_ref() - .map(|b| b.is_running()) - .unwrap_or(false); - Ok(json!({ - "bridge_running": bridge_running, - })) - } - - Command::ClearCache => { - // TODO: Implement cache clearing - Ok(json!({"cleared": true})) - } - - Command::Shutdown => { - // Shutdown is handled at a higher level - Ok(json!({"status": "shutting_down"})) - } - - // === Import: reuses running bridge if available, otherwise lazy-starts === - Command::Import { - binary_path, - project, - program, - } => { - // Check if bridge is already running - let bridge_running = { - let bridge_guard = state.bridge.lock().await; - bridge_guard.as_ref().map(|b| b.is_running()).unwrap_or(false) - }; - - if bridge_running { - // Bridge already running — import via bridge command, no JVM restart - let mut bridge_guard = state.bridge.lock().await; - let bridge = bridge_guard.as_mut().unwrap(); - - let import_response = bridge.send_command::( - "import", - Some(json!({ - "binary_path": binary_path, - "program": program, - })), - )?; - - if import_response.status != "success" { - let msg = import_response.message.unwrap_or_else(|| "Import failed".to_string()); - anyhow::bail!("{}", msg); - } - - let program_name = program.unwrap_or_else(|| { - import_response - .data - .as_ref() - .and_then(|d| d.get("program")) - .and_then(|n| n.as_str()) - .unwrap_or("unknown") - .to_string() - }); - - // Switch to the newly imported program - let open_response = bridge.send_command::( - "open_program", - Some(json!({"program": program_name})), - )?; - - if open_response.status != "success" { - let msg = open_response.message.unwrap_or_else(|| "Failed to switch program".to_string()); - anyhow::bail!("{}", msg); - } - - Ok(json!({"program": program_name})) - } else { - // No bridge running — start one in import mode - start_bridge_for_import(state, &binary_path, &project).await?; - - let mut bridge_guard = state.bridge.lock().await; - let bridge = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge failed to start"))?; - - let response = bridge.send_command::( - "program_info", - None, - )?; - - if response.status == "success" { - let program_name = program.unwrap_or_else(|| { - response - .data - .as_ref() - .and_then(|d| d.get("name")) - .and_then(|n| n.as_str()) - .unwrap_or("unknown") - .to_string() - }); - Ok(json!({"program": program_name})) - } else { - let msg = response.message.unwrap_or_else(|| "Import failed".to_string()); - anyhow::bail!("{}", msg) - } - } - } - - // === Analyze: reuses running bridge, Python side handles program switching === - Command::Analyze { project, program } => { - // Check if bridge is already running - let bridge_running = { - let bridge_guard = state.bridge.lock().await; - bridge_guard.as_ref().map(|b| b.is_running()).unwrap_or(false) - }; - - if bridge_running { - // Bridge already running — analyze command handles open_program internally - let mut bridge_guard = state.bridge.lock().await; - let bridge = bridge_guard.as_mut().unwrap(); - let response = bridge.send_command::( - "analyze", - Some(json!({"project": project, "program": program})), - )?; - - if response.status == "success" { - Ok(response.data.unwrap_or(json!({"status": "analysis_complete"}))) - } else { - let msg = response.message.unwrap_or_else(|| "Analysis failed".to_string()); - anyhow::bail!("{}", msg) - } - } else { - // Bridge not running, start it in process mode - start_bridge_for_process(state, &project, &program).await?; - Ok(json!({"status": "bridge_started", "program": program})) - } - } - - // === Program management commands === - Command::ListPrograms => { - require_bridge(&state.bridge).await?; - execute_bridge_command(state, "list_programs", None).await - } - - Command::OpenProgram { program } => { - require_bridge(&state.bridge).await?; - execute_bridge_command( - state, - "open_program", - Some(json!({"program": program})), - ) - .await - } - - // === All other commands require bridge to be running === - Command::ListFunctions { limit, filter } => { - require_bridge(&state.bridge).await?; - execute_bridge_command( - state, - "list_functions", - Some(json!({ - "limit": limit, - "filter": filter, - })), - ) - .await - } - - Command::Decompile { address } => { - require_bridge(&state.bridge).await?; - execute_bridge_command( - state, - "decompile", - Some(json!({ - "address": address, - })), - ) - .await - } - - Command::ListStrings { limit } => { - require_bridge(&state.bridge).await?; - execute_bridge_command( - state, - "list_strings", - Some(json!({ - "limit": limit, - })), - ) - .await - } - - Command::ListImports => { - require_bridge(&state.bridge).await?; - execute_bridge_command(state, "list_imports", None).await - } - - Command::ListExports => { - require_bridge(&state.bridge).await?; - execute_bridge_command(state, "list_exports", None).await - } - - Command::MemoryMap => { - require_bridge(&state.bridge).await?; - execute_bridge_command(state, "memory_map", None).await - } - - Command::ProgramInfo => { - require_bridge(&state.bridge).await?; - execute_bridge_command(state, "program_info", None).await - } - - Command::XRefsTo { address } => { - require_bridge(&state.bridge).await?; - execute_bridge_command( - state, - "xrefs_to", - Some(json!({ - "address": address, - })), - ) - .await - } - - Command::XRefsFrom { address } => { - require_bridge(&state.bridge).await?; - execute_bridge_command( - state, - "xrefs_from", - Some(json!({ - "address": address, - })), - ) - .await - } - - Command::ExecuteCli { command_json } => { - // Deserialize and execute CLI command through the queue handlers - let cli_command: crate::cli::Commands = serde_json::from_str(&command_json) - .map_err(|e| anyhow::anyhow!("Failed to deserialize CLI command: {}", e))?; - - // Ensure bridge is running for ExecuteCli commands - require_bridge(&state.bridge).await?; - - // Execute using the queue's command execution logic - let result = crate::daemon::queue::execute_command_direct(&state.bridge, &cli_command).await?; - - // Parse the result as JSON (handlers return JSON strings) - Ok(serde_json::from_str(&result).unwrap_or_else(|_| json!({"output": result}))) - } - } -} - -/// Execute a command on the Ghidra bridge. -/// -/// If the bridge process dies during command execution, triggers daemon shutdown. -async fn execute_bridge_command( - state: &Arc, - command: &str, - args: Option, -) -> anyhow::Result { - let mut bridge_guard = state.bridge.lock().await; - - let bridge = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("No program loaded. Run 'ghidra import ' first."))?; - - if !bridge.is_running() { - anyhow::bail!("Bridge is not running"); - } - - debug!("Executing bridge command: {}", command); - - let response = match bridge.send_command::(command, args) { - Ok(resp) => resp, - Err(e) => { - // Check if bridge process died - trigger daemon shutdown - let err_msg = e.to_string(); - if err_msg.contains("process died") || !bridge.is_running() { - info!("Bridge process died, triggering daemon shutdown"); - let _ = state.shutdown_tx.send(()); - } - return Err(e); - } - }; - - if response.status == "success" { - Ok(response.data.unwrap_or(json!({}))) - } else { - let message = response - .message - .unwrap_or_else(|| "Unknown error".to_string()); - anyhow::bail!("{}", message) - } -} diff --git a/src/daemon/handlers/batch.rs b/src/daemon/handlers/batch.rs deleted file mode 100644 index e5676cd..0000000 --- a/src/daemon/handlers/batch.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Batch operation handler. - -use anyhow::{Context, Result}; -use serde_json::json; -use std::fs; -use std::path::Path; - -pub async fn handle_batch(file_path: &str) -> Result { - let path = Path::new(file_path); - - if !path.exists() { - anyhow::bail!("Batch file not found: {}", file_path); - } - - let content = fs::read_to_string(path) - .with_context(|| format!("Failed to read batch file: {}", file_path))?; - - let mut results = Vec::new(); - let mut line_number = 0; - - for line in content.lines() { - line_number += 1; - let trimmed = line.trim(); - - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } - - results.push(json!({ - "line": line_number, - "command": trimmed, - "status": "not_implemented", - "message": "Batch command execution not yet implemented" - })); - } - - let response = json!({ - "file": file_path, - "commands_parsed": results.len(), - "results": results - }); - - serde_json::to_string(&response).context("Failed to serialize batch results") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_batch_placeholder() { - assert!(true); - } -} diff --git a/src/daemon/handlers/comments.rs b/src/daemon/handlers/comments.rs deleted file mode 100644 index 7aed452..0000000 --- a/src/daemon/handlers/comments.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Comment operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_comment_list(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("comment_list", None) - .context("Failed to list comments")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to list comments".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_comment_get(bridge: &mut GhidraBridge, address: &str) -> Result { - let response = bridge - .send_command::("comment_get", Some(json!({"address": address}))) - .context("Failed to get comment")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get comment".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_comment_set( - bridge: &mut GhidraBridge, - address: &str, - text: &str, - comment_type: Option<&str>, -) -> Result { - let mut args = json!({ - "address": address, - "text": text - }); - - if let Some(ctype) = comment_type { - args["comment_type"] = json!(ctype); - } - - let response = bridge - .send_command::("comment_set", Some(args)) - .context("Failed to set comment")?; - - if response.status == "success" { - Ok(json!({"status": "set", "address": address}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to set comment".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_comment_delete(bridge: &mut GhidraBridge, address: &str) -> Result { - let response = bridge - .send_command::("comment_delete", Some(json!({"address": address}))) - .context("Failed to delete comment")?; - - if response.status == "success" { - Ok(json!({"status": "deleted", "address": address}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to delete comment".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/daemon/handlers/diff.rs b/src/daemon/handlers/diff.rs deleted file mode 100644 index d9c187e..0000000 --- a/src/daemon/handlers/diff.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Diff operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_diff_programs( - bridge: &mut GhidraBridge, - prog1: &str, - prog2: &str, -) -> Result { - let response = bridge - .send_command::( - "diff_programs", - Some(json!({"prog1": prog1, "prog2": prog2})), - ) - .context("Failed to diff programs")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to diff programs".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_diff_functions( - bridge: &mut GhidraBridge, - func1: &str, - func2: &str, -) -> Result { - let response = bridge - .send_command::( - "diff_functions", - Some(json!({"func1": func1, "func2": func2})), - ) - .context("Failed to diff functions")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to diff functions".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_diff_handlers_exist() { - assert!(true); - } -} diff --git a/src/daemon/handlers/disasm.rs b/src/daemon/handlers/disasm.rs deleted file mode 100644 index e45f510..0000000 --- a/src/daemon/handlers/disasm.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Disassembly operation handler. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_disasm( - bridge: &mut GhidraBridge, - address: &str, - count: Option, -) -> Result { - let mut args = json!({"address": address}); - - if let Some(num) = count { - args["count"] = json!(num); - } - - let response = bridge - .send_command::("disasm", Some(args)) - .context("Failed to disassemble")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to disassemble".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_disasm_placeholder() { - assert!(true); - } -} diff --git a/src/daemon/handlers/find.rs b/src/daemon/handlers/find.rs deleted file mode 100644 index 7380da0..0000000 --- a/src/daemon/handlers/find.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Find/search operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_find_string(bridge: &mut GhidraBridge, pattern: &str) -> Result { - let response = bridge - .send_command::("find_string", Some(json!({"pattern": pattern}))) - .context("Failed to find string")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to find string".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_find_bytes(bridge: &mut GhidraBridge, hex: &str) -> Result { - let response = bridge - .send_command::("find_bytes", Some(json!({"hex": hex}))) - .context("Failed to find bytes")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to find bytes".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_find_function(bridge: &mut GhidraBridge, pattern: &str) -> Result { - let response = bridge - .send_command::("find_function", Some(json!({"pattern": pattern}))) - .context("Failed to find function")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to find function".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_find_calls(bridge: &mut GhidraBridge, function: &str) -> Result { - let response = bridge - .send_command::("find_calls", Some(json!({"function": function}))) - .context("Failed to find calls")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to find calls".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_find_crypto(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("find_crypto", None) - .context("Failed to find crypto constants")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to find crypto constants".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_find_interesting(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("find_interesting", None) - .context("Failed to find interesting functions")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to find interesting functions".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_find_placeholder() { - assert!(true); - } -} diff --git a/src/daemon/handlers/graph.rs b/src/daemon/handlers/graph.rs deleted file mode 100644 index 7feb797..0000000 --- a/src/daemon/handlers/graph.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Graph operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_graph_calls(bridge: &mut GhidraBridge, limit: Option) -> Result { - let args = limit.map(|lim| json!({"limit": lim})); - - let response = bridge - .send_command::("graph_calls", args) - .context("Failed to get call graph")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get call graph".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_graph_callers( - bridge: &mut GhidraBridge, - function: &str, - depth: Option, -) -> Result { - let mut args = json!({"function": function}); - if let Some(d) = depth { - args["depth"] = json!(d); - } - - let response = bridge - .send_command::("graph_callers", Some(args)) - .context("Failed to get callers")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get callers".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_graph_callees( - bridge: &mut GhidraBridge, - function: &str, - depth: Option, -) -> Result { - let mut args = json!({"function": function}); - if let Some(d) = depth { - args["depth"] = json!(d); - } - - let response = bridge - .send_command::("graph_callees", Some(args)) - .context("Failed to get callees")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get callees".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_graph_export(bridge: &mut GhidraBridge, format: &str) -> Result { - let response = bridge - .send_command::("graph_export", Some(json!({"format": format}))) - .context("Failed to export graph")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to export graph".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/daemon/handlers/mod.rs b/src/daemon/handlers/mod.rs deleted file mode 100644 index 3c6a0fc..0000000 --- a/src/daemon/handlers/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Handler modules for daemon commands grouped by category. - -pub mod batch; -pub mod comments; -pub mod diff; -pub mod disasm; -pub mod find; -pub mod graph; -pub mod patch; -pub mod program; -pub mod script; -pub mod stats; -pub mod symbols; -pub mod types; diff --git a/src/daemon/handlers/patch.rs b/src/daemon/handlers/patch.rs deleted file mode 100644 index a98a97b..0000000 --- a/src/daemon/handlers/patch.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Patch operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_patch_bytes( - bridge: &mut GhidraBridge, - address: &str, - hex: &str, -) -> Result { - let response = bridge - .send_command::( - "patch_bytes", - Some(json!({ - "address": address, - "hex": hex - })), - ) - .context("Failed to patch bytes")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to patch bytes".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_patch_nop(bridge: &mut GhidraBridge, address: &str) -> Result { - let response = bridge - .send_command::("patch_nop", Some(json!({"address": address}))) - .context("Failed to patch NOP")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to patch NOP".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_patch_export(bridge: &mut GhidraBridge, output: &str) -> Result { - let response = bridge - .send_command::("patch_export", Some(json!({"output": output}))) - .context("Failed to export patched binary")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to export patched binary".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_placeholder() { - assert!(true); - } -} diff --git a/src/daemon/handlers/program.rs b/src/daemon/handlers/program.rs deleted file mode 100644 index a285890..0000000 --- a/src/daemon/handlers/program.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Program operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_program_list(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("list_programs", None) - .context("Failed to list programs")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to list programs".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_program_open( - bridge: &mut GhidraBridge, - program_name: &str, -) -> Result { - let response = bridge - .send_command::( - "open_program", - Some(json!({ - "program": program_name - })), - ) - .context("Failed to open program")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to open program".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_program_close(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("program_close", None) - .context("Failed to close program")?; - - if response.status == "success" { - Ok(json!({"status": "closed"}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to close program".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_program_delete( - bridge: &mut GhidraBridge, - program_name: &str, -) -> Result { - let response = bridge - .send_command::( - "program_delete", - Some(json!({ - "program": program_name - })), - ) - .context("Failed to delete program")?; - - if response.status == "success" { - Ok(json!({"status": "deleted", "program": program_name}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to delete program".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_program_info(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("program_info", None) - .context("Failed to get program info")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get program info".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_program_export( - bridge: &mut GhidraBridge, - format: &str, - output: Option<&str>, -) -> Result { - let mut args = json!({ - "format": format - }); - - if let Some(output_path) = output { - args["output"] = json!(output_path); - } - - let response = bridge - .send_command::("program_export", Some(args)) - .context("Failed to export program")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to export program".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/daemon/handlers/script.rs b/src/daemon/handlers/script.rs deleted file mode 100644 index f98ca95..0000000 --- a/src/daemon/handlers/script.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Script execution handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_script_run( - bridge: &mut GhidraBridge, - path: &str, - args: &[String], -) -> Result { - let response = bridge - .send_command::("script_run", Some(json!({"path": path, "args": args}))) - .context("Failed to run script")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to run script".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_script_python(bridge: &mut GhidraBridge, code: &str) -> Result { - let response = bridge - .send_command::("script_python", Some(json!({"code": code}))) - .context("Failed to execute Python code")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to execute Python code".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_script_java(bridge: &mut GhidraBridge, code: &str) -> Result { - let response = bridge - .send_command::("script_java", Some(json!({"code": code}))) - .context("Failed to execute Java code")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to execute Java code".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_script_list(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("script_list", None) - .context("Failed to list scripts")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to list scripts".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_script_placeholder() { - assert!(true); - } -} diff --git a/src/daemon/handlers/stats.rs b/src/daemon/handlers/stats.rs deleted file mode 100644 index 8d5d63c..0000000 --- a/src/daemon/handlers/stats.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Program statistics handler. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; - -pub async fn handle_stats(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("stats", None) - .context("Failed to get program statistics")?; - - if response.status == "success" { - let data = response.data.unwrap_or(serde_json::json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get program statistics".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_stats_placeholder() { - assert!(true); - } -} diff --git a/src/daemon/handlers/symbols.rs b/src/daemon/handlers/symbols.rs deleted file mode 100644 index 374505b..0000000 --- a/src/daemon/handlers/symbols.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Symbol operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_symbol_list(bridge: &mut GhidraBridge, filter: Option<&str>) -> Result { - let args = filter.map(|f| json!({"filter": f})); - - let response = bridge - .send_command::("symbol_list", args) - .context("Failed to list symbols")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to list symbols".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_symbol_get(bridge: &mut GhidraBridge, name: &str) -> Result { - let response = bridge - .send_command::("symbol_get", Some(json!({"name": name}))) - .context("Failed to get symbol")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get symbol".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_symbol_create( - bridge: &mut GhidraBridge, - address: &str, - name: &str, -) -> Result { - let response = bridge - .send_command::( - "symbol_create", - Some(json!({ - "address": address, - "name": name - })), - ) - .context("Failed to create symbol")?; - - if response.status == "success" { - Ok(json!({"status": "created", "address": address, "name": name}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to create symbol".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_symbol_delete(bridge: &mut GhidraBridge, name: &str) -> Result { - let response = bridge - .send_command::("symbol_delete", Some(json!({"name": name}))) - .context("Failed to delete symbol")?; - - if response.status == "success" { - Ok(json!({"status": "deleted", "name": name}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to delete symbol".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_symbol_rename( - bridge: &mut GhidraBridge, - old_name: &str, - new_name: &str, -) -> Result { - let response = bridge - .send_command::( - "symbol_rename", - Some(json!({ - "old_name": old_name, - "new_name": new_name - })), - ) - .context("Failed to rename symbol")?; - - if response.status == "success" { - Ok(json!({"status": "renamed", "old_name": old_name, "new_name": new_name}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to rename symbol".to_string()); - anyhow::bail!("{}", message) - } -} - -/// Resolve address input - handles both hex addresses and symbol names. -/// The actual resolution is done on the Python side. -#[allow(dead_code)] -fn resolve_address(input: &str) -> Result { - // Pass-through to Python layer which handles both hex addresses and symbol name lookups - Ok(input.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_resolve_address_hex() { - assert_eq!(resolve_address("0x1000").unwrap(), "0x1000"); - } - - #[test] - fn test_resolve_address_name() { - assert_eq!(resolve_address("main").unwrap(), "main"); - } -} diff --git a/src/daemon/handlers/types.rs b/src/daemon/handlers/types.rs deleted file mode 100644 index d4a8014..0000000 --- a/src/daemon/handlers/types.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Type operation handlers. - -use crate::ghidra::bridge::GhidraBridge; -use anyhow::{Context, Result}; -use serde_json::json; - -pub async fn handle_type_list(bridge: &mut GhidraBridge) -> Result { - let response = bridge - .send_command::("type_list", None) - .context("Failed to list types")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to list types".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_type_get(bridge: &mut GhidraBridge, name: &str) -> Result { - let response = bridge - .send_command::("type_get", Some(json!({"name": name}))) - .context("Failed to get type")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to get type".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_type_create(bridge: &mut GhidraBridge, name: &str) -> Result { - let response = bridge - .send_command::("type_create", Some(json!({"name": name}))) - .context("Failed to create type")?; - - if response.status == "success" { - Ok(json!({"status": "created", "name": name}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to create type".to_string()); - anyhow::bail!("{}", message) - } -} - -pub async fn handle_type_apply( - bridge: &mut GhidraBridge, - address: &str, - type_name: &str, -) -> Result { - let response = bridge - .send_command::( - "type_apply", - Some(json!({ - "address": address, - "type_name": type_name - })), - ) - .context("Failed to apply type")?; - - if response.status == "success" { - Ok(json!({"status": "applied", "address": address, "type": type_name}).to_string()) - } else { - let message = response - .message - .unwrap_or_else(|| "Failed to apply type".to_string()); - anyhow::bail!("{}", message) - } -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/daemon/ipc_server.rs b/src/daemon/ipc_server.rs deleted file mode 100644 index ea1cb9d..0000000 --- a/src/daemon/ipc_server.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! IPC server for daemon communication. -//! -//! Uses local sockets (Unix domain sockets / Windows named pipes) with -//! the new IPC layer instead of TCP. -//! -//! Each project gets its own socket for concurrent daemon operation. - -#![allow(dead_code)] - -use std::path::Path; -use std::sync::Arc; -use std::time::Instant; - -use interprocess::local_socket::traits::tokio::Listener as ListenerTrait; -use tokio::io::BufReader; -use tokio::sync::broadcast; -use tracing::{debug, error, info}; - -use crate::ipc::protocol::{Command, Request, Response}; -use crate::ipc::transport; - -use super::handler; -use super::DaemonState; - -/// IPC server state -pub struct IpcServer { - /// Shared daemon state (bridge + config) - daemon_state: Arc, - /// Shutdown signal sender - shutdown_tx: broadcast::Sender<()>, - /// Server start time - started_at: Instant, -} - -impl IpcServer { - /// Create a new IPC server. - pub fn new( - daemon_state: Arc, - shutdown_tx: broadcast::Sender<()>, - ) -> Self { - Self { - daemon_state, - shutdown_tx, - started_at: Instant::now(), - } - } - - /// Handle a single client connection. - async fn handle_client(&self, stream: transport::platform::Stream) -> anyhow::Result { - let (reader, mut writer) = tokio::io::split(stream); - let mut reader = BufReader::new(reader); - - loop { - // Read request with timeout - let request_data = tokio::select! { - result = transport::recv_message(&mut reader) => { - match result { - Ok(data) => data, - Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { - debug!("Client disconnected"); - return Ok(false); - } - Err(e) => { - error!("Error reading request: {}", e); - return Ok(false); - } - } - } - _ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => { - debug!("Client timeout"); - return Ok(false); - } - }; - - // Parse request - let request: Request = match serde_json::from_slice(&request_data) { - Ok(req) => req, - Err(e) => { - error!("Invalid request: {}", e); - let response = Response::error(0, format!("Invalid request: {}", e)); - let json = serde_json::to_vec(&response)?; - transport::send_message(&mut writer, &json).await?; - continue; - } - }; - - debug!("Received command: {:?}", request.command); - - // Check for shutdown command - if matches!(request.command, Command::Shutdown) { - let response = Response::ok(request.id); - let json = serde_json::to_vec(&response)?; - transport::send_message(&mut writer, &json).await?; - return Ok(true); // Signal shutdown - } - - // Handle command - let response = handler::handle_command(&self.daemon_state, request.id, request.command).await; - - // Send response - let json = serde_json::to_vec(&response)?; - transport::send_message(&mut writer, &json).await?; - } - } -} - -/// Run the IPC server for a specific project. -pub async fn run_ipc_server( - daemon_state: Arc, - shutdown_tx: broadcast::Sender<()>, - project_path: &Path, -) -> anyhow::Result<()> { - // Create the IPC listener for this project - let listener = transport::create_listener_for_project(project_path) - .await - .map_err(|e| anyhow::anyhow!("Failed to create IPC listener: {}", e))?; - - info!( - "IPC server listening on {}", - transport::socket_name_for_project(project_path) - ); - - let server = Arc::new(IpcServer::new(daemon_state, shutdown_tx.clone())); - let mut shutdown_rx = shutdown_tx.subscribe(); - - loop { - tokio::select! { - accept_result = listener.accept() => { - match accept_result { - Ok(stream) => { - info!("Accepted IPC connection"); - let server = server.clone(); - let shutdown_tx = shutdown_tx.clone(); - tokio::spawn(async move { - match server.handle_client(stream).await { - Ok(should_shutdown) if should_shutdown => { - info!("Shutdown requested via IPC"); - let _ = shutdown_tx.send(()); - } - Ok(_) => {} - Err(e) => { - error!("Connection error: {}", e); - } - } - }); - } - Err(e) => { - error!("Accept error: {}", e); - } - } - } - _ = shutdown_rx.recv() => { - info!("IPC server shutting down"); - break; - } - } - } - - // Clean up socket for this project - transport::remove_socket_for_project(project_path).ok(); - - Ok(()) -} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 61041a7..380df05 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -1,183 +1,64 @@ -//! Daemon core logic. +//! Bridge management module. //! -//! The daemon is the main runtime that: -//! - Manages a persistent Ghidra bridge process -//! - Serves commands via local socket IPC -//! - Handles graceful shutdown +//! Manages the lifecycle of the Java GhidraCliBridge process. +//! The "daemon" is just the long-running Ghidra/Java bridge process - +//! there is no separate Rust daemon. The CLI connects directly to +//! the bridge via TCP. -use std::path::PathBuf; -use std::sync::Arc; +use std::path::{Path, PathBuf}; -use anyhow::{Context, Result}; -use tokio::sync::{broadcast, Mutex}; -use tracing::{error, info, warn}; +use anyhow::Result; -use crate::daemon::process::{acquire_daemon_lock, get_data_dir, remove_info_file, write_daemon_info, DaemonInfo}; -use crate::ghidra::bridge::GhidraBridge; +use crate::ghidra::bridge::{self, BridgeStartMode, BridgeStatus}; -pub mod cache; -pub mod handler; -pub mod handlers; -pub mod ipc_server; -pub mod process; -pub mod queue; -pub mod state; - -/// Daemon configuration. -pub struct DaemonConfig { - /// Path to the project directory +/// Bridge configuration (replaces old DaemonConfig). +pub struct BridgeConfig { + /// Path to the Ghidra project directory pub project_path: PathBuf, /// Ghidra installation directory - pub ghidra_install_dir: Option, - /// Log file path - pub log_file: PathBuf, + pub ghidra_install_dir: PathBuf, } -/// Shared daemon state accessible by handlers. -pub struct DaemonState { - /// The Ghidra bridge instance (None until first import/analyze) - pub bridge: Arc>>, - /// Ghidra installation directory - pub ghidra_install_dir: Option, - /// Project path on disk - pub project_path: PathBuf, - /// Shutdown signal sender - handlers can trigger daemon shutdown on bridge death - pub shutdown_tx: broadcast::Sender<()>, +/// Ensure a bridge is running for the given project. +/// If import mode, starts with the binary. If process mode, opens existing program. +/// Returns the port number for connecting. +pub fn ensure_bridge( + config: &BridgeConfig, + mode: BridgeStartMode, +) -> Result { + bridge::ensure_bridge_running( + &config.project_path, + &config.ghidra_install_dir, + mode, + ) } -/// Run the daemon with the new bridge architecture. -pub async fn run(config: DaemonConfig) -> Result<()> { - info!("Starting Ghidra daemon"); - info!("Project: {}", config.project_path.display()); - - // Get data directory - let data_dir = get_data_dir().context("Failed to get data directory")?; - - // Create shutdown channel - let (shutdown_tx, _shutdown_rx) = broadcast::channel::<()>(1); - - // Initialize shared daemon state - bridge starts as None, lazy-started on first command - let daemon_state = Arc::new(DaemonState { - bridge: Arc::new(Mutex::new(None)), - ghidra_install_dir: config.ghidra_install_dir.clone(), - project_path: config.project_path.clone(), - shutdown_tx: shutdown_tx.clone(), - }); - - info!("Bridge will be started on first import/analyze command"); - - // Acquire OS-level lock (atomic liveness check) - let _lock = acquire_daemon_lock(&data_dir, &config.project_path) - .context("Failed to acquire daemon lock")?; - - // Write daemon info to separate file - let daemon_info = DaemonInfo::new(&config.project_path, &config.log_file); - write_daemon_info(&data_dir, &config.project_path, &daemon_info) - .context("Failed to write daemon info")?; - - // Start IPC server task - let ipc_state = daemon_state.clone(); - let ipc_shutdown_tx = shutdown_tx.clone(); - let ipc_project_path = config.project_path.clone(); - let ipc_handle = tokio::spawn(async move { - if let Err(e) = - ipc_server::run_ipc_server(ipc_state, ipc_shutdown_tx, &ipc_project_path).await - { - error!("IPC server error: {}", e); - } - }); - - // Wait for shutdown signal - let shutdown_reason = wait_for_shutdown(shutdown_tx.clone()).await; - - info!("Shutdown initiated: {:?}", shutdown_reason); - - // Clean up - shutdown_tx.send(()).ok(); // Signal all tasks to stop - - // Stop the bridge if it was started - { - let mut bridge_guard = daemon_state.bridge.lock().await; - if let Some(mut b) = bridge_guard.take() { - info!("Stopping Ghidra bridge..."); - if let Err(e) = b.stop() { - error!("Error stopping bridge: {}", e); - } - } - } - - // Wait for IPC server to stop (with timeout) - tokio::select! { - _ = ipc_handle => { - info!("IPC server stopped"); - } - _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => { - warn!("IPC server did not stop in time"); - } - } - - // Remove info file; lock file is released when _lock drops at end of scope - remove_info_file(&data_dir, &config.project_path).ok(); - - info!("Daemon stopped"); - Ok(()) +/// Start a new bridge for the given project. +/// Returns the port number for connecting. +pub fn start_bridge( + config: &BridgeConfig, + mode: BridgeStartMode, +) -> Result { + bridge::start_bridge( + &config.project_path, + &config.ghidra_install_dir, + mode, + ) } -/// The reason for shutdown. -#[derive(Debug, Clone)] -pub enum ShutdownReason { - /// SIGINT (Ctrl+C) - Interrupt, - /// SIGTERM - Terminate, - /// RPC shutdown request - RpcRequest, +/// Stop the bridge for a project. +pub fn stop_bridge(project_path: &Path) -> Result<()> { + bridge::stop_bridge(project_path) } -/// Wait for a shutdown signal. -async fn wait_for_shutdown(shutdown_tx: broadcast::Sender<()>) -> ShutdownReason { - let mut shutdown_rx = shutdown_tx.subscribe(); +/// Get bridge status for a project. +pub fn get_bridge_status(project_path: &Path) -> Result { + bridge::bridge_status(project_path) +} - #[cfg(unix)] - { - use tokio::signal::unix::{signal, SignalKind}; - - let mut sigint = - signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler"); - let mut sigterm = - signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler"); - - tokio::select! { - _ = sigint.recv() => { - info!("Received SIGINT"); - ShutdownReason::Interrupt - } - _ = sigterm.recv() => { - info!("Received SIGTERM"); - ShutdownReason::Terminate - } - _ = shutdown_rx.recv() => { - info!("Received shutdown request via RPC"); - ShutdownReason::RpcRequest - } - } - } - - #[cfg(windows)] - { - use tokio::signal; - - tokio::select! { - _ = signal::ctrl_c() => { - info!("Received Ctrl+C"); - ShutdownReason::Interrupt - } - _ = shutdown_rx.recv() => { - info!("Received shutdown request via RPC"); - ShutdownReason::RpcRequest - } - } - } +/// Check if a bridge is running for a project. +pub fn is_bridge_running(project_path: &Path) -> bool { + bridge::is_bridge_running(project_path) } #[cfg(test)] @@ -185,13 +66,13 @@ mod tests { use super::*; #[test] - fn test_daemon_config() { - let config = DaemonConfig { + fn test_bridge_config() { + let config = BridgeConfig { project_path: PathBuf::from("/test/project"), - ghidra_install_dir: None, - log_file: PathBuf::from("/test/logs/daemon.log"), + ghidra_install_dir: PathBuf::from("/opt/ghidra"), }; assert_eq!(config.project_path, PathBuf::from("/test/project")); + assert_eq!(config.ghidra_install_dir, PathBuf::from("/opt/ghidra")); } } diff --git a/src/daemon/process.rs b/src/daemon/process.rs deleted file mode 100644 index 3563cf5..0000000 --- a/src/daemon/process.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! Process management for the daemon. -//! -//! Handles lock files and daemon process information using OS-level file locking -//! via `fslock` for atomic daemon liveness detection. - -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; - -use anyhow::{bail, Context, Result}; -use chrono::{DateTime, Utc}; -use fslock::LockFile; -use serde::{Deserialize, Serialize}; - -/// Daemon information stored in the info file. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DaemonInfo { - /// Process ID of the daemon - pub pid: u32, - /// Project path being managed - pub project_path: PathBuf, - /// Log file path - pub log_file: PathBuf, - /// When the daemon was started - pub started_at: DateTime, -} - -impl DaemonInfo { - /// Create new daemon info. - pub fn new(project_path: &Path, log_file: &Path) -> Self { - Self { - pid: std::process::id(), - project_path: project_path.to_path_buf(), - log_file: log_file.to_path_buf(), - started_at: Utc::now(), - } - } -} - -/// Get the data directory for daemon files. -/// -/// Checks GHIDRA_CLI_DATA_DIR env var first (used for testing), then falls back to default. -pub fn get_data_dir() -> Result { - let data_dir = if let Ok(path) = std::env::var("GHIDRA_CLI_DATA_DIR") { - PathBuf::from(path) - } else { - dirs::data_local_dir() - .context("Failed to get local data directory")? - .join("ghidra-cli") - }; - - fs::create_dir_all(&data_dir).context("Failed to create data directory")?; - - Ok(data_dir) -} - -/// Get the lock file path for a project (used for OS-level locking only). -fn get_lock_file_path(data_dir: &Path, project_path: &Path) -> PathBuf { - let project_hash = format!( - "{:x}", - md5::compute(project_path.to_string_lossy().as_bytes()) - ); - data_dir.join(format!("daemon-{}.lock", project_hash)) -} - -/// Get the info file path for a project (stores DaemonInfo JSON). -fn get_info_file_path(data_dir: &Path, project_path: &Path) -> PathBuf { - let project_hash = format!( - "{:x}", - md5::compute(project_path.to_string_lossy().as_bytes()) - ); - data_dir.join(format!("daemon-{}.info", project_hash)) -} - -/// Acquire an exclusive OS-level lock for the daemon. -/// -/// Returns the held `LockFile` — the caller must keep it alive for the daemon's -/// entire lifetime. The lock is automatically released when the `LockFile` is dropped -/// (including on crash). -pub fn acquire_daemon_lock( - data_dir: &Path, - project_path: &Path, -) -> Result { - let lock_path = get_lock_file_path(data_dir, project_path); - let mut lock = LockFile::open(&lock_path) - .context("Failed to open lock file")?; - - if !lock.try_lock_with_pid() - .context("Failed to acquire lock")? { - bail!("Daemon is already running for this project"); - } - - Ok(lock) -} - -/// Write daemon info to the info file (separate from the lock file). -pub fn write_daemon_info(data_dir: &Path, project_path: &Path, info: &DaemonInfo) -> Result<()> { - let info_path = get_info_file_path(data_dir, project_path); - let json = serde_json::to_string_pretty(info).context("Failed to serialize daemon info")?; - - let mut file = fs::File::create(&info_path).context("Failed to create info file")?; - file.write_all(json.as_bytes()) - .context("Failed to write info file")?; - - Ok(()) -} - -/// Remove the info file for a project. -/// -/// The `.lock` file is released automatically when the daemon's `LockFile` handle drops. -/// Stale `.lock` files are harmless (empty, unlocked) and cleaned up by `get_running_daemon_info()`. -pub fn remove_info_file(data_dir: &Path, project_path: &Path) -> Result<()> { - let info_path = get_info_file_path(data_dir, project_path); - - if info_path.exists() { - fs::remove_file(&info_path).context("Failed to remove info file")?; - } - - Ok(()) -} - -/// Get daemon info if running, or clean up stale files. -/// -/// Uses OS-level locking for atomic liveness detection: -/// - If we can acquire the lock, no daemon holds it — clean up stale files. -/// - If we cannot acquire the lock, a daemon is alive — read the info file. -pub fn get_running_daemon_info(data_dir: &Path, project_path: &Path) -> Result> { - let lock_path = get_lock_file_path(data_dir, project_path); - if !lock_path.exists() { - return Ok(None); - } - - let mut lock = LockFile::open(&lock_path) - .context("Failed to open lock file for status check")?; - - if lock.try_lock().context("Failed to check lock")? { - // We got the lock — no daemon is holding it. Clean up stale files. - lock.unlock().context("Failed to release lock")?; - fs::remove_file(&lock_path).ok(); - let info_path = get_info_file_path(data_dir, project_path); - fs::remove_file(&info_path).ok(); - // Also clean up stale socket file (daemon may have crashed without cleanup) - crate::ipc::transport::remove_socket_for_project(project_path).ok(); - return Ok(None); - } - - // Lock is held by another process — daemon is running. Read the info file. - let info_path = get_info_file_path(data_dir, project_path); - let contents = fs::read_to_string(&info_path) - .context("Lock is held but info file is missing")?; - let info: DaemonInfo = serde_json::from_str(&contents) - .context("Failed to parse daemon info file")?; - Ok(Some(info)) -} - -/// Ensure no daemon is currently running for this project. -pub fn ensure_not_running(data_dir: &Path, project_path: &Path) -> Result<()> { - if let Some(info) = get_running_daemon_info(data_dir, project_path)? { - bail!("Daemon is already running (PID: {})", info.pid); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - #[test] - fn test_daemon_info_creation() { - let info = DaemonInfo::new( - Path::new("/test/project"), - Path::new("/test/logs/daemon.log"), - ); - - assert_eq!(info.project_path, PathBuf::from("/test/project")); - } - - #[test] - fn test_lock_and_info_file_operations() -> Result<()> { - let temp_dir = tempdir()?; - let data_dir = temp_dir.path(); - let project_path = PathBuf::from("/test/project"); - - // Acquire lock - let _lock = acquire_daemon_lock(data_dir, &project_path)?; - - // Write info - let info = DaemonInfo::new(&project_path, Path::new("/test/logs/daemon.log")); - write_daemon_info(data_dir, &project_path, &info)?; - - // Info file should exist - let info_path = get_info_file_path(data_dir, &project_path); - assert!(info_path.exists()); - - // Remove info file - remove_info_file(data_dir, &project_path)?; - assert!(!info_path.exists()); - - Ok(()) - } - - #[test] - fn test_cannot_acquire_lock_twice() -> Result<()> { - let temp_dir = tempdir()?; - let data_dir = temp_dir.path(); - let project_path = PathBuf::from("/test/project"); - - // First lock succeeds - let _lock = acquire_daemon_lock(data_dir, &project_path)?; - - // Second lock should fail - let result = acquire_daemon_lock(data_dir, &project_path); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("already running")); - - Ok(()) - } - - #[test] - fn test_stale_lock_cleaned_up() -> Result<()> { - let temp_dir = tempdir()?; - let data_dir = temp_dir.path(); - let project_path = PathBuf::from("/test/project"); - - // Create a lock file but don't hold the lock (simulates crashed daemon) - let lock_path = get_lock_file_path(data_dir, &project_path); - fs::File::create(&lock_path)?; - - // Also create a stale info file - let info_path = get_info_file_path(data_dir, &project_path); - let info = DaemonInfo::new(&project_path, Path::new("/test/logs/daemon.log")); - let json = serde_json::to_string_pretty(&info)?; - fs::write(&info_path, json)?; - - // get_running_daemon_info should detect no lock holder and clean up - let result = get_running_daemon_info(data_dir, &project_path)?; - assert!(result.is_none()); - - // Stale files should be cleaned up - assert!(!lock_path.exists()); - assert!(!info_path.exists()); - - Ok(()) - } -} diff --git a/src/daemon/queue.rs b/src/daemon/queue.rs deleted file mode 100644 index 25ee410..0000000 --- a/src/daemon/queue.rs +++ /dev/null @@ -1,559 +0,0 @@ -//! Command queue for serializing Ghidra operations. -//! -//! Ensures only one Ghidra headless operation runs at a time to prevent conflicts. - -#![allow(dead_code)] - -use std::collections::VecDeque; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use anyhow::{Context, Result}; -use tokio::sync::{oneshot, Mutex, Semaphore}; -use tracing::{info, warn}; - -use crate::cli::Commands; -use crate::daemon::cache::Cache; -use crate::daemon::handlers; -use crate::ghidra::bridge::GhidraBridge; - -/// A queued command waiting to be executed. -struct QueuedCommand { - command: Commands, - response_tx: oneshot::Sender>, -} - -/// Command queue for managing Ghidra operations. -pub struct CommandQueue { - /// The project path being managed - project_path: PathBuf, - /// Queue of pending commands - queue: Arc>>, - /// Semaphore to ensure only one command executes at a time - execution_lock: Arc, - /// Number of completed commands - completed_count: Arc>, - /// Cache for common requests - cache: Arc, - /// The Ghidra bridge instance - bridge: Arc>>, -} - -impl CommandQueue { - /// Create a new command queue. - pub fn new(project_path: PathBuf, bridge: Arc>>) -> Self { - Self { - project_path, - queue: Arc::new(Mutex::new(VecDeque::new())), - execution_lock: Arc::new(Semaphore::new(1)), - completed_count: Arc::new(Mutex::new(0)), - cache: Arc::new(Cache::new()), - bridge, - } - } - - /// Submit a command for execution. - pub async fn submit(&self, command: Commands) -> Result { - // Check cache first - if let Some(cached) = self.cache.get(&command).await { - info!("Cache hit for command"); - return Ok(cached); - } - - let (response_tx, response_rx) = oneshot::channel(); - - // Add to queue - { - let mut queue = self.queue.lock().await; - queue.push_back(QueuedCommand { - command: command.clone(), - response_tx, - }); - info!("Command queued (queue depth: {})", queue.len()); - } - - // Process queue - self.process_queue().await; - - // Wait for response - response_rx - .await - .context("Failed to receive command response")? - } - - /// Process commands in the queue. - async fn process_queue(&self) { - let execution_lock = self.execution_lock.clone(); - let queue = self.queue.clone(); - let completed_count = self.completed_count.clone(); - let cache = self.cache.clone(); - let project_path = self.project_path.clone(); - let bridge = self.bridge.clone(); - - tokio::spawn(async move { - // Try to acquire execution lock (non-blocking) - if let Ok(_permit) = execution_lock.try_acquire() { - while let Some(queued_cmd) = { - let mut q = queue.lock().await; - q.pop_front() - } { - info!("Executing command from queue"); - - // Execute the command - let result = execute_command(&project_path, &bridge, &queued_cmd.command).await; - - // Cache successful results - if let Ok(ref output) = result { - cache.set(&queued_cmd.command, output.clone()).await; - } - - // Send response - if queued_cmd.response_tx.send(result).is_err() { - warn!("Failed to send command response (receiver dropped)"); - } - - // Increment completed count - let mut count = completed_count.lock().await; - *count += 1; - } - } - }); - } - - /// Get the current queue depth. - pub fn queue_depth(&self) -> usize { - // This is a synchronous method, so we can't await the lock - // Return 0 as an estimate (actual depth available via async method) - 0 - } - - /// Get the current queue depth (async version). - pub async fn queue_depth_async(&self) -> usize { - let queue = self.queue.lock().await; - queue.len() - } - - /// Get the number of completed commands. - pub fn completed_count(&self) -> usize { - // This is a synchronous method, so we can't await the lock - // Return 0 as an estimate (actual count available via async method) - 0 - } - - /// Get the number of completed commands (async version). - pub async fn completed_count_async(&self) -> usize { - let count = self.completed_count.lock().await; - *count - } - - /// Get the project path. - pub fn project_path(&self) -> &Path { - &self.project_path - } -} - -/// Execute a command against Ghidra via the bridge. -async fn execute_command( - _project_path: &Path, - bridge: &Arc>>, - command: &Commands, -) -> Result { - use serde_json::json; - - let (bridge_cmd, args) = match command { - Commands::Query(query_args) => match query_args.data_type.as_str() { - "functions" => ( - "list_functions", - Some(json!({ - "limit": query_args.limit, - "filter": query_args.filter, - })), - ), - "strings" => ( - "list_strings", - Some(json!({ - "limit": query_args.limit, - })), - ), - "imports" => ("list_imports", None), - "exports" => ("list_exports", None), - _ => anyhow::bail!("Unknown query type: {}", query_args.data_type), - }, - Commands::Decompile(decompile_args) => ( - "decompile", - Some(json!({ - "address": decompile_args.target, - })), - ), - Commands::Memory(mem_cmd) => { - use crate::cli::MemoryCommands; - match mem_cmd { - MemoryCommands::Map(_) => ("memory_map", None), - _ => anyhow::bail!("Memory command not yet supported in daemon"), - } - } - Commands::XRef(xref_cmd) => { - use crate::cli::XRefCommands; - match xref_cmd { - XRefCommands::To(args) => ( - "xrefs_to", - Some(json!({ - "address": args.address, - })), - ), - XRefCommands::From(args) => ( - "xrefs_from", - Some(json!({ - "address": args.address, - })), - ), - XRefCommands::List(_) => anyhow::bail!("XRef List not yet supported"), - } - } - Commands::Program(prog_cmd) => { - use crate::cli::ProgramCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match prog_cmd { - ProgramCommands::List(_) => { - handlers::program::handle_program_list(bridge_ref).await - } - ProgramCommands::Open(args) => { - let program = args - .program - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Program name required"))?; - handlers::program::handle_program_open(bridge_ref, program).await - } - ProgramCommands::Close(_) => { - handlers::program::handle_program_close(bridge_ref).await - } - ProgramCommands::Delete(args) => { - let program = args - .program - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Program name required"))?; - handlers::program::handle_program_delete(bridge_ref, program).await - } - ProgramCommands::Info(_) => { - handlers::program::handle_program_info(bridge_ref).await - } - ProgramCommands::Export(args) => { - handlers::program::handle_program_export( - bridge_ref, - &args.format, - args.output.as_deref(), - ) - .await - } - }; - } - Commands::Symbol(sym_cmd) => { - use crate::cli::SymbolCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match sym_cmd { - SymbolCommands::List(opts) => { - handlers::symbols::handle_symbol_list(bridge_ref, opts.filter.as_deref()).await - } - SymbolCommands::Get(args) => { - handlers::symbols::handle_symbol_get(bridge_ref, &args.name).await - } - SymbolCommands::Create(args) => { - handlers::symbols::handle_symbol_create(bridge_ref, &args.address, &args.name) - .await - } - SymbolCommands::Delete(args) => { - handlers::symbols::handle_symbol_delete(bridge_ref, &args.name).await - } - SymbolCommands::Rename(args) => { - handlers::symbols::handle_symbol_rename( - bridge_ref, - &args.old_name, - &args.new_name, - ) - .await - } - }; - } - Commands::Type(type_cmd) => { - use crate::cli::TypeCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match type_cmd { - TypeCommands::List(_) => handlers::types::handle_type_list(bridge_ref).await, - TypeCommands::Get(args) => { - handlers::types::handle_type_get(bridge_ref, &args.name).await - } - TypeCommands::Create(args) => { - handlers::types::handle_type_create(bridge_ref, &args.definition).await - } - TypeCommands::Apply(args) => { - handlers::types::handle_type_apply(bridge_ref, &args.address, &args.type_name) - .await - } - }; - } - Commands::Comment(comment_cmd) => { - use crate::cli::CommentCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match comment_cmd { - CommentCommands::List(_) => { - handlers::comments::handle_comment_list(bridge_ref).await - } - CommentCommands::Get(args) => { - handlers::comments::handle_comment_get(bridge_ref, &args.address).await - } - CommentCommands::Set(args) => { - handlers::comments::handle_comment_set( - bridge_ref, - &args.address, - &args.text, - args.comment_type.as_deref(), - ) - .await - } - CommentCommands::Delete(args) => { - handlers::comments::handle_comment_delete(bridge_ref, &args.address).await - } - }; - } - Commands::Graph(graph_cmd) => { - use crate::cli::GraphCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match graph_cmd { - GraphCommands::Calls(opts) => { - handlers::graph::handle_graph_calls(bridge_ref, opts.limit).await - } - GraphCommands::Callers(args) => { - handlers::graph::handle_graph_callers(bridge_ref, &args.function, args.depth) - .await - } - GraphCommands::Callees(args) => { - handlers::graph::handle_graph_callees(bridge_ref, &args.function, args.depth) - .await - } - GraphCommands::Export(args) => { - handlers::graph::handle_graph_export(bridge_ref, &args.format).await - } - }; - } - Commands::Find(find_cmd) => { - use crate::cli::FindCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match find_cmd { - FindCommands::String(args) => { - handlers::find::handle_find_string(bridge_ref, &args.pattern).await - } - FindCommands::Bytes(args) => { - handlers::find::handle_find_bytes(bridge_ref, &args.hex).await - } - FindCommands::Function(args) => { - handlers::find::handle_find_function(bridge_ref, &args.pattern).await - } - FindCommands::Calls(args) => { - handlers::find::handle_find_calls(bridge_ref, &args.function).await - } - FindCommands::Crypto(_) => handlers::find::handle_find_crypto(bridge_ref).await, - FindCommands::Interesting(_) => { - handlers::find::handle_find_interesting(bridge_ref).await - } - }; - } - Commands::Diff(diff_cmd) => { - use crate::cli::DiffCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match diff_cmd { - DiffCommands::Programs(args) => { - handlers::diff::handle_diff_programs(bridge_ref, &args.program1, &args.program2) - .await - } - DiffCommands::Functions(args) => { - handlers::diff::handle_diff_functions(bridge_ref, &args.func1, &args.func2) - .await - } - }; - } - Commands::Patch(patch_cmd) => { - use crate::cli::PatchCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match patch_cmd { - PatchCommands::Bytes(args) => { - handlers::patch::handle_patch_bytes(bridge_ref, &args.address, &args.hex).await - } - PatchCommands::Nop(args) => { - handlers::patch::handle_patch_nop(bridge_ref, &args.address).await - } - PatchCommands::Export(args) => { - handlers::patch::handle_patch_export(bridge_ref, &args.output).await - } - }; - } - Commands::Script(script_cmd) => { - use crate::cli::ScriptCommands; - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return match script_cmd { - ScriptCommands::Run(args) => { - handlers::script::handle_script_run(bridge_ref, &args.script_path, &args.args) - .await - } - ScriptCommands::Python(args) => { - handlers::script::handle_script_python(bridge_ref, &args.code).await - } - ScriptCommands::Java(args) => { - handlers::script::handle_script_java(bridge_ref, &args.code).await - } - ScriptCommands::List => handlers::script::handle_script_list(bridge_ref).await, - }; - } - Commands::Disasm(args) => { - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return handlers::disasm::handle_disasm( - bridge_ref, - &args.address, - args.num_instructions, - ) - .await; - } - Commands::Batch(args) => { - return handlers::batch::handle_batch(&args.script_file).await; - } - Commands::Stats(_) => { - let mut bridge_guard = bridge.lock().await; - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - return handlers::stats::handle_stats(bridge_ref).await; - } - Commands::Summary(_) => ("program_info", None), - _ => anyhow::bail!("Command not yet supported in daemon: {:?}", command), - }; - - let mut bridge_guard = bridge.lock().await; - - let bridge_ref = bridge_guard - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?; - - if !bridge_ref.is_running() { - anyhow::bail!("Bridge is not running"); - } - - let response = bridge_ref - .send_command::(bridge_cmd, args) - .context("Bridge command failed")?; - - if response.status == "success" { - let data = response.data.unwrap_or(json!({})); - serde_json::to_string_pretty(&data).context("Failed to serialize response") - } else { - let message = response - .message - .unwrap_or_else(|| "Unknown error".to_string()); - anyhow::bail!("{}", message) - } -} - -/// Execute a CLI command directly (for IPC handler use). -/// This bypasses the queue and executes immediately. -pub async fn execute_command_direct( - bridge: &Arc>>, - command: &Commands, -) -> Result { - execute_command(&PathBuf::new(), bridge, command).await -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_queue_creation() { - let bridge = Arc::new(Mutex::new(None)); - let queue = CommandQueue::new(PathBuf::from("/test/project"), bridge); - assert_eq!(queue.project_path(), Path::new("/test/project")); - assert_eq!(queue.queue_depth_async().await, 0); - } -} diff --git a/src/daemon/state.rs b/src/daemon/state.rs deleted file mode 100644 index cae406e..0000000 --- a/src/daemon/state.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Daemon state management. -//! -//! Manages the state of loaded Ghidra projects and maintains metadata. - -#![allow(dead_code)] - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use anyhow::{Context, Result}; -use tokio::sync::RwLock; -use tracing::info; - -use crate::config::Config; -use crate::ghidra::GhidraClient; - -/// Daemon state. -pub struct DaemonState { - /// The Ghidra client - client: Arc>, - /// Project path being managed - project_path: PathBuf, -} - -impl DaemonState { - /// Load daemon state for a project. - pub fn load(project_path: &Path, ghidra_install_dir: Option<&Path>) -> Result { - info!( - "Loading daemon state for project: {}", - project_path.display() - ); - - // Load config - let mut config = Config::load().context("Failed to load config")?; - - // Override ghidra install dir if provided - if let Some(dir) = ghidra_install_dir { - config.ghidra_install_dir = Some(dir.to_path_buf()); - } - - // Create Ghidra client - let client = GhidraClient::new(config).context("Failed to create Ghidra client")?; - - // Verify the client installation is valid - client - .verify_installation() - .context("Invalid Ghidra installation")?; - - info!("Daemon state loaded successfully"); - - Ok(Self { - client: Arc::new(RwLock::new(client)), - project_path: project_path.to_path_buf(), - }) - } - - /// Get a read lock on the Ghidra client. - pub async fn client(&self) -> tokio::sync::RwLockReadGuard<'_, GhidraClient> { - self.client.read().await - } - - /// Get a write lock on the Ghidra client. - pub async fn client_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, GhidraClient> { - self.client.write().await - } - - /// Get the project path. - pub fn project_path(&self) -> &Path { - &self.project_path - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_state_creation() { - // Note: This test would need a real Ghidra project to work - // In a real test environment, you'd set up a test project first - } -} diff --git a/src/ghidra/bridge.rs b/src/ghidra/bridge.rs index b49cb6e..3211852 100644 --- a/src/ghidra/bridge.rs +++ b/src/ghidra/bridge.rs @@ -1,23 +1,18 @@ -//! Ghidra Bridge - manages a persistent Ghidra process. +//! Ghidra Bridge - manages a persistent Ghidra Java bridge process. //! -//! Instead of spawning a new `analyzeHeadless` process for each command, -//! the bridge maintains a single long-running Ghidra process that serves -//! commands via a TCP socket. +//! The bridge runs a GhidraCliBridge.java script via `analyzeHeadless` that +//! starts a TCP socket server. The CLI connects directly to this server +//! to execute commands. No intermediate daemon process is needed. use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use tracing::{debug, error, info, warn}; - -/// Default bridge port -const DEFAULT_BRIDGE_PORT: u16 = 18700; +use tracing::{debug, info, warn}; /// Response from the bridge #[derive(Debug, Deserialize)] @@ -48,407 +43,442 @@ pub enum BridgeStartMode { }, } -/// Manages a persistent Ghidra bridge process. -pub struct GhidraBridge { - /// Child process handle - child: Option, - /// TCP connection to the bridge - stream: Option, - /// Bridge port - port: u16, - /// Project name - project_name: String, - /// Path to Ghidra installation - ghidra_install_dir: PathBuf, - /// Project directory - project_dir: PathBuf, - /// Whether the bridge is running - running: Arc, +/// Embedded Java bridge script +const JAVA_BRIDGE_SCRIPT: &str = include_str!("scripts/GhidraCliBridge.java"); + +/// Get the data directory for bridge port/PID files. +pub fn get_data_dir() -> Result { + let dir = dirs::data_local_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))? + .join("ghidra-cli"); + std::fs::create_dir_all(&dir)?; + Ok(dir) } -impl GhidraBridge { - /// Create a new bridge (not started yet). - pub fn new( - ghidra_install_dir: PathBuf, - project_dir: PathBuf, - project_name: String, - ) -> Self { - Self { - child: None, - stream: None, - port: DEFAULT_BRIDGE_PORT, - project_name, - ghidra_install_dir, - project_dir, - running: Arc::new(AtomicBool::new(false)), +/// Compute MD5 hash of project path for file naming. +fn project_hash(project_path: &Path) -> String { + format!( + "{:x}", + md5::compute(project_path.to_string_lossy().as_bytes()) + ) +} + +/// Get the port file path for a project. +pub fn port_file_path(project_path: &Path) -> Result { + let data_dir = get_data_dir()?; + let hash = project_hash(project_path); + Ok(data_dir.join(format!("bridge-{}.port", hash))) +} + +/// Get the PID file path for a project. +pub fn pid_file_path(project_path: &Path) -> Result { + let data_dir = get_data_dir()?; + let hash = project_hash(project_path); + Ok(data_dir.join(format!("bridge-{}.pid", hash))) +} + +/// Read the port from the port file. +pub fn read_port_file(project_path: &Path) -> Result> { + let path = port_file_path(project_path)?; + if !path.exists() { + return Ok(None); + } + let content = std::fs::read_to_string(&path)?; + let port: u16 = content.trim().parse() + .context("Invalid port in port file")?; + Ok(Some(port)) +} + +/// Read the PID from the PID file. +pub fn read_pid_file(project_path: &Path) -> Result> { + let path = pid_file_path(project_path)?; + if !path.exists() { + return Ok(None); + } + let content = std::fs::read_to_string(&path)?; + let pid: u32 = content.trim().parse() + .context("Invalid PID in PID file")?; + Ok(Some(pid)) +} + +/// Check if a process with the given PID is alive. +pub fn is_pid_alive(pid: u32) -> bool { + #[cfg(unix)] + { + unsafe { libc::kill(pid as i32, 0) == 0 } + } + #[cfg(windows)] + { + use std::process::Command; + Command::new("tasklist") + .args(["/FI", &format!("PID eq {}", pid)]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string())) + .unwrap_or(false) + } +} + +/// Clean up stale port and PID files. +pub fn cleanup_stale_files(project_path: &Path) -> Result<()> { + let port_path = port_file_path(project_path)?; + let pid_path = pid_file_path(project_path)?; + if port_path.exists() { + std::fs::remove_file(&port_path).ok(); + } + if pid_path.exists() { + std::fs::remove_file(&pid_path).ok(); + } + Ok(()) +} + +/// Check if a bridge is running for the given project. +/// +/// Verifies: port file exists, PID is alive, TCP connect succeeds. +pub fn is_bridge_running(project_path: &Path) -> bool { + let port = match read_port_file(project_path) { + Ok(Some(p)) => p, + _ => return false, + }; + + let pid = match read_pid_file(project_path) { + Ok(Some(p)) => p, + _ => return false, + }; + + if !is_pid_alive(pid) { + return false; + } + + // Verify TCP connect + TcpStream::connect(format!("127.0.0.1:{}", port)) + .map(|_| true) + .unwrap_or(false) +} + +/// Ensure a bridge is running for the given project. +/// Returns the port number to connect to. +pub fn ensure_bridge_running( + project_path: &Path, + ghidra_install_dir: &Path, + mode: BridgeStartMode, +) -> Result { + // Check if already running + if let Ok(Some(port)) = read_port_file(project_path) { + if let Ok(Some(pid)) = read_pid_file(project_path) { + if is_pid_alive(pid) { + // Verify TCP connect + if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { + info!("Bridge already running on port {}", port); + return Ok(port); + } + } + } + // Stale files - clean up + cleanup_stale_files(project_path)?; + } + + // Start a new bridge + start_bridge(project_path, ghidra_install_dir, mode) +} + +/// Start a new bridge process. +/// Returns the port number once the bridge is ready. +pub fn start_bridge( + project_path: &Path, + ghidra_install_dir: &Path, + mode: BridgeStartMode, +) -> Result { + info!("Starting Ghidra bridge..."); + + // Write the Java bridge script to disk + let scripts_dir = dirs::config_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))? + .join("ghidra-cli") + .join("scripts"); + std::fs::create_dir_all(&scripts_dir)?; + let java_script_path = scripts_dir.join("GhidraCliBridge.java"); + std::fs::write(&java_script_path, JAVA_BRIDGE_SCRIPT)?; + + // Find analyzeHeadless + let headless_script = find_headless_script(ghidra_install_dir)?; + + // Compute port file path + let port_file = port_file_path(project_path)?; + + // Build command + let mut cmd = Command::new(&headless_script); + + // analyzeHeadless expects: + let ghidra_project_dir = project_path + .parent() + .unwrap_or(project_path); + let ghidra_project_name = project_path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "project".to_string()); + + cmd.arg(ghidra_project_dir) + .arg(&ghidra_project_name); + + // Add mode-specific args + match &mode { + BridgeStartMode::Import { binary_path } => { + cmd.arg("-import").arg(binary_path); + } + BridgeStartMode::Process { program_name } => { + cmd.arg("-process") + .arg(program_name) + .arg("-noanalysis"); } } - /// Start the bridge with the given mode. - pub fn start(&mut self, mode: BridgeStartMode) -> Result<()> { - if self.running.load(Ordering::SeqCst) { - return Ok(()); - } + // Add Java bridge script args + cmd.arg("-scriptPath") + .arg(scripts_dir.to_str().unwrap()) + .arg("-postScript") + .arg("GhidraCliBridge.java") + .arg(port_file.to_str().unwrap()); - info!("Starting Ghidra bridge..."); + cmd.stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); - // Find headless script (pyghidraRun or analyzeHeadless) - let headless_script = self.find_headless_script()?; - let is_pyghidra = headless_script - .file_name() - .map(|n| n.to_string_lossy().contains("pyghidra")) - .unwrap_or(false); + info!("Ghidra command: {:?}", cmd); - // Get bridge script path - let bridge_script = self.get_bridge_script_path()?; + // Spawn the process + let mut child = cmd.spawn().context("Failed to spawn Ghidra headless")?; + info!("Ghidra process started with PID: {:?}", child.id()); - // Build command - pyghidraRun needs different arguments - let mut cmd = Command::new(&headless_script); - - // analyzeHeadless/pyghidraRun expects: - // self.project_dir is the FULL project path (e.g., /c/Users/dev/git/ghidra-altium) - // We need to split it into parent dir and project name for Ghidra's CLI - let ghidra_project_dir = self - .project_dir - .parent() - .unwrap_or(&self.project_dir); - let ghidra_project_name = self - .project_dir - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| self.project_name.clone()); - - if is_pyghidra { - cmd.arg("--headless") - .arg(ghidra_project_dir) - .arg(&ghidra_project_name); - } else { - cmd.arg(ghidra_project_dir) - .arg(&ghidra_project_name); - } - - // Add mode-specific args - match &mode { - BridgeStartMode::Import { binary_path } => { - cmd.arg("-import").arg(binary_path); - } - BridgeStartMode::Process { program_name } => { - cmd.arg("-process") - .arg(program_name) - .arg("-noanalysis"); - } - } - - // Add bridge script args - cmd.arg("-scriptPath") - .arg(bridge_script.parent().unwrap()) - .arg("-postScript") - .arg("bridge.py") - .arg(self.port.to_string()); - - cmd.stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - // Log the full command for debugging - info!("Ghidra command: {:?}", cmd); - - // Spawn the process - let mut child = cmd.spawn().context("Failed to spawn Ghidra headless")?; - info!("Ghidra process started with PID: {:?}", child.id()); - - // Spawn a thread to capture stderr - let stderr = child.stderr.take().expect("stderr should be piped"); - let stderr_handle = std::thread::spawn(move || { - let reader = BufReader::new(stderr); - let mut stderr_output = Vec::new(); - for line in reader.lines() { - if let Ok(line) = line { - // Log all stderr to info level so it's always visible - info!("[Ghidra stderr] {}", line); - stderr_output.push(line); - } - } - stderr_output - }); - - // Wait for ready signal from stdout - let stdout = child.stdout.take().expect("stdout should be piped"); - let reader = BufReader::new(stdout); - - let mut ready = false; - let mut last_error = String::new(); - let mut stdout_lines = Vec::new(); + // Spawn a thread to capture stderr + let stderr = child.stderr.take().expect("stderr should be piped"); + let stderr_handle = std::thread::spawn(move || { + let reader = BufReader::new(stderr); + let mut stderr_output = Vec::new(); for line in reader.lines() { - let line = line?; - // Log all stdout to info level so it's always visible during startup - info!("[Ghidra stdout] {}", line); - stdout_lines.push(line.clone()); - - // Capture Ghidra errors for better error messages - if line.contains("ERROR") || line.contains("Exception") || line.contains("SEVERE") { - last_error = line.clone(); - } - - // Look for ready signal - if line.contains("---GHIDRA_CLI_START---") { - // Read the next line for the JSON ready message - continue; - } - if line.contains("\"status\": \"ready\"") || line.contains("\"status\":\"ready\"") { - info!("Bridge is ready on port {}", self.port); - ready = true; - break; - } - if line.contains("---GHIDRA_CLI_END---") && ready { - break; + if let Ok(line) = line { + info!("[Ghidra stderr] {}", line); + stderr_output.push(line); } } + stderr_output + }); - if !ready { - // Wait for stderr thread and collect output - let stderr_output = stderr_handle.join().unwrap_or_default(); + // Wait for ready signal from stdout + let stdout = child.stdout.take().expect("stdout should be piped"); + let reader = BufReader::new(stdout); - // Check if process died - let detail = if !last_error.is_empty() { - format!(": {}", last_error) - } else if !stderr_output.is_empty() { - // Include last few stderr lines - let last_stderr: Vec<_> = stderr_output.iter().rev().take(5).rev().cloned().collect::>(); - format!(": stderr: {}", last_stderr.join("\n")) - } else { - // Include last few stdout lines for context - let last_stdout: Vec<_> = stdout_lines.iter().rev().take(10).rev().cloned().collect::>(); - format!("\nLast stdout:\n{}", last_stdout.join("\n")) - }; - match child.try_wait() { - Ok(Some(status)) => { - anyhow::bail!("Ghidra process exited with status: {}{}", status, detail); - } - Ok(None) => { - anyhow::bail!("Ghidra bridge did not send ready signal{}", detail); - } - Err(e) => { - anyhow::bail!("Error checking process status: {}", e); - } - } + let mut ready = false; + let mut last_error = String::new(); + let mut stdout_lines = Vec::new(); + for line in reader.lines() { + let line = line?; + info!("[Ghidra stdout] {}", line); + stdout_lines.push(line.clone()); + + if line.contains("ERROR") || line.contains("Exception") || line.contains("SEVERE") { + last_error = line.clone(); } - // Connect to the bridge - let stream = TcpStream::connect(format!("127.0.0.1:{}", self.port)) - .context("Failed to connect to bridge")?; - stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); - stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); - - self.child = Some(child); - self.stream = Some(stream); - self.running.store(true, Ordering::SeqCst); - - info!("Ghidra bridge started successfully"); - Ok(()) + if line.contains("---GHIDRA_CLI_START---") { + continue; + } + if line.contains("\"status\"") && line.contains("\"ready\"") { + info!("Bridge is ready"); + ready = true; + break; + } + if line.contains("---GHIDRA_CLI_END---") && ready { + break; + } } - /// Send a command to the bridge. - /// - /// On I/O errors, checks if the bridge process has died and updates - /// state accordingly. Returns a specific error if the process died. - pub fn send_command Deserialize<'de>>( - &mut self, - command: &str, - args: Option, - ) -> Result> { - if !self.running.load(Ordering::SeqCst) { - anyhow::bail!("Bridge not running"); - } - - let stream = self - .stream - .as_mut() - .ok_or_else(|| anyhow::anyhow!("No connection to bridge"))?; - - let request = BridgeRequest { - command: command.to_string(), - args, + if !ready { + let stderr_output = stderr_handle.join().unwrap_or_default(); + let detail = if !last_error.is_empty() { + format!(": {}", last_error) + } else if !stderr_output.is_empty() { + let last_stderr: Vec<_> = stderr_output.iter().rev().take(5).rev().cloned().collect(); + format!(": stderr: {}", last_stderr.join("\n")) + } else { + let last_stdout: Vec<_> = stdout_lines.iter().rev().take(10).rev().cloned().collect(); + format!("\nLast stdout:\n{}", last_stdout.join("\n")) }; - - let request_json = serde_json::to_string(&request)?; - debug!("Sending: {}", request_json); - - // Send request - check process health on I/O error - if let Err(e) = writeln!(stream, "{}", request_json) { - if !self.check_health() { - anyhow::bail!("Bridge process died unexpectedly"); + match child.try_wait() { + Ok(Some(status)) => { + anyhow::bail!("Ghidra process exited with status: {}{}", status, detail); } - return Err(e.into()); - } - if let Err(e) = stream.flush() { - if !self.check_health() { - anyhow::bail!("Bridge process died unexpectedly"); + Ok(None) => { + anyhow::bail!("Ghidra bridge did not send ready signal{}", detail); } - return Err(e.into()); - } - - // Read response - check process health on I/O error - let mut reader = BufReader::new(stream.try_clone()?); - let mut response_line = String::new(); - if let Err(e) = reader.read_line(&mut response_line) { - if !self.check_health() { - anyhow::bail!("Bridge process died unexpectedly"); + Err(e) => { + anyhow::bail!("Error checking process status: {}", e); } - return Err(e.into()); } - - debug!("Received: {}", response_line.trim()); - - let response: BridgeResponse = serde_json::from_str(&response_line)?; - Ok(response) } - /// Stop the bridge. - pub fn stop(&mut self) -> Result<()> { - if !self.running.load(Ordering::SeqCst) { - return Ok(()); + // Read port from port file + let port = read_port_file(project_path)? + .ok_or_else(|| anyhow::anyhow!("Port file not created by bridge"))?; + + info!("Ghidra bridge started on port {}", port); + Ok(port) +} + +/// Send a command to the bridge and return the response. +pub fn send_command( + port: u16, + command: &str, + args: Option, +) -> Result { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)) + .context("Failed to connect to bridge")?; + stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); + stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); + + let request = BridgeRequest { + command: command.to_string(), + args, + }; + + let request_json = serde_json::to_string(&request)?; + debug!("Sending: {}", request_json); + + writeln!(stream, "{}", request_json)?; + stream.flush()?; + + let mut reader = BufReader::new(&stream); + let mut response_line = String::new(); + reader.read_line(&mut response_line)?; + + debug!("Received: {}", response_line.trim()); + + let response: BridgeResponse = serde_json::from_str(&response_line)?; + + match response.status.as_str() { + "success" => { + Ok(response.data.unwrap_or(serde_json::json!({}))) } + "error" => { + let msg = response.message.unwrap_or_else(|| "Unknown error".to_string()); + anyhow::bail!("{}", msg) + } + "shutdown" => { + Ok(serde_json::json!({"status": "shutdown"})) + } + _ => { + Ok(response.data.unwrap_or(serde_json::json!({}))) + } + } +} - info!("Stopping Ghidra bridge..."); +/// Send a typed command to the bridge. +pub fn send_typed_command Deserialize<'de>>( + port: u16, + command: &str, + args: Option, +) -> Result> { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)) + .context("Failed to connect to bridge")?; + stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); + stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); - // Send shutdown command - if let Ok(response) = self.send_command::("shutdown", None) { + let request = BridgeRequest { + command: command.to_string(), + args, + }; + + let request_json = serde_json::to_string(&request)?; + debug!("Sending: {}", request_json); + + writeln!(stream, "{}", request_json)?; + stream.flush()?; + + let mut reader = BufReader::new(&stream); + let mut response_line = String::new(); + reader.read_line(&mut response_line)?; + + debug!("Received: {}", response_line.trim()); + + let response: BridgeResponse = serde_json::from_str(&response_line)?; + Ok(response) +} + +/// Stop the bridge for a project. +pub fn stop_bridge(project_path: &Path) -> Result<()> { + // Try graceful shutdown via TCP + if let Ok(Some(port)) = read_port_file(project_path) { + if let Ok(response) = send_command(port, "shutdown", None) { debug!("Shutdown response: {:?}", response); } + } - // Close stream - self.stream.take(); - - // Wait for child to exit - if let Some(mut child) = self.child.take() { - match child.wait_timeout(Duration::from_secs(10)) { - Ok(Some(status)) => { - info!("Ghidra process exited with status: {}", status); - } - Ok(None) => { - warn!("Ghidra process did not exit, killing..."); - child.kill().ok(); - } - Err(e) => { - error!("Error waiting for process: {}", e); - child.kill().ok(); - } + // If PID file exists, kill the process as fallback + if let Ok(Some(pid)) = read_pid_file(project_path) { + if is_pid_alive(pid) { + warn!("Killing bridge process {} as fallback", pid); + #[cfg(unix)] + unsafe { + libc::kill(pid as i32, libc::SIGTERM); + } + #[cfg(windows)] + { + let _ = std::process::Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .output(); } } - - self.running.store(false, Ordering::SeqCst); - info!("Bridge stopped"); - Ok(()) } - /// Check if the bridge is running. - pub fn is_running(&self) -> bool { - self.running.load(Ordering::SeqCst) - } + // Clean up files + cleanup_stale_files(project_path)?; - /// Check if the bridge process is actually healthy (still running). - /// - /// Performs an OS-level check on the child process to detect if it - /// has exited unexpectedly. If the process has died, updates the running - /// flag and returns false. - pub fn check_health(&mut self) -> bool { - if let Some(ref mut child) = self.child { - match child.try_wait() { - Ok(None) => true, // Process still running - Ok(Some(status)) => { - // Process has exited - warn!("Bridge process exited with status: {}", status); - self.running.store(false, Ordering::SeqCst); - false - } - Err(e) => { - // Error checking process - assume dead - error!("Error checking bridge process health: {}", e); - self.running.store(false, Ordering::SeqCst); - false - } - } - } else { - // No child process - self.running.store(false, Ordering::SeqCst); - false - } - } - - /// Get the embedded bridge script path, writing all scripts to disk. - fn get_bridge_script_path(&self) -> Result { - let scripts_dir = dirs::config_dir() - .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))? - .join("ghidra-cli") - .join("scripts"); - - std::fs::create_dir_all(&scripts_dir)?; - - // Write all embedded Python scripts - // Bridge and its module dependencies - let scripts: &[(&str, &str)] = &[ - ("bridge.py", include_str!("scripts/bridge.py")), - ("comments.py", include_str!("scripts/comments.py")), - ("symbols.py", include_str!("scripts/symbols.py")), - ("types.py", include_str!("scripts/types.py")), - ("graph.py", include_str!("scripts/graph.py")), - ("find.py", include_str!("scripts/find.py")), - ("diff.py", include_str!("scripts/diff.py")), - ("patch.py", include_str!("scripts/patch.py")), - ("disasm.py", include_str!("scripts/disasm.py")), - ("stats.py", include_str!("scripts/stats.py")), - ("program.py", include_str!("scripts/program.py")), - ("script_runner.py", include_str!("scripts/script_runner.py")), - ("batch.py", include_str!("scripts/batch.py")), - ]; - - for (name, content) in scripts { - std::fs::write(scripts_dir.join(name), content)?; - } - - Ok(scripts_dir.join("bridge.py")) - } - - /// Find the analyzeHeadless script. - fn find_headless_script(&self) -> Result { - // First try pyghidraRun for Ghidra 12+ (required for Python support) - #[cfg(unix)] - let pyghidra_name = "pyghidraRun"; - #[cfg(windows)] - let pyghidra_name = "pyghidraRun.bat"; - - let support_dir = self.ghidra_install_dir.join("support"); - let pyghidra_path = support_dir.join(pyghidra_name); - - if pyghidra_path.exists() { - return Ok(pyghidra_path); - } - - // Fall back to analyzeHeadless for older versions - #[cfg(unix)] - let script_name = "analyzeHeadless"; - #[cfg(windows)] - let script_name = "analyzeHeadless.bat"; - - let script_path = support_dir.join(script_name); - - if script_path.exists() { - Ok(script_path) - } else { - anyhow::bail!( - "Neither pyghidraRun nor analyzeHeadless found at: {}", - support_dir.display() - ) - } - } + info!("Bridge stopped"); + Ok(()) } -impl Drop for GhidraBridge { - fn drop(&mut self) { - if let Err(e) = self.stop() { - error!("Error stopping bridge on drop: {}", e); +/// Get bridge status for a project. +pub fn bridge_status(project_path: &Path) -> Result { + let port = read_port_file(project_path)?; + let pid = read_pid_file(project_path)?; + + if let (Some(port), Some(pid)) = (port, pid) { + if is_pid_alive(pid) { + if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { + return Ok(BridgeStatus::Running { port, pid }); + } } + // Stale files + cleanup_stale_files(project_path).ok(); + } + + Ok(BridgeStatus::Stopped) +} + +/// Bridge status +#[derive(Debug)] +pub enum BridgeStatus { + Running { port: u16, pid: u32 }, + Stopped, +} + +/// Find the analyzeHeadless script. +fn find_headless_script(ghidra_install_dir: &Path) -> Result { + let support_dir = ghidra_install_dir.join("support"); + + #[cfg(unix)] + let script_name = "analyzeHeadless"; + #[cfg(windows)] + let script_name = "analyzeHeadless.bat"; + + let script_path = support_dir.join(script_name); + + if script_path.exists() { + Ok(script_path) + } else { + anyhow::bail!( + "analyzeHeadless not found at: {}", + support_dir.display() + ) } } @@ -482,27 +512,3 @@ impl ChildExt for Child { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bridge_request_serialization() { - let req = BridgeRequest { - command: "list_functions".to_string(), - args: Some(serde_json::json!({"limit": 100})), - }; - let json = serde_json::to_string(&req).unwrap(); - assert!(json.contains("list_functions")); - assert!(json.contains("100")); - } - - #[test] - fn test_bridge_response_deserialization() { - let json = r#"{"status": "success", "data": {"count": 42}}"#; - let resp: BridgeResponse = serde_json::from_str(json).unwrap(); - assert_eq!(resp.status, "success"); - assert!(resp.data.is_some()); - } -} diff --git a/src/ghidra/scripts/GhidraCliBridge.java b/src/ghidra/scripts/GhidraCliBridge.java new file mode 100644 index 0000000..9e30df6 --- /dev/null +++ b/src/ghidra/scripts/GhidraCliBridge.java @@ -0,0 +1,2528 @@ +// Ghidra CLI Bridge - TCP socket server for CLI commands +// @category Bridge +// @keybinding +// @menupath Tools.Start CLI Bridge +// @toolbar +// +// Single-file GhidraScript that runs a persistent TCP server inside Ghidra +// to serve CLI commands. Replaces the Python bridge.py with a pure Java +// implementation using Ghidra's bundled Gson for JSON serialization. + +import ghidra.app.script.GhidraScript; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.app.util.importer.AutoImporter; +import ghidra.app.util.importer.MessageLog; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.DomainObject; +import ghidra.framework.model.Project; +import ghidra.framework.model.ProjectData; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressFactory; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.symbol.*; +import ghidra.util.task.ConsoleTaskMonitor; +import ghidra.util.task.TaskMonitor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + +import java.io.*; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Iterator; + +public class GhidraCliBridge extends GhidraScript { + + private Gson gson = new GsonBuilder().serializeNulls().create(); + + @Override + public void run() throws Exception { + // Get port file path from script arguments + String[] scriptArgs = getScriptArgs(); + if (scriptArgs.length < 1) { + printerr("Usage: GhidraCliBridge.java "); + return; + } + String portFilePath = scriptArgs[0]; + + // Bind to dynamic port on localhost only + ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); + int port = serverSocket.getLocalPort(); + + // Write port file + File portFile = new File(portFilePath); + portFile.getParentFile().mkdirs(); + try (PrintWriter pw = new PrintWriter(new FileWriter(portFile))) { + pw.println(port); + } + + // Write PID file + String pidFilePath = portFilePath.replaceAll("\\.port$", ".pid"); + File pidFile = new File(pidFilePath); + try (PrintWriter pw = new PrintWriter(new FileWriter(pidFile))) { + pw.println(ProcessHandle.current().pid()); + } + + // Signal ready to parent process + println("---GHIDRA_CLI_START---"); + JsonObject readyMsg = new JsonObject(); + readyMsg.addProperty("status", "ready"); + readyMsg.addProperty("port", port); + println(gson.toJson(readyMsg)); + println("---GHIDRA_CLI_END---"); + System.out.flush(); + + // Accept loop + boolean running = true; + while (running) { + try { + Socket client = serverSocket.accept(); + try ( + BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); + PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true) + ) { + String line; + while ((line = in.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + HandleResult result = handleRequest(line); + out.println(gson.toJson(result.response)); + out.flush(); + + if (result.shouldShutdown) { + running = false; + break; + } + } + } catch (IOException e) { + printerr("Client error: " + e.getMessage()); + } finally { + client.close(); + } + } catch (IOException e) { + if (running) { + printerr("Accept error: " + e.getMessage()); + } + } + } + + // Cleanup + serverSocket.close(); + portFile.delete(); + pidFile.delete(); + } + + // --- Request Handling --- + + private static class HandleResult { + JsonObject response; + boolean shouldShutdown; + + HandleResult(JsonObject response, boolean shouldShutdown) { + this.response = response; + this.shouldShutdown = shouldShutdown; + } + } + + private HandleResult handleRequest(String line) { + try { + JsonObject req = JsonParser.parseString(line).getAsJsonObject(); + String command = req.has("command") ? req.get("command").getAsString() : null; + JsonObject args = req.has("args") && !req.get("args").isJsonNull() + ? req.getAsJsonObject("args") : new JsonObject(); + + if ("shutdown".equals(command)) { + JsonObject resp = new JsonObject(); + resp.addProperty("status", "shutdown"); + return new HandleResult(resp, true); + } + + JsonObject result = dispatchCommand(command, args); + if (result == null) { + return new HandleResult(errorResponse("Unknown command: " + command), false); + } + + // Check if the handler returned an error + if (result.has("error")) { + return new HandleResult(errorResponse(result.get("error").getAsString()), false); + } + + return new HandleResult(successResponse(result), false); + + } catch (Exception e) { + return new HandleResult(errorResponse(e.getMessage()), false); + } + } + + private JsonObject dispatchCommand(String command, JsonObject args) { + if (command == null) return null; + switch (command) { + case "ping": return handlePing(); + case "program_info": return handleProgramInfo(); + case "list_functions": return handleListFunctions(args); + case "decompile": return handleDecompile(args); + case "list_strings": return handleListStrings(args); + case "list_imports": return handleListImports(); + case "list_exports": return handleListExports(); + case "memory_map": return handleMemoryMap(); + case "xrefs_to": return handleXrefsTo(args); + case "xrefs_from": return handleXrefsFrom(args); + case "import": return handleImport(args); + case "analyze": return handleAnalyze(args); + case "list_programs": return handleListPrograms(); + case "open_program": return handleOpenProgram(args); + case "program_close": return handleProgramClose(); + case "program_delete": return handleProgramDelete(args); + case "program_export": return handleProgramExport(args); + // Find commands + case "find_string": return handleFindString(args); + case "find_bytes": return handleFindBytes(args); + case "find_function": return handleFindFunction(args); + case "find_calls": return handleFindCalls(args); + case "find_crypto": return handleFindCrypto(); + case "find_interesting": return handleFindInteresting(); + // Symbol commands + case "symbol_list": return handleSymbolList(args); + case "symbol_get": return handleSymbolGet(args); + case "symbol_create": return handleSymbolCreate(args); + case "symbol_delete": return handleSymbolDelete(args); + case "symbol_rename": return handleSymbolRename(args); + // Type commands + case "type_list": return handleTypeList(); + case "type_get": return handleTypeGet(args); + case "type_create": return handleTypeCreate(args); + case "type_apply": return handleTypeApply(args); + // Comment commands + case "comment_list": return handleCommentList(); + case "comment_get": return handleCommentGet(args); + case "comment_set": return handleCommentSet(args); + case "comment_delete": return handleCommentDelete(args); + // Graph commands + case "graph_calls": return handleGraphCalls(args); + case "graph_callers": return handleGraphCallers(args); + case "graph_callees": return handleGraphCallees(args); + case "graph_export": return handleGraphExport(args); + // Diff commands + case "diff_programs": return handleDiffPrograms(args); + case "diff_functions": return handleDiffFunctions(args); + // Patch commands + case "patch_bytes": return handlePatchBytes(args); + case "patch_nop": return handlePatchNop(args); + case "patch_export": return handlePatchExport(args); + // Other commands + case "disasm": return handleDisasm(args); + case "stats": return handleStats(); + // Script commands + case "script_run": return handleScriptRun(args); + case "script_java": return handleScriptJava(args); + case "script_python": return handleScriptPython(args); + case "script_list": return handleScriptList(); + // Batch + case "batch": return handleBatch(args); + default: return null; + } + } + + // --- Response Helpers --- + + private JsonObject successResponse(JsonObject data) { + JsonObject resp = new JsonObject(); + resp.addProperty("status", "success"); + resp.add("data", data); + return resp; + } + + private JsonObject errorResponse(String message) { + JsonObject resp = new JsonObject(); + resp.addProperty("status", "error"); + resp.addProperty("message", message); + return resp; + } + + private JsonObject errorResult(String message) { + JsonObject result = new JsonObject(); + result.addProperty("error", message); + return result; + } + + // --- Address Resolution --- + + private Address resolveAddress(String addrStr) { + if (currentProgram == null || addrStr == null || addrStr.isEmpty()) { + return null; + } + + // Try as hex address first + Address addr = currentProgram.getAddressFactory().getAddress(addrStr); + if (addr != null) { + return addr; + } + + // Try as function name + FunctionManager fm = currentProgram.getFunctionManager(); + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + if (func.getName().equals(addrStr)) { + return func.getEntryPoint(); + } + } + + return null; + } + + // --- Helper to safely get string from JsonObject --- + + private String getArgString(JsonObject args, String key) { + if (args == null || !args.has(key) || args.get(key).isJsonNull()) return null; + return args.get(key).getAsString(); + } + + private int getArgInt(JsonObject args, String key, int defaultVal) { + if (args == null || !args.has(key) || args.get(key).isJsonNull()) return defaultVal; + return args.get(key).getAsInt(); + } + + private boolean getArgBool(JsonObject args, String key, boolean defaultVal) { + if (args == null || !args.has(key) || args.get(key).isJsonNull()) return defaultVal; + return args.get(key).getAsBoolean(); + } + + // --- Command Handlers (M1: Core) --- + + private JsonObject handlePing() { + JsonObject result = new JsonObject(); + result.addProperty("message", "pong"); + return result; + } + + private JsonObject handleProgramInfo() { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + JsonObject result = new JsonObject(); + result.addProperty("name", currentProgram.getName()); + result.addProperty("executable_path", currentProgram.getExecutablePath()); + result.addProperty("executable_format", currentProgram.getExecutableFormat()); + String compiler = currentProgram.getCompiler(); + if (compiler != null && !compiler.isEmpty()) { + result.addProperty("compiler", compiler); + } else { + result.add("compiler", JsonNull.INSTANCE); + } + result.addProperty("language", currentProgram.getLanguage().toString()); + result.addProperty("image_base", currentProgram.getImageBase().toString()); + result.addProperty("min_address", currentProgram.getMinAddress().toString()); + result.addProperty("max_address", currentProgram.getMaxAddress().toString()); + + FunctionManager fm = currentProgram.getFunctionManager(); + result.addProperty("function_count", fm.getFunctionCount()); + + return result; + } + + private JsonObject handleListFunctions(JsonObject args) { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + int limit = getArgInt(args, "limit", 0); + String nameFilter = getArgString(args, "filter"); + + JsonArray functions = new JsonArray(); + FunctionManager fm = currentProgram.getFunctionManager(); + int count = 0; + + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + if (limit > 0 && count >= limit) break; + + Function func = iter.next(); + String name = func.getName(); + + if (nameFilter != null && !name.toLowerCase().contains(nameFilter.toLowerCase())) { + continue; + } + + JsonObject funcData = new JsonObject(); + funcData.addProperty("name", name); + funcData.addProperty("address", func.getEntryPoint().toString()); + funcData.addProperty("size", func.getBody().getNumAddresses()); + funcData.addProperty("entry_point", func.getEntryPoint().toString()); + + String sig = null; + try { + sig = func.getPrototypeString(false, false); + } catch (Exception e) { + // ignore + } + if (sig != null) { + funcData.addProperty("signature", sig); + } else { + funcData.add("signature", JsonNull.INSTANCE); + } + + funcData.addProperty("calling_convention", func.getCallingConventionName()); + + String comment = func.getComment(); + if (comment != null) { + funcData.addProperty("comment", comment); + } else { + funcData.add("comment", JsonNull.INSTANCE); + } + + functions.add(funcData); + count++; + } + + JsonObject result = new JsonObject(); + result.add("functions", functions); + result.addProperty("count", functions.size()); + return result; + } + + private JsonObject handleDecompile(JsonObject args) { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + String addrStr = getArgString(args, "address"); + if (addrStr == null || addrStr.isEmpty()) { + return errorResult("No address provided"); + } + + Address addr = resolveAddress(addrStr); + if (addr == null) { + return errorResult("Cannot resolve address or function name: " + addrStr); + } + + FunctionManager fm = currentProgram.getFunctionManager(); + Function func = fm.getFunctionContaining(addr); + if (func == null) { + return errorResult("No function at address " + addrStr); + } + + DecompInterface decompiler = new DecompInterface(); + try { + decompiler.openProgram(currentProgram); + + TaskMonitor mon = new ConsoleTaskMonitor(); + DecompileResults results = decompiler.decompileFunction(func, 30, mon); + + if (results.decompileCompleted()) { + String code = results.getDecompiledFunction().getC(); + JsonObject result = new JsonObject(); + result.addProperty("name", func.getName()); + result.addProperty("address", func.getEntryPoint().toString()); + String sig = null; + try { + sig = func.getPrototypeString(false, false); + } catch (Exception e) { + // ignore + } + if (sig != null) { + result.addProperty("signature", sig); + } else { + result.add("signature", JsonNull.INSTANCE); + } + result.addProperty("code", code); + return result; + } else { + return errorResult("Decompilation failed"); + } + } finally { + decompiler.dispose(); + } + } + + private JsonObject handleListStrings(JsonObject args) { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + int limit = getArgInt(args, "limit", 0); + + JsonArray strings = new JsonArray(); + Listing listing = currentProgram.getListing(); + DataIterator dataIter = listing.getDefinedData(true); + int count = 0; + + while (dataIter.hasNext()) { + if (limit > 0 && count >= limit) break; + + Data data = dataIter.next(); + if (data.hasStringValue()) { + try { + String val = data.getValue().toString(); + JsonObject strData = new JsonObject(); + strData.addProperty("address", data.getAddress().toString()); + strData.addProperty("value", val); + strData.addProperty("length", val.length()); + strings.add(strData); + count++; + } catch (Exception e) { + // skip + } + } + } + + JsonObject result = new JsonObject(); + result.add("strings", strings); + result.addProperty("count", strings.size()); + return result; + } + + private JsonObject handleListImports() { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + JsonArray imports = new JsonArray(); + SymbolTable symbolTable = currentProgram.getSymbolTable(); + ExternalManager extMgr = currentProgram.getExternalManager(); + + SymbolIterator extSymbols = symbolTable.getExternalSymbols(); + while (extSymbols.hasNext()) { + Symbol symbol = extSymbols.next(); + ExternalLocation extLoc = extMgr.getExternalLocation(symbol); + if (extLoc != null) { + JsonObject importData = new JsonObject(); + importData.addProperty("name", symbol.getName()); + importData.addProperty("address", symbol.getAddress().toString()); + importData.addProperty("library", extLoc.getLibraryName()); + imports.add(importData); + } + } + + JsonObject result = new JsonObject(); + result.add("imports", imports); + result.addProperty("count", imports.size()); + return result; + } + + private JsonObject handleListExports() { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + JsonArray exports = new JsonArray(); + SymbolTable symbolTable = currentProgram.getSymbolTable(); + + SymbolIterator symIter = symbolTable.getSymbolIterator(); + while (symIter.hasNext()) { + Symbol symbol = symIter.next(); + if (symbol.isExternalEntryPoint()) { + JsonObject exportData = new JsonObject(); + exportData.addProperty("name", symbol.getName()); + exportData.addProperty("address", symbol.getAddress().toString()); + exports.add(exportData); + } + } + + JsonObject result = new JsonObject(); + result.add("exports", exports); + result.addProperty("count", exports.size()); + return result; + } + + private JsonObject handleMemoryMap() { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + JsonArray blocks = new JsonArray(); + Memory memory = currentProgram.getMemory(); + + for (MemoryBlock block : memory.getBlocks()) { + StringBuilder perms = new StringBuilder(); + if (block.isRead()) perms.append("r"); + if (block.isWrite()) perms.append("w"); + if (block.isExecute()) perms.append("x"); + + JsonObject blockData = new JsonObject(); + blockData.addProperty("name", block.getName()); + blockData.addProperty("start", block.getStart().toString()); + blockData.addProperty("end", block.getEnd().toString()); + blockData.addProperty("size", block.getSize()); + blockData.addProperty("permissions", perms.toString()); + blockData.addProperty("is_initialized", block.isInitialized()); + blockData.addProperty("is_loaded", block.isLoaded()); + blocks.add(blockData); + } + + JsonObject result = new JsonObject(); + result.add("blocks", blocks); + result.addProperty("count", blocks.size()); + return result; + } + + private JsonObject handleXrefsTo(JsonObject args) { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + String addrStr = getArgString(args, "address"); + if (addrStr == null || addrStr.isEmpty()) { + return errorResult("No address provided"); + } + + Address addr = resolveAddress(addrStr); + if (addr == null) { + return errorResult("Cannot resolve address or function name: " + addrStr); + } + + JsonArray xrefs = new JsonArray(); + ReferenceManager refMgr = currentProgram.getReferenceManager(); + FunctionManager fm = currentProgram.getFunctionManager(); + + Reference[] refs = refMgr.getReferencesTo(addr); + for (Reference ref : refs) { + Address fromAddr = ref.getFromAddress(); + Function fromFunc = fm.getFunctionContaining(fromAddr); + Function toFunc = fm.getFunctionContaining(addr); + + JsonObject xrefData = new JsonObject(); + xrefData.addProperty("from", fromAddr.toString()); + xrefData.addProperty("to", addr.toString()); + xrefData.addProperty("ref_type", ref.getReferenceType().toString()); + if (fromFunc != null) { + xrefData.addProperty("from_function", fromFunc.getName()); + } else { + xrefData.add("from_function", JsonNull.INSTANCE); + } + if (toFunc != null) { + xrefData.addProperty("to_function", toFunc.getName()); + } else { + xrefData.add("to_function", JsonNull.INSTANCE); + } + xrefs.add(xrefData); + } + + JsonObject result = new JsonObject(); + result.add("xrefs", xrefs); + result.addProperty("count", xrefs.size()); + return result; + } + + private JsonObject handleXrefsFrom(JsonObject args) { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + String addrStr = getArgString(args, "address"); + if (addrStr == null || addrStr.isEmpty()) { + return errorResult("No address provided"); + } + + Address addr = resolveAddress(addrStr); + if (addr == null) { + return errorResult("Cannot resolve address or function name: " + addrStr); + } + + JsonArray xrefs = new JsonArray(); + ReferenceManager refMgr = currentProgram.getReferenceManager(); + FunctionManager fm = currentProgram.getFunctionManager(); + + Reference[] refs = refMgr.getReferencesFrom(addr); + for (Reference ref : refs) { + Address toAddr = ref.getToAddress(); + Function fromFunc = fm.getFunctionContaining(addr); + Function toFunc = fm.getFunctionContaining(toAddr); + + JsonObject xrefData = new JsonObject(); + xrefData.addProperty("from", addr.toString()); + xrefData.addProperty("to", toAddr.toString()); + xrefData.addProperty("ref_type", ref.getReferenceType().toString()); + if (fromFunc != null) { + xrefData.addProperty("from_function", fromFunc.getName()); + } else { + xrefData.add("from_function", JsonNull.INSTANCE); + } + if (toFunc != null) { + xrefData.addProperty("to_function", toFunc.getName()); + } else { + xrefData.add("to_function", JsonNull.INSTANCE); + } + xrefs.add(xrefData); + } + + JsonObject result = new JsonObject(); + result.add("xrefs", xrefs); + result.addProperty("count", xrefs.size()); + return result; + } + + private JsonObject handleImport(JsonObject args) { + String binaryPath = getArgString(args, "binary_path"); + if (binaryPath == null || binaryPath.isEmpty()) { + return errorResult("No binary_path provided"); + } + + String programName = getArgString(args, "program"); + File binaryFile = new File(binaryPath); + if (programName == null || programName.isEmpty()) { + programName = binaryFile.getName(); + } + + Project project = state.getProject(); + if (project == null) { + return errorResult("No project open"); + } + + if (!binaryFile.exists()) { + return errorResult("Binary file not found: " + binaryPath); + } + + try { + TaskMonitor mon = new ConsoleTaskMonitor(); + MessageLog log = new MessageLog(); + Object consumer = project; + + // Ghidra 12+ API: importByUsingBestGuess(File, Project, String, Object, MessageLog, TaskMonitor) + Object loadResults = AutoImporter.importByUsingBestGuess( + binaryFile, project, "/", consumer, log, mon + ); + + if (loadResults == null) { + return errorResult("Failed to import binary"); + } + + // Save and release - loadResults is a LoadResults + // Use reflection to handle API differences across Ghidra versions + try { + java.lang.reflect.Method saveMethod = loadResults.getClass().getMethod("save", TaskMonitor.class); + // Actually it's per-loaded item; iterate + // LoadResults implements Iterable> + if (loadResults instanceof Iterable) { + for (Object loaded : (Iterable) loadResults) { + java.lang.reflect.Method saveMeth = loaded.getClass().getMethod("save", TaskMonitor.class); + saveMeth.invoke(loaded, mon); + } + } + java.lang.reflect.Method releaseMethod = loadResults.getClass().getMethod("release", Object.class); + releaseMethod.invoke(loadResults, consumer); + } catch (Exception reflectEx) { + // Fallback: try direct cast for older APIs + printerr("Import save warning: " + reflectEx.getMessage()); + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "success"); + result.addProperty("program", programName); + return result; + + } catch (Exception e) { + return errorResult("Import failed: " + e.getMessage()); + } + } + + private JsonObject handleAnalyze(JsonObject args) { + String programName = getArgString(args, "program"); + if (programName == null || programName.isEmpty()) { + return errorResult("No program name provided"); + } + + if (currentProgram == null) { + return errorResult("No program currently loaded"); + } + + // If requested program differs from current, switch to it + if (!currentProgram.getName().equals(programName)) { + JsonObject switchArgs = new JsonObject(); + switchArgs.addProperty("program", programName); + JsonObject switchResult = handleOpenProgram(switchArgs); + if (switchResult.has("error")) { + return switchResult; + } + } + + try { + // Try Ghidra 12+ import path first, fall back to older path + Class aamClass; + try { + aamClass = Class.forName("ghidra.app.plugin.core.analysis.AutoAnalysisManager"); + } catch (ClassNotFoundException e) { + aamClass = Class.forName("ghidra.app.cmd.analysis.AutoAnalysisManager"); + } + + java.lang.reflect.Method getManager = aamClass.getMethod("getAnalysisManager", ghidra.program.model.listing.Program.class); + Object autoMgr = getManager.invoke(null, currentProgram); + + if (autoMgr == null) { + return errorResult("Could not get AutoAnalysisManager"); + } + + TaskMonitor mon = new ConsoleTaskMonitor(); + + // Schedule full re-analysis + java.lang.reflect.Method reAnalyze = aamClass.getMethod("reAnalyzeAll", Address.class); + reAnalyze.invoke(autoMgr, (Address) null); + + java.lang.reflect.Method startAnalysis = aamClass.getMethod("startAnalysis", TaskMonitor.class); + startAnalysis.invoke(autoMgr, mon); + + // Poll until analysis completes + java.lang.reflect.Method isAnalyzing = aamClass.getMethod("isAnalyzing"); + while ((Boolean) isAnalyzing.invoke(autoMgr)) { + Thread.sleep(1000); + } + + // Save the program + try { + currentProgram.save("Analysis complete", mon); + } catch (Exception saveErr) { + // Best effort + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "success"); + result.addProperty("program", programName); + return result; + + } catch (Exception e) { + return errorResult("Analysis failed: " + e.getMessage()); + } + } + + private JsonObject handleListPrograms() { + Project project = state.getProject(); + if (project == null) { + return errorResult("No project open"); + } + + try { + ProjectData projectData = project.getProjectData(); + DomainFolder rootFolder = projectData.getRootFolder(); + JsonArray programs = new JsonArray(); + + for (DomainFile domainFile : rootFolder.getFiles()) { + boolean isCurrent = (currentProgram != null && + domainFile.getName().equals(currentProgram.getName())); + + JsonObject prog = new JsonObject(); + prog.addProperty("name", domainFile.getName()); + prog.addProperty("path", domainFile.getPathname()); + prog.addProperty("type", domainFile.getContentType()); + prog.addProperty("version", domainFile.getVersion()); + prog.addProperty("current", isCurrent); + programs.add(prog); + } + + JsonObject result = new JsonObject(); + result.add("programs", programs); + result.addProperty("count", programs.size()); + return result; + + } catch (Exception e) { + return errorResult("Failed to list programs: " + e.getMessage()); + } + } + + private JsonObject handleOpenProgram(JsonObject args) { + String programName = getArgString(args, "program"); + if (programName == null || programName.isEmpty()) { + return errorResult("Program name required"); + } + + // Already the current program? No-op. + if (currentProgram != null && currentProgram.getName().equals(programName)) { + JsonObject result = new JsonObject(); + result.addProperty("status", "success"); + result.addProperty("program", programName); + return result; + } + + Project project = state.getProject(); + if (project == null) { + return errorResult("No project open"); + } + + try { + ProjectData projectData = project.getProjectData(); + DomainFolder rootFolder = projectData.getRootFolder(); + + // Find the domain file by name + DomainFile domainFile = null; + for (DomainFile f : rootFolder.getFiles()) { + if (f.getName().equals(programName)) { + domainFile = f; + break; + } + } + + if (domainFile == null) { + // Try as a path + String path = programName.startsWith("/") ? programName : "/" + programName; + domainFile = projectData.getFile(path); + } + + if (domainFile == null) { + // Build list of available programs for error message + StringBuilder available = new StringBuilder(); + for (DomainFile f : rootFolder.getFiles()) { + if (available.length() > 0) available.append(", "); + available.append(f.getName()); + } + return errorResult("Program not found: " + programName + + ". Available: " + available.toString()); + } + + Object consumer = project; + TaskMonitor mon = new ConsoleTaskMonitor(); + + // Release current program if one is open + if (currentProgram != null) { + try { + currentProgram.save("Auto-save before switch", mon); + } catch (Exception e) { + // Best effort save + } + try { + currentProgram.release(consumer); + } catch (Exception e) { + // Best effort release + } + } + + // Open the requested program + DomainObject domObj = domainFile.getDomainObject(consumer, true, false, mon); + if (domObj instanceof ghidra.program.model.listing.Program) { + currentProgram = (ghidra.program.model.listing.Program) domObj; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "success"); + result.addProperty("program", currentProgram.getName()); + return result; + + } catch (Exception e) { + return errorResult("Failed to open program: " + e.getMessage()); + } + } + + private JsonObject handleProgramClose() { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + String programName = currentProgram.getName(); + + // In headless mode, we release the program + try { + Project project = state.getProject(); + if (project != null) { + currentProgram.release(project); + } + } catch (Exception e) { + // Best effort + } + + currentProgram = null; + + JsonObject result = new JsonObject(); + result.addProperty("status", "closed"); + result.addProperty("program", programName); + return result; + } + + private JsonObject handleProgramDelete(JsonObject args) { + String programName = getArgString(args, "program"); + if (programName == null || programName.isEmpty()) { + return errorResult("Program name required"); + } + + Project project = state.getProject(); + if (project == null) { + return errorResult("No project open"); + } + + try { + ProjectData projectData = project.getProjectData(); + String path = programName.startsWith("/") ? programName : "/" + programName; + DomainFile programFile = projectData.getFile(path); + + if (programFile == null) { + return errorResult("Program not found: " + programName); + } + + programFile.delete(); + + JsonObject result = new JsonObject(); + result.addProperty("status", "deleted"); + result.addProperty("program", programName); + return result; + + } catch (Exception e) { + return errorResult("Failed to delete program: " + e.getMessage()); + } + } + + private JsonObject handleProgramExport(JsonObject args) { + if (currentProgram == null) { + return errorResult("No program loaded"); + } + + String exportFormat = getArgString(args, "format"); + if (exportFormat == null) exportFormat = "json"; + String outputPath = getArgString(args, "output"); + + if ("json".equals(exportFormat)) { + // Get program info as base + JsonObject data = handleProgramInfo(); + if (data.has("error")) { + return data; + } + + // Add function list + FunctionManager fm = currentProgram.getFunctionManager(); + JsonArray functions = new JsonArray(); + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + JsonObject funcObj = new JsonObject(); + funcObj.addProperty("name", func.getName()); + funcObj.addProperty("address", func.getEntryPoint().toString()); + funcObj.addProperty("size", func.getBody().getNumAddresses()); + functions.add(funcObj); + } + data.add("functions", functions); + + if (outputPath != null && !outputPath.isEmpty()) { + try (PrintWriter pw = new PrintWriter(new FileWriter(outputPath))) { + Gson prettyGson = new GsonBuilder().setPrettyPrinting().create(); + pw.println(prettyGson.toJson(data)); + + JsonObject result = new JsonObject(); + result.addProperty("status", "exported"); + result.addProperty("format", "json"); + result.addProperty("output", outputPath); + return result; + } catch (IOException e) { + return errorResult("Failed to write file: " + e.getMessage()); + } + } else { + return data; + } + } else { + return errorResult("Unsupported export format: " + exportFormat); + } + } + + // ================================================================ + // M2: Extended Command Handlers + // ================================================================ + + // --- Find Handlers --- + + private JsonObject handleFindString(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String pattern = getArgString(args, "pattern"); + if (pattern == null) pattern = ""; + + try { + JsonArray results = new JsonArray(); + Listing listing = currentProgram.getListing(); + DataIterator dataIter = listing.getDefinedData(true); + + while (dataIter.hasNext()) { + Data data = dataIter.next(); + if (data.hasStringValue()) { + try { + String val = data.getValue().toString(); + if (pattern.isEmpty() || val.toLowerCase().contains(pattern.toLowerCase())) { + JsonObject item = new JsonObject(); + item.addProperty("address", data.getAddress().toString()); + item.addProperty("value", val); + item.addProperty("length", data.getLength()); + results.add(item); + } + } catch (Exception e) { /* skip */ } + } + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", results.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to find strings: " + e.getMessage()); + } + } + + private JsonObject handleFindBytes(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String hexPattern = getArgString(args, "hex"); + if (hexPattern == null || hexPattern.isEmpty()) { + return errorResult("No hex pattern provided"); + } + + try { + String hexClean = hexPattern.replace("0x", "").replace(" ", ""); + byte[] searchBytes = new byte[hexClean.length() / 2]; + for (int i = 0; i < searchBytes.length; i++) { + searchBytes[i] = (byte) Integer.parseInt(hexClean.substring(i * 2, i * 2 + 2), 16); + } + + Memory memory = currentProgram.getMemory(); + JsonArray results = new JsonArray(); + + Address addr = memory.getMinAddress(); + while (addr != null && results.size() < 100) { + Address found = memory.findBytes(addr, searchBytes, null, true, monitor); + if (found == null) break; + JsonObject item = new JsonObject(); + item.addProperty("address", found.toString()); + results.add(item); + addr = found.add(1); + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", results.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to find bytes: " + e.getMessage()); + } + } + + private JsonObject handleFindFunction(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String pattern = getArgString(args, "pattern"); + if (pattern == null) pattern = ""; + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + JsonArray results = new JsonArray(); + boolean isWildcard = pattern.contains("*"); + + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + String name = func.getName(); + boolean matches; + + if (isWildcard) { + // Simple wildcard matching: convert * to regex .* + String regex = pattern.replace(".", "\\.").replace("*", ".*"); + matches = name.matches(regex); + } else { + matches = name.toLowerCase().contains(pattern.toLowerCase()); + } + + if (matches) { + JsonObject item = new JsonObject(); + item.addProperty("name", name); + item.addProperty("address", func.getEntryPoint().toString()); + item.addProperty("size", func.getBody().getNumAddresses()); + results.add(item); + } + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", results.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to find functions: " + e.getMessage()); + } + } + + private JsonObject handleFindCalls(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String funcName = getArgString(args, "function"); + if (funcName == null || funcName.isEmpty()) { + return errorResult("No function name provided"); + } + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + Function targetFunc = null; + + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + if (func.getName().equals(funcName)) { + targetFunc = func; + break; + } + } + + if (targetFunc == null) { + return errorResult("Function not found: " + funcName); + } + + ReferenceManager refMgr = currentProgram.getReferenceManager(); + Address targetAddr = targetFunc.getEntryPoint(); + Reference[] refs = refMgr.getReferencesTo(targetAddr); + JsonArray results = new JsonArray(); + + for (Reference ref : refs) { + if (ref.getReferenceType().isCall()) { + Address fromAddr = ref.getFromAddress(); + Function callerFunc = fm.getFunctionContaining(fromAddr); + JsonObject item = new JsonObject(); + item.addProperty("address", fromAddr.toString()); + item.addProperty("caller", callerFunc != null ? callerFunc.getName() : "unknown"); + item.addProperty("type", ref.getReferenceType().toString()); + results.add(item); + } + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", results.size()); + result.addProperty("target", funcName); + return result; + } catch (Exception e) { + return errorResult("Failed to find calls: " + e.getMessage()); + } + } + + private JsonObject handleFindCrypto() { + if (currentProgram == null) return errorResult("No program loaded"); + + try { + Memory memory = currentProgram.getMemory(); + JsonArray results = new JsonArray(); + + String[][] cryptoPatterns = { + {"AES S-box", "637c777bf26b6fc53001672bfed7ab76"}, + {"SHA-256", "428a2f98d728ae227137449123ef65cd"}, + {"MD5", "d76aa478e8c7b756242070db01234567"} + }; + + for (String[] cp : cryptoPatterns) { + String name = cp[0]; + String hexPattern = cp[1]; + byte[] searchBytes = new byte[hexPattern.length() / 2]; + for (int i = 0; i < searchBytes.length; i++) { + searchBytes[i] = (byte) Integer.parseInt(hexPattern.substring(i * 2, i * 2 + 2), 16); + } + + Address addr = memory.getMinAddress(); + Address found = memory.findBytes(addr, searchBytes, null, true, monitor); + if (found != null) { + JsonObject item = new JsonObject(); + item.addProperty("type", name); + item.addProperty("address", found.toString()); + item.addProperty("pattern", hexPattern); + results.add(item); + } + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", results.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to find crypto: " + e.getMessage()); + } + } + + private JsonObject handleFindInteresting() { + if (currentProgram == null) return errorResult("No program loaded"); + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + ReferenceManager refMgr = currentProgram.getReferenceManager(); + List resultsList = new ArrayList<>(); + + String[] suspiciousNames = {"password", "key", "encrypt", "decrypt", "crypt", + "auth", "login", "admin", "secret"}; + + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + String funcName = func.getName(); + Address funcAddr = func.getEntryPoint(); + long funcSize = func.getBody().getNumAddresses(); + + Reference[] refs = refMgr.getReferencesTo(funcAddr); + int xrefCount = refs.length; + + JsonArray reasons = new JsonArray(); + + if (funcSize > 1000) { + reasons.add(new JsonPrimitive("large function (" + funcSize + " bytes)")); + } + if (xrefCount > 50) { + reasons.add(new JsonPrimitive("many xrefs (" + xrefCount + ")")); + } + for (String sus : suspiciousNames) { + if (funcName.toLowerCase().contains(sus)) { + reasons.add(new JsonPrimitive("suspicious name")); + break; + } + } + + if (reasons.size() > 0) { + JsonObject item = new JsonObject(); + item.addProperty("name", funcName); + item.addProperty("address", funcAddr.toString()); + item.addProperty("size", funcSize); + item.addProperty("xrefs", xrefCount); + item.add("reasons", reasons); + resultsList.add(item); + } + } + + // Sort by number of reasons (descending) + resultsList.sort((a, b) -> b.getAsJsonArray("reasons").size() - a.getAsJsonArray("reasons").size()); + + JsonArray results = new JsonArray(); + int limit = Math.min(50, resultsList.size()); + for (int i = 0; i < limit; i++) { + results.add(resultsList.get(i)); + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", resultsList.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to find interesting functions: " + e.getMessage()); + } + } + + // --- Symbol Handlers --- + + private JsonObject handleSymbolList(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String nameFilter = getArgString(args, "filter"); + + SymbolTable symbolTable = currentProgram.getSymbolTable(); + JsonArray symbols = new JsonArray(); + + SymbolIterator symIter = symbolTable.getAllSymbols(true); + while (symIter.hasNext()) { + Symbol symbol = symIter.next(); + String name = symbol.getName(); + + if (nameFilter != null && !name.toLowerCase().contains(nameFilter.toLowerCase())) { + continue; + } + + JsonObject symData = new JsonObject(); + symData.addProperty("name", name); + symData.addProperty("address", symbol.getAddress().toString()); + symData.addProperty("type", symbol.getSymbolType().toString()); + symData.addProperty("source", symbol.getSource().toString()); + symData.addProperty("is_primary", symbol.isPrimary()); + symbols.add(symData); + } + + JsonObject result = new JsonObject(); + result.add("symbols", symbols); + result.addProperty("count", symbols.size()); + return result; + } + + private JsonObject handleSymbolGet(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressOrName = getArgString(args, "name"); + if (addressOrName == null || addressOrName.isEmpty()) { + return errorResult("No symbol name or address provided"); + } + + SymbolTable symbolTable = currentProgram.getSymbolTable(); + + // Try as address first + boolean looksLikeAddress = addressOrName.startsWith("0x") || + addressOrName.chars().allMatch(c -> "0123456789abcdefABCDEF".indexOf(c) >= 0); + + if (looksLikeAddress) { + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressOrName); + if (addr != null) { + Symbol[] symbolsAtAddr = symbolTable.getSymbols(addr); + if (symbolsAtAddr.length == 0) { + return errorResult("No symbol at address: " + addressOrName); + } + JsonArray syms = new JsonArray(); + for (Symbol s : symbolsAtAddr) { + JsonObject symData = new JsonObject(); + symData.addProperty("name", s.getName()); + symData.addProperty("address", s.getAddress().toString()); + symData.addProperty("type", s.getSymbolType().toString()); + symData.addProperty("source", s.getSource().toString()); + syms.add(symData); + } + JsonObject result = new JsonObject(); + result.add("symbols", syms); + return result; + } + } catch (Exception e) { + // fall through to name lookup + } + } + + // Try as name + SymbolIterator symsByName = symbolTable.getSymbols(addressOrName); + JsonArray syms = new JsonArray(); + while (symsByName.hasNext()) { + Symbol s = symsByName.next(); + JsonObject symData = new JsonObject(); + symData.addProperty("name", s.getName()); + symData.addProperty("address", s.getAddress().toString()); + symData.addProperty("type", s.getSymbolType().toString()); + symData.addProperty("source", s.getSource().toString()); + syms.add(symData); + } + + if (syms.size() == 0) { + return errorResult("Symbol not found: " + addressOrName); + } + + JsonObject result = new JsonObject(); + result.add("symbols", syms); + return result; + } + + private JsonObject handleSymbolCreate(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + String name = getArgString(args, "name"); + if (addressStr == null || name == null) { + return errorResult("Address and name required"); + } + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + int txId = currentProgram.startTransaction("Create symbol"); + try { + SymbolTable symbolTable = currentProgram.getSymbolTable(); + symbolTable.createLabel(addr, name, SourceType.USER_DEFINED); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "created"); + result.addProperty("address", addressStr); + result.addProperty("name", name); + return result; + } catch (Exception e) { + return errorResult("Failed to create symbol: " + e.getMessage()); + } + } + + private JsonObject handleSymbolDelete(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String name = getArgString(args, "name"); + if (name == null) return errorResult("Symbol name required"); + + try { + SymbolTable symbolTable = currentProgram.getSymbolTable(); + SymbolIterator syms = symbolTable.getSymbols(name); + List toDelete = new ArrayList<>(); + while (syms.hasNext()) { + toDelete.add(syms.next()); + } + + if (toDelete.isEmpty()) { + return errorResult("Symbol not found: " + name); + } + + int txId = currentProgram.startTransaction("Delete symbol"); + try { + for (Symbol s : toDelete) { + s.delete(); + } + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "deleted"); + result.addProperty("name", name); + return result; + } catch (Exception e) { + return errorResult("Failed to delete symbol: " + e.getMessage()); + } + } + + private JsonObject handleSymbolRename(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String oldName = getArgString(args, "old_name"); + String newName = getArgString(args, "new_name"); + if (oldName == null || newName == null) { + return errorResult("old_name and new_name required"); + } + + try { + SymbolTable symbolTable = currentProgram.getSymbolTable(); + SymbolIterator syms = symbolTable.getSymbols(oldName); + List toRename = new ArrayList<>(); + while (syms.hasNext()) { + toRename.add(syms.next()); + } + + if (toRename.isEmpty()) { + return errorResult("Symbol not found: " + oldName); + } + + int txId = currentProgram.startTransaction("Rename symbol"); + try { + for (Symbol s : toRename) { + s.setName(newName, SourceType.USER_DEFINED); + } + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "renamed"); + result.addProperty("old_name", oldName); + result.addProperty("new_name", newName); + return result; + } catch (Exception e) { + return errorResult("Failed to rename symbol: " + e.getMessage()); + } + } + + // --- Type Handlers --- + + private JsonObject handleTypeList() { + if (currentProgram == null) return errorResult("No program loaded"); + + DataTypeManager dtm = currentProgram.getDataTypeManager(); + JsonArray types = new JsonArray(); + + Iterator dtIter = dtm.getAllDataTypes(); + while (dtIter.hasNext()) { + DataType dt = dtIter.next(); + 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); + } + + JsonObject result = new JsonObject(); + result.add("types", types); + result.addProperty("count", types.size()); + return result; + } + + private JsonObject handleTypeGet(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String typeName = getArgString(args, "name"); + if (typeName == null) return errorResult("Type name required"); + + DataTypeManager dtm = currentProgram.getDataTypeManager(); + + // Try by path first, then by name + DataType dataType = dtm.getDataType(typeName); + if (dataType == null) { + Iterator dtIter = dtm.getAllDataTypes(); + while (dtIter.hasNext()) { + DataType dt = dtIter.next(); + if (dt.getName().equals(typeName)) { + dataType = dt; + break; + } + } + } + + if (dataType == null) { + return errorResult("Type not found: " + typeName); + } + + JsonObject typeInfo = new JsonObject(); + typeInfo.addProperty("name", dataType.getName()); + typeInfo.addProperty("path", dataType.getPathName()); + typeInfo.addProperty("category", dataType.getCategoryPath().toString()); + typeInfo.addProperty("size", dataType.getLength()); + typeInfo.addProperty("description", dataType.getDescription()); + + if (dataType instanceof Structure) { + Structure struct = (Structure) dataType; + JsonArray components = new JsonArray(); + for (DataTypeComponent comp : struct.getComponents()) { + JsonObject compObj = new JsonObject(); + compObj.addProperty("name", comp.getFieldName()); + compObj.addProperty("type", comp.getDataType().getName()); + compObj.addProperty("offset", comp.getOffset()); + compObj.addProperty("size", comp.getLength()); + components.add(compObj); + } + typeInfo.add("components", components); + } else if (dataType instanceof Union) { + Union union = (Union) dataType; + JsonArray components = new JsonArray(); + for (DataTypeComponent comp : union.getComponents()) { + JsonObject compObj = new JsonObject(); + compObj.addProperty("name", comp.getFieldName()); + compObj.addProperty("type", comp.getDataType().getName()); + compObj.addProperty("offset", comp.getOffset()); + compObj.addProperty("size", comp.getLength()); + components.add(compObj); + } + typeInfo.add("components", components); + } + + return typeInfo; + } + + private JsonObject handleTypeCreate(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String typeName = getArgString(args, "definition"); + if (typeName == null) typeName = getArgString(args, "name"); + if (typeName == null) return errorResult("Type name required"); + + try { + DataTypeManager dtm = currentProgram.getDataTypeManager(); + int txId = currentProgram.startTransaction("Create type"); + try { + StructureDataType newStruct = new StructureDataType(typeName, 0); + dtm.addDataType(newStruct, null); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "created"); + result.addProperty("name", typeName); + return result; + } catch (Exception e) { + return errorResult("Failed to create type: " + e.getMessage()); + } + } + + private JsonObject handleTypeApply(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + String typeName = getArgString(args, "type_name"); + if (addressStr == null || typeName == null) { + return errorResult("Address and type_name required"); + } + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + DataTypeManager dtm = currentProgram.getDataTypeManager(); + DataType dataType = dtm.getDataType(typeName); + if (dataType == null) { + Iterator dtIter = dtm.getAllDataTypes(); + while (dtIter.hasNext()) { + DataType dt = dtIter.next(); + if (dt.getName().equals(typeName)) { + dataType = dt; + break; + } + } + } + if (dataType == null) { + return errorResult("Type not found: " + typeName); + } + + int txId = currentProgram.startTransaction("Apply type"); + try { + Listing listing = currentProgram.getListing(); + listing.createData(addr, dataType); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "applied"); + result.addProperty("address", addressStr); + result.addProperty("type", typeName); + return result; + } catch (Exception e) { + return errorResult("Failed to apply type: " + e.getMessage()); + } + } + + // --- Comment Handlers --- + + private int resolveCommentType(String typeStr) { + if (typeStr == null) return CodeUnit.EOL_COMMENT; + switch (typeStr.toUpperCase()) { + case "PRE": return CodeUnit.PRE_COMMENT; + case "POST": return CodeUnit.POST_COMMENT; + case "PLATE": return CodeUnit.PLATE_COMMENT; + default: return CodeUnit.EOL_COMMENT; + } + } + + private JsonObject handleCommentList() { + if (currentProgram == null) return errorResult("No program loaded"); + + Listing listing = currentProgram.getListing(); + Memory memory = currentProgram.getMemory(); + JsonArray comments = new JsonArray(); + + int[][] commentTypes = { + {CodeUnit.EOL_COMMENT}, + {CodeUnit.PRE_COMMENT}, + {CodeUnit.POST_COMMENT}, + {CodeUnit.PLATE_COMMENT} + }; + String[] commentNames = {"EOL", "PRE", "POST", "PLATE"}; + + for (MemoryBlock block : memory.getBlocks()) { + ghidra.program.model.address.AddressSet addrSet = + new ghidra.program.model.address.AddressSet(block.getStart(), block.getEnd()); + + ghidra.program.model.address.AddressIterator addrIter = + listing.getCommentAddressIterator(addrSet, true); + + while (addrIter.hasNext()) { + Address addr = addrIter.next(); + CodeUnit cu = listing.getCodeUnitAt(addr); + if (cu == null) continue; + + for (int i = 0; i < commentNames.length; i++) { + String text = cu.getComment(commentTypes[i][0]); + if (text != null) { + JsonObject commentObj = new JsonObject(); + commentObj.addProperty("address", addr.toString()); + commentObj.addProperty("type", commentNames[i]); + commentObj.addProperty("text", text); + comments.add(commentObj); + } + } + } + } + + JsonObject result = new JsonObject(); + result.add("comments", comments); + result.addProperty("count", comments.size()); + return result; + } + + private JsonObject handleCommentGet(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + if (addressStr == null) return errorResult("Address required"); + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + Listing listing = currentProgram.getListing(); + CodeUnit cu = listing.getCodeUnitAt(addr); + if (cu == null) return errorResult("No code unit at address: " + addressStr); + + int[] types = {CodeUnit.EOL_COMMENT, CodeUnit.PRE_COMMENT, CodeUnit.POST_COMMENT, CodeUnit.PLATE_COMMENT}; + String[] names = {"EOL", "PRE", "POST", "PLATE"}; + + JsonArray comments = new JsonArray(); + for (int i = 0; i < types.length; i++) { + String text = cu.getComment(types[i]); + if (text != null) { + JsonObject commentObj = new JsonObject(); + commentObj.addProperty("type", names[i]); + commentObj.addProperty("text", text); + comments.add(commentObj); + } + } + + JsonObject result = new JsonObject(); + result.addProperty("address", addressStr); + result.add("comments", comments); + return result; + } catch (Exception e) { + return errorResult("Failed to get comments: " + e.getMessage()); + } + } + + private JsonObject handleCommentSet(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + String text = getArgString(args, "text"); + String commentTypeStr = getArgString(args, "comment_type"); + if (commentTypeStr == null) commentTypeStr = "EOL"; + + if (addressStr == null) return errorResult("Address required"); + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + Set validTypes = new HashSet<>(Arrays.asList("EOL", "PRE", "POST", "PLATE")); + if (!validTypes.contains(commentTypeStr.toUpperCase())) { + return errorResult("Invalid comment type: " + commentTypeStr + ". Must be one of: EOL, PRE, POST, PLATE"); + } + + int commentType = resolveCommentType(commentTypeStr); + Listing listing = currentProgram.getListing(); + + int txId = currentProgram.startTransaction("Set comment"); + try { + listing.setComment(addr, commentType, text); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "set"); + result.addProperty("address", addressStr); + return result; + } catch (Exception e) { + return errorResult("Failed to set comment: " + e.getMessage()); + } + } + + private JsonObject handleCommentDelete(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + if (addressStr == null) return errorResult("Address required"); + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + Listing listing = currentProgram.getListing(); + + int txId = currentProgram.startTransaction("Delete comments"); + try { + listing.setComment(addr, CodeUnit.EOL_COMMENT, null); + listing.setComment(addr, CodeUnit.PRE_COMMENT, null); + listing.setComment(addr, CodeUnit.POST_COMMENT, null); + listing.setComment(addr, CodeUnit.PLATE_COMMENT, null); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "deleted"); + result.addProperty("address", addressStr); + return result; + } catch (Exception e) { + return errorResult("Failed to delete comment: " + e.getMessage()); + } + } + + // --- Graph Handlers --- + + private JsonObject handleGraphCalls(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + int limit = getArgInt(args, "limit", 0); + + FunctionManager fm = currentProgram.getFunctionManager(); + ReferenceManager refMgr = currentProgram.getReferenceManager(); + JsonArray nodes = new JsonArray(); + JsonArray edges = new JsonArray(); + int count = 0; + + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + if (limit > 0 && count >= limit) break; + Function func = iter.next(); + String funcAddr = func.getEntryPoint().toString(); + + JsonObject node = new JsonObject(); + node.addProperty("id", funcAddr); + node.addProperty("name", func.getName()); + node.addProperty("address", funcAddr); + nodes.add(node); + + Reference[] refs = refMgr.getReferencesFrom(func.getEntryPoint()); + for (Reference ref : refs) { + if (ref.getReferenceType().isCall()) { + Address targetAddr = ref.getToAddress(); + Function targetFunc = fm.getFunctionAt(targetAddr); + if (targetFunc != null) { + JsonObject edge = new JsonObject(); + edge.addProperty("from", funcAddr); + edge.addProperty("to", targetAddr.toString()); + edge.addProperty("type", "call"); + edges.add(edge); + } + } + } + count++; + } + + JsonObject result = new JsonObject(); + result.add("nodes", nodes); + result.add("edges", edges); + result.addProperty("node_count", nodes.size()); + result.addProperty("edge_count", edges.size()); + return result; + } + + private Function findFunctionByNameOrAddress(String nameOrAddr) { + FunctionManager fm = currentProgram.getFunctionManager(); + + // Try as address + boolean looksLikeAddr = nameOrAddr.startsWith("0x") || + nameOrAddr.chars().allMatch(c -> "0123456789abcdefABCDEF".indexOf(c) >= 0); + if (looksLikeAddr) { + Address addr = currentProgram.getAddressFactory().getAddress(nameOrAddr); + if (addr != null) { + Function f = fm.getFunctionAt(addr); + if (f != null) return f; + } + } + + // Try as name + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + if (func.getName().equals(nameOrAddr)) return func; + } + return null; + } + + private JsonObject handleGraphCallers(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String funcName = getArgString(args, "function"); + if (funcName == null) return errorResult("Function name required"); + int depth = getArgInt(args, "depth", 1); + + Function targetFunc = findFunctionByNameOrAddress(funcName); + if (targetFunc == null) return errorResult("Function not found: " + funcName); + + ReferenceManager refMgr = currentProgram.getReferenceManager(); + FunctionManager fm = currentProgram.getFunctionManager(); + JsonArray callers = new JsonArray(); + Set visited = new HashSet<>(); + + findCallersRecursive(targetFunc, 0, depth, callers, visited, refMgr, fm); + + JsonObject result = new JsonObject(); + result.addProperty("function", funcName); + result.add("callers", callers); + result.addProperty("count", callers.size()); + return result; + } + + private void findCallersRecursive(Function func, int currentDepth, int maxDepth, + JsonArray callers, Set visited, ReferenceManager refMgr, FunctionManager fm) { + if (maxDepth > 0 && currentDepth >= maxDepth) return; + String funcAddrStr = func.getEntryPoint().toString(); + if (visited.contains(funcAddrStr)) return; + visited.add(funcAddrStr); + + Reference[] refs = refMgr.getReferencesTo(func.getEntryPoint()); + for (Reference ref : refs) { + if (ref.getReferenceType().isCall()) { + Address fromAddr = ref.getFromAddress(); + Function callerFunc = fm.getFunctionContaining(fromAddr); + if (callerFunc != null) { + JsonObject callerInfo = new JsonObject(); + callerInfo.addProperty("name", callerFunc.getName()); + callerInfo.addProperty("address", callerFunc.getEntryPoint().toString()); + callerInfo.addProperty("call_site", fromAddr.toString()); + callerInfo.addProperty("depth", currentDepth); + callers.add(callerInfo); + + if (maxDepth == 0 || currentDepth + 1 < maxDepth) { + findCallersRecursive(callerFunc, currentDepth + 1, maxDepth, callers, visited, refMgr, fm); + } + } + } + } + } + + private JsonObject handleGraphCallees(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String funcName = getArgString(args, "function"); + if (funcName == null) return errorResult("Function name required"); + int depth = getArgInt(args, "depth", 1); + + Function targetFunc = findFunctionByNameOrAddress(funcName); + if (targetFunc == null) return errorResult("Function not found: " + funcName); + + ReferenceManager refMgr = currentProgram.getReferenceManager(); + FunctionManager fm = currentProgram.getFunctionManager(); + JsonArray callees = new JsonArray(); + Set visited = new HashSet<>(); + + findCalleesRecursive(targetFunc, 0, depth, callees, visited, refMgr, fm); + + JsonObject result = new JsonObject(); + result.addProperty("function", funcName); + result.add("callees", callees); + result.addProperty("count", callees.size()); + return result; + } + + private void findCalleesRecursive(Function func, int currentDepth, int maxDepth, + JsonArray callees, Set visited, ReferenceManager refMgr, FunctionManager fm) { + if (maxDepth > 0 && currentDepth >= maxDepth) return; + String funcAddrStr = func.getEntryPoint().toString(); + if (visited.contains(funcAddrStr)) return; + visited.add(funcAddrStr); + + Reference[] refs = refMgr.getReferencesFrom(func.getEntryPoint()); + for (Reference ref : refs) { + if (ref.getReferenceType().isCall()) { + Address toAddr = ref.getToAddress(); + Function calleeFunc = fm.getFunctionAt(toAddr); + if (calleeFunc != null) { + JsonObject calleeInfo = new JsonObject(); + calleeInfo.addProperty("name", calleeFunc.getName()); + calleeInfo.addProperty("address", calleeFunc.getEntryPoint().toString()); + calleeInfo.addProperty("call_site", ref.getFromAddress().toString()); + calleeInfo.addProperty("depth", currentDepth); + callees.add(calleeInfo); + + if (maxDepth == 0 || currentDepth + 1 < maxDepth) { + findCalleesRecursive(calleeFunc, currentDepth + 1, maxDepth, callees, visited, refMgr, fm); + } + } + } + } + } + + private JsonObject handleGraphExport(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String format = getArgString(args, "format"); + if (format == null) format = "json"; + + // Build graph first + JsonObject graphData = handleGraphCalls(new JsonObject()); + if (graphData.has("error")) return graphData; + + if ("json".equals(format)) { + return graphData; + } else if ("dot".equals(format)) { + StringBuilder sb = new StringBuilder(); + sb.append("digraph CallGraph {\n"); + sb.append(" rankdir=LR;\n"); + sb.append(" node [shape=box];\n"); + + JsonArray nodes = graphData.getAsJsonArray("nodes"); + for (int i = 0; i < nodes.size(); i++) { + JsonObject node = nodes.get(i).getAsJsonObject(); + String nodeId = node.get("id").getAsString().replace(":", "_"); + String label = node.get("name").getAsString(); + sb.append(" \"").append(nodeId).append("\" [label=\"").append(label).append("\"];\n"); + } + + JsonArray edges = graphData.getAsJsonArray("edges"); + for (int i = 0; i < edges.size(); i++) { + JsonObject edge = edges.get(i).getAsJsonObject(); + String fromId = edge.get("from").getAsString().replace(":", "_"); + String toId = edge.get("to").getAsString().replace(":", "_"); + sb.append(" \"").append(fromId).append("\" -> \"").append(toId).append("\";\n"); + } + + sb.append("}"); + + JsonObject result = new JsonObject(); + result.addProperty("format", "dot"); + result.addProperty("output", sb.toString()); + return result; + } else { + return errorResult("Unsupported format: " + format); + } + } + + // --- Diff Handlers --- + + private JsonObject handleDiffPrograms(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String prog1 = getArgString(args, "program1"); + String prog2 = getArgString(args, "program2"); + if (prog1 == null) prog1 = ""; + if (prog2 == null) prog2 = ""; + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + Memory memory = currentProgram.getMemory(); + SymbolTable symbolTable = currentProgram.getSymbolTable(); + + JsonObject prog1Stats = new JsonObject(); + prog1Stats.addProperty("name", prog1); + prog1Stats.addProperty("function_count", fm.getFunctionCount()); + prog1Stats.addProperty("memory_size", memory.getSize()); + prog1Stats.addProperty("symbol_count", symbolTable.getNumSymbols()); + + JsonArray memBlocks = new JsonArray(); + for (MemoryBlock block : memory.getBlocks()) { + JsonObject blockObj = new JsonObject(); + blockObj.addProperty("name", block.getName()); + blockObj.addProperty("start", block.getStart().toString()); + blockObj.addProperty("end", block.getEnd().toString()); + blockObj.addProperty("size", block.getSize()); + memBlocks.add(blockObj); + } + prog1Stats.add("memory_blocks", memBlocks); + + JsonObject prog2Stats = new JsonObject(); + prog2Stats.addProperty("name", prog2); + prog2Stats.addProperty("note", "Comparison requires loading second program"); + + JsonObject result = new JsonObject(); + result.add("program1", prog1Stats); + result.add("program2", prog2Stats); + result.addProperty("status", "partial"); + result.addProperty("message", "Single program stats returned (multi-program comparison not implemented)"); + return result; + } catch (Exception e) { + return errorResult("Failed to diff programs: " + e.getMessage()); + } + } + + private JsonObject handleDiffFunctions(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String func1Name = getArgString(args, "func1"); + String func2Name = getArgString(args, "func2"); + if (func1Name == null || func2Name == null) { + return errorResult("func1 and func2 required"); + } + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + Function func1 = null, func2 = null; + + FunctionIterator iter = fm.getFunctions(true); + while (iter.hasNext()) { + Function f = iter.next(); + if (f.getName().equals(func1Name)) func1 = f; + if (f.getName().equals(func2Name)) func2 = f; + } + + if (func1 == null) return errorResult("Function not found: " + func1Name); + if (func2 == null) return errorResult("Function not found: " + func2Name); + + DecompInterface decompiler = new DecompInterface(); + try { + decompiler.openProgram(currentProgram); + TaskMonitor mon = new ConsoleTaskMonitor(); + + DecompileResults res1 = decompiler.decompileFunction(func1, 30, mon); + DecompileResults res2 = decompiler.decompileFunction(func2, 30, mon); + + if (!res1.decompileCompleted()) return errorResult("Failed to decompile " + func1Name); + if (!res2.decompileCompleted()) return errorResult("Failed to decompile " + func2Name); + + String code1 = res1.getDecompiledFunction().getC(); + String code2 = res2.getDecompiledFunction().getC(); + + String[] lines1 = code1.split("\n"); + String[] lines2 = code2.split("\n"); + + JsonArray diffLines = new JsonArray(); + int maxLines = Math.max(lines1.length, lines2.length); + for (int i = 0; i < maxLines; i++) { + String l1 = i < lines1.length ? lines1[i] : ""; + String l2 = i < lines2.length ? lines2[i] : ""; + if (!l1.equals(l2)) { + JsonObject diff = new JsonObject(); + diff.addProperty("line", i + 1); + diff.addProperty("func1", l1); + diff.addProperty("func2", l2); + diff.addProperty("status", "changed"); + diffLines.add(diff); + } + } + + JsonObject f1Info = new JsonObject(); + f1Info.addProperty("name", func1Name); + f1Info.addProperty("lines", lines1.length); + f1Info.addProperty("code", code1); + + JsonObject f2Info = new JsonObject(); + f2Info.addProperty("name", func2Name); + f2Info.addProperty("lines", lines2.length); + f2Info.addProperty("code", code2); + + JsonObject result = new JsonObject(); + result.add("func1", f1Info); + result.add("func2", f2Info); + result.add("differences", diffLines); + result.addProperty("diff_count", diffLines.size()); + return result; + } finally { + decompiler.dispose(); + } + } catch (Exception e) { + return errorResult("Failed to diff functions: " + e.getMessage()); + } + } + + // --- Patch Handlers --- + + private JsonObject handlePatchBytes(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + String hexData = getArgString(args, "hex"); + if (addressStr == null || hexData == null) { + return errorResult("Address and hex data required"); + } + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + String hexClean = hexData.replace("0x", "").replace(" ", ""); + byte[] patchData = new byte[hexClean.length() / 2]; + for (int i = 0; i < patchData.length; i++) { + patchData[i] = (byte) Integer.parseInt(hexClean.substring(i * 2, i * 2 + 2), 16); + } + + Memory memory = currentProgram.getMemory(); + int txId = currentProgram.startTransaction("Patch bytes"); + try { + memory.setBytes(addr, patchData); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "patched"); + result.addProperty("address", addr.toString()); + result.addProperty("bytes", patchData.length); + return result; + } catch (Exception e) { + return errorResult("Failed to patch bytes: " + e.getMessage()); + } + } + + private JsonObject handlePatchNop(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + if (addressStr == null) return errorResult("Address required"); + + try { + Address addr = currentProgram.getAddressFactory().getAddress(addressStr); + if (addr == null) return errorResult("Invalid address: " + addressStr); + + Listing listing = currentProgram.getListing(); + Instruction instruction = listing.getInstructionAt(addr); + if (instruction == null) { + return errorResult("No instruction at address: " + addressStr); + } + + int instrLength = instruction.getLength(); + String processor = currentProgram.getLanguage().getProcessor().toString(); + + byte nopByte; + if (processor.toLowerCase().contains("x86")) { + nopByte = (byte) 0x90; + } else { + nopByte = (byte) 0x00; + } + + byte[] nopBytes = new byte[instrLength]; + Arrays.fill(nopBytes, nopByte); + + Memory memory = currentProgram.getMemory(); + int txId = currentProgram.startTransaction("NOP instruction"); + try { + memory.setBytes(addr, nopBytes); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "nopped"); + result.addProperty("address", addr.toString()); + result.addProperty("bytes", instrLength); + return result; + } catch (Exception e) { + return errorResult("Failed to NOP instruction: " + e.getMessage()); + } + } + + private JsonObject handlePatchExport(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String outputPath = getArgString(args, "output"); + if (outputPath == null || outputPath.isEmpty()) { + return errorResult("Output path required"); + } + + try { + // Use reflection to access BinaryExporter which may not always be available + Class exporterClass = Class.forName("ghidra.app.util.exporter.BinaryExporter"); + Object exporter = exporterClass.getDeclaredConstructor().newInstance(); + + java.lang.reflect.Method exportMethod = exporterClass.getMethod("export", + File.class, ghidra.program.model.listing.Program.class, + ghidra.program.model.address.AddressSetView.class, TaskMonitor.class); + + File outputFile = new File(outputPath); + TaskMonitor mon = new ConsoleTaskMonitor(); + exportMethod.invoke(exporter, outputFile, currentProgram, null, mon); + + JsonObject result = new JsonObject(); + result.addProperty("status", "exported"); + result.addProperty("output", outputPath); + return result; + } catch (Exception e) { + return errorResult("Failed to export binary: " + e.getMessage()); + } + } + + // --- Disasm Handler --- + + private JsonObject handleDisasm(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addressStr = getArgString(args, "address"); + int count = getArgInt(args, "count", 10); + + if (addressStr == null || addressStr.isEmpty()) { + return errorResult("Address required"); + } + + 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); + } + if (addr == null) return errorResult("Invalid address: " + addressStr); + + Listing listing = currentProgram.getListing(); + Instruction instruction = listing.getInstructionAt(addr); + if (instruction == null) { + return errorResult("No instruction at address: " + addressStr); + } + + JsonArray results = new JsonArray(); + Instruction current = instruction; + + for (int i = 0; i < count && current != null; i++) { + Address instrAddr = current.getAddress(); + byte[] byteArray = current.getBytes(); + StringBuilder bytesHex = new StringBuilder(); + for (byte b : byteArray) { + bytesHex.append(String.format("%02x", b & 0xff)); + } + + String mnemonic = current.getMnemonicString(); + JsonArray operands = new JsonArray(); + int numOperands = current.getNumOperands(); + for (int j = 0; j < numOperands; j++) { + operands.add(new JsonPrimitive(current.getDefaultOperandRepresentation(j))); + } + + JsonObject instrData = new JsonObject(); + instrData.addProperty("address", instrAddr.toString()); + instrData.addProperty("bytes", bytesHex.toString()); + instrData.addProperty("mnemonic", mnemonic); + instrData.add("operands", operands); + results.add(instrData); + + current = current.getNext(); + } + + JsonObject result = new JsonObject(); + result.add("results", results); + result.addProperty("count", results.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to disassemble: " + e.getMessage()); + } + } + + // --- Stats Handler --- + + private JsonObject handleStats() { + if (currentProgram == null) return errorResult("No program loaded"); + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + SymbolTable symbolTable = currentProgram.getSymbolTable(); + Memory memory = currentProgram.getMemory(); + DataTypeManager dtm = currentProgram.getDataTypeManager(); + Listing listing = currentProgram.getListing(); + + int functionCount = fm.getFunctionCount(); + + int symbolCount = 0; + SymbolIterator symIter = symbolTable.getAllSymbols(true); + while (symIter.hasNext()) { symIter.next(); symbolCount++; } + + int stringCount = 0; + DataIterator dataIter = listing.getDefinedData(true); + while (dataIter.hasNext()) { + if (dataIter.next().hasStringValue()) stringCount++; + } + + long memorySize = 0; + int sectionCount = 0; + for (MemoryBlock block : memory.getBlocks()) { + memorySize += block.getSize(); + sectionCount++; + } + + int importCount = 0; + SymbolIterator extSyms = symbolTable.getExternalSymbols(); + while (extSyms.hasNext()) { extSyms.next(); importCount++; } + + int exportCount = 0; + ghidra.program.model.address.AddressIterator epIter = symbolTable.getExternalEntryPointIterator(); + while (epIter.hasNext()) { epIter.next(); exportCount++; } + + int dataTypeCount = dtm.getDataTypeCount(false); + + int instructionCount = 0; + InstructionIterator instrIter = listing.getInstructions(true); + while (instrIter.hasNext()) { instrIter.next(); instructionCount++; } + + JsonObject stats = new JsonObject(); + stats.addProperty("functions", functionCount); + stats.addProperty("symbols", symbolCount); + stats.addProperty("strings", stringCount); + stats.addProperty("imports", importCount); + stats.addProperty("exports", exportCount); + stats.addProperty("memory_size", memorySize); + stats.addProperty("sections", sectionCount); + stats.addProperty("data_types", dataTypeCount); + stats.addProperty("instructions", instructionCount); + stats.addProperty("program_name", currentProgram.getName()); + stats.addProperty("executable_format", currentProgram.getExecutableFormat()); + String compiler = currentProgram.getCompiler(); + stats.addProperty("compiler", (compiler != null && !compiler.isEmpty()) ? compiler : "Unknown"); + + JsonObject result = new JsonObject(); + result.add("stats", stats); + return result; + } catch (Exception e) { + return errorResult("Failed to gather statistics: " + e.getMessage()); + } + } + + // --- Script Handlers --- + + private JsonObject handleScriptRun(JsonObject args) { + String scriptPath = getArgString(args, "path"); + if (scriptPath == null) return errorResult("Script path required"); + + try { + File scriptFile = new File(scriptPath); + if (!scriptFile.exists()) return errorResult("Script not found: " + scriptPath); + + // Use GhidraScript's runScript method + runScript(scriptPath); + + JsonObject result = new JsonObject(); + result.addProperty("status", "executed"); + result.addProperty("script", scriptPath); + return result; + } catch (Exception e) { + return errorResult("Failed to run script: " + e.getMessage()); + } + } + + private JsonObject handleScriptJava(JsonObject args) { + return errorResult("Inline Java execution not supported in bridge mode"); + } + + private JsonObject handleScriptPython(JsonObject args) { + return errorResult("Python execution not available (Java bridge replaces Python bridge)"); + } + + private JsonObject handleScriptList() { + try { + JsonArray scripts = new JsonArray(); + + // List scripts from Ghidra's script directories + Class utilClass = Class.forName("ghidra.app.script.GhidraScriptUtil"); + java.lang.reflect.Method getDirs = utilClass.getMethod("getScriptSourceDirectories"); + Object dirs = getDirs.invoke(null); + + if (dirs instanceof Iterable) { + for (Object dirObj : (Iterable) dirs) { + File dir = new File(dirObj.toString()); + if (dir.exists() && dir.isDirectory()) { + for (File f : dir.listFiles()) { + if (f.getName().endsWith(".py") || f.getName().endsWith(".java")) { + JsonObject scriptObj = new JsonObject(); + scriptObj.addProperty("name", f.getName()); + scriptObj.addProperty("path", f.getAbsolutePath()); + scriptObj.addProperty("type", f.getName().endsWith(".py") ? "python" : "java"); + scripts.add(scriptObj); + } + } + } + } + } + + JsonObject result = new JsonObject(); + result.add("scripts", scripts); + result.addProperty("count", scripts.size()); + return result; + } catch (Exception e) { + return errorResult("Failed to list scripts: " + e.getMessage()); + } + } + + // --- Batch Handler --- + + private JsonObject handleBatch(JsonObject args) { + // Batch operations are handled by the Rust side, not the bridge directly + return errorResult("Batch operations are handled by the CLI, not via bridge script"); + } +} diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 7156ab5..12a9a2d 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -1,204 +1,328 @@ -//! CLI-side IPC client for communicating with the daemon. +//! Bridge client for direct communication with the Java bridge. +//! +//! Connects directly to the Java GhidraCliBridge via TCP. +//! No intermediate daemon process is needed. -#![allow(dead_code)] +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; +use std::time::Duration; -use std::path::Path; +use anyhow::Result; +use serde_json::json; +use tracing::debug; -use anyhow::{Context, Result}; -use tokio::io::{ReadHalf, WriteHalf}; +use super::protocol::{BridgeRequest, BridgeResponse}; -use super::protocol::{Command, Request, Response}; -use super::transport::{self, Stream}; - -/// Client for communicating with the Ghidra daemon. -pub struct DaemonClient { - reader: ReadHalf, - writer: WriteHalf, - next_id: u64, +/// Client for communicating with the Ghidra Java bridge. +pub struct BridgeClient { + port: u16, } -impl DaemonClient { - /// Connect to the running daemon for a specific project. - pub async fn connect(project_path: &Path) -> Result { - let stream = transport::connect_for_project(project_path) - .await - .map_err(|e| { - if e.kind() == std::io::ErrorKind::NotFound - || e.kind() == std::io::ErrorKind::ConnectionRefused - { - anyhow::anyhow!("Daemon not running for project: {}", project_path.display()) - } else { - anyhow::anyhow!("Failed to connect to daemon: {}", e) - } - })?; - - let (reader, writer) = tokio::io::split(stream); - - Ok(Self { - reader, - writer, - next_id: 1, - }) +impl BridgeClient { + /// Create a client for a known port. + pub fn new(port: u16) -> Self { + Self { port } } - /// Send a command and wait for the response. - pub async fn send_command(&mut self, command: Command) -> Result { - let id = self.next_id; - self.next_id += 1; + /// Get the port this client connects to. + pub fn port(&self) -> u16 { + self.port + } - let request = Request::new(id, command); - let json = serde_json::to_vec(&request).context("Failed to serialize request")?; + /// Send a command to the bridge and return the result. + pub fn send_command( + &self, + command: &str, + args: Option, + ) -> Result { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", self.port)) + .map_err(|e| anyhow::anyhow!("Failed to connect to bridge on port {}: {}", self.port, e))?; + stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); + stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); - transport::send_message(&mut self.writer, &json) - .await - .context("Failed to send message to daemon")?; + let request = BridgeRequest { + command: command.to_string(), + args, + }; - let response_data = transport::recv_message(&mut self.reader) - .await - .context("Failed to receive message from daemon")?; + let request_json = serde_json::to_string(&request)?; + debug!("Sending: {}", request_json); - let response: Response = - serde_json::from_slice(&response_data).context("Failed to parse daemon response")?; + writeln!(stream, "{}", request_json)?; + stream.flush()?; - if response.id != id { - anyhow::bail!("Response ID mismatch: expected {}, got {}", id, response.id); - } + let mut reader = BufReader::new(&stream); + let mut response_line = String::new(); + reader.read_line(&mut response_line)?; - if response.success { - Ok(response.result.unwrap_or(serde_json::json!({}))) - } else { - let error = response - .error - .unwrap_or_else(|| "Unknown error".to_string()); - anyhow::bail!("{}", error) + debug!("Received: {}", response_line.trim()); + + let response: BridgeResponse = serde_json::from_str(&response_line)?; + + match response.status.as_str() { + "success" => Ok(response.data.unwrap_or(json!({}))), + "error" => { + let msg = response.message.unwrap_or_else(|| "Unknown error".to_string()); + anyhow::bail!("{}", msg) + } + "shutdown" => Ok(json!({"status": "shutdown"})), + _ => Ok(response.data.unwrap_or(json!({}))), } } - /// Check if daemon is responding. - pub async fn ping(&mut self) -> Result { - match self.send_command(Command::Ping).await { + /// Check if bridge is responding. + pub fn ping(&self) -> Result { + match self.send_command("ping", None) { Ok(_) => Ok(true), - Err(e) if e.to_string().contains("not running") => Ok(false), - Err(e) => Err(e), + Err(_) => Ok(false), } } - /// Get daemon status. - pub async fn status(&mut self) -> Result { - self.send_command(Command::Status).await - } - - /// Shutdown the daemon. - pub async fn shutdown(&mut self) -> Result<()> { - self.send_command(Command::Shutdown).await?; + /// Shutdown the bridge. + pub fn shutdown(&self) -> Result<()> { + self.send_command("shutdown", None)?; Ok(()) } - /// Clear the result cache. - pub async fn clear_cache(&mut self) -> Result<()> { - self.send_command(Command::ClearCache).await?; - Ok(()) + /// Get bridge status. + pub fn status(&self) -> Result { + self.send_command("status", None) } /// List functions. - pub async fn list_functions( - &mut self, + pub fn list_functions( + &self, limit: Option, filter: Option, ) -> Result { - self.send_command(Command::ListFunctions { limit, filter }) - .await + self.send_command( + "list_functions", + Some(json!({"limit": limit, "filter": filter})), + ) } /// Decompile a function. - pub async fn decompile(&mut self, address: String) -> Result { - self.send_command(Command::Decompile { address }).await + pub fn decompile(&self, address: String) -> Result { + self.send_command("decompile", Some(json!({"address": address}))) } /// List strings. - pub async fn list_strings(&mut self, limit: Option) -> Result { - self.send_command(Command::ListStrings { limit }).await + pub fn list_strings(&self, limit: Option) -> Result { + self.send_command("list_strings", Some(json!({"limit": limit}))) } /// List imports. - pub async fn list_imports(&mut self) -> Result { - self.send_command(Command::ListImports).await + pub fn list_imports(&self) -> Result { + self.send_command("list_imports", None) } /// List exports. - pub async fn list_exports(&mut self) -> Result { - self.send_command(Command::ListExports).await + pub fn list_exports(&self) -> Result { + self.send_command("list_exports", None) } /// Get memory map. - pub async fn memory_map(&mut self) -> Result { - self.send_command(Command::MemoryMap).await + pub fn memory_map(&self) -> Result { + self.send_command("memory_map", None) } /// Get program info. - pub async fn program_info(&mut self) -> Result { - self.send_command(Command::ProgramInfo).await + pub fn program_info(&self) -> Result { + self.send_command("program_info", None) } /// Get cross-references to an address. - pub async fn xrefs_to(&mut self, address: String) -> Result { - self.send_command(Command::XRefsTo { address }).await + pub fn xrefs_to(&self, address: String) -> Result { + self.send_command("xrefs_to", Some(json!({"address": address}))) } /// Get cross-references from an address. - pub async fn xrefs_from(&mut self, address: String) -> Result { - self.send_command(Command::XRefsFrom { address }).await + pub fn xrefs_from(&self, address: String) -> Result { + self.send_command("xrefs_from", Some(json!({"address": address}))) } - /// Execute a CLI command through the daemon (takes pre-serialized JSON). - pub async fn execute_cli_json(&mut self, command_json: String) -> Result { - self.send_command(Command::ExecuteCli { command_json }) - .await - } - - /// Import a binary into a project. - pub async fn import_binary( - &mut self, + /// Import a binary. + pub fn import_binary( + &self, binary_path: &str, - project: &str, program: Option<&str>, ) -> Result { - self.send_command(Command::Import { - binary_path: binary_path.to_string(), - project: project.to_string(), - program: program.map(|s| s.to_string()), - }) - .await + self.send_command( + "import", + Some(json!({"binary_path": binary_path, "program": program})), + ) } - /// List all programs in the project. - pub async fn list_programs(&mut self) -> Result { - self.send_command(Command::ListPrograms).await + /// Analyze the current program. + pub fn analyze(&self) -> Result { + self.send_command("analyze", None) } - /// Open/switch to a program in the project. - pub async fn open_program(&mut self, program: &str) -> Result { - self.send_command(Command::OpenProgram { - program: program.to_string(), - }) - .await + /// List programs in the project. + pub fn list_programs(&self) -> Result { + self.send_command("list_programs", None) } - /// Analyze a program in a project. - pub async fn analyze_program( - &mut self, - project: &str, - program: &str, - ) -> Result { - self.send_command(Command::Analyze { - project: project.to_string(), - program: program.to_string(), - }) - .await + /// Open/switch to a program. + pub fn open_program(&self, program: &str) -> Result { + self.send_command("open_program", Some(json!({"program": program}))) + } + + // === Extended commands (symbols, types, comments, etc.) === + + pub fn symbol_list(&self, filter: Option<&str>) -> Result { + self.send_command("symbol_list", Some(json!({"filter": filter}))) + } + + pub fn symbol_get(&self, name: &str) -> Result { + self.send_command("symbol_get", Some(json!({"name": name}))) + } + + pub fn symbol_create(&self, address: &str, name: &str) -> Result { + self.send_command("symbol_create", Some(json!({"address": address, "name": name}))) + } + + pub fn symbol_delete(&self, name: &str) -> Result { + self.send_command("symbol_delete", Some(json!({"name": name}))) + } + + pub fn symbol_rename(&self, old_name: &str, new_name: &str) -> Result { + self.send_command("symbol_rename", Some(json!({"old_name": old_name, "new_name": new_name}))) + } + + pub fn type_list(&self) -> Result { + self.send_command("type_list", None) + } + + pub fn type_get(&self, name: &str) -> Result { + self.send_command("type_get", Some(json!({"name": name}))) + } + + pub fn type_create(&self, definition: &str) -> Result { + self.send_command("type_create", Some(json!({"definition": definition}))) + } + + pub fn type_apply(&self, address: &str, type_name: &str) -> Result { + self.send_command("type_apply", Some(json!({"address": address, "type_name": type_name}))) + } + + pub fn comment_list(&self) -> Result { + self.send_command("comment_list", None) + } + + pub fn comment_get(&self, address: &str) -> Result { + self.send_command("comment_get", Some(json!({"address": address}))) + } + + pub fn comment_set(&self, address: &str, text: &str, comment_type: Option<&str>) -> Result { + self.send_command("comment_set", Some(json!({ + "address": address, + "text": text, + "type": comment_type, + }))) + } + + pub fn comment_delete(&self, address: &str) -> Result { + self.send_command("comment_delete", Some(json!({"address": address}))) + } + + pub fn graph_calls(&self, limit: Option) -> Result { + self.send_command("graph_calls", Some(json!({"limit": limit}))) + } + + pub fn graph_callers(&self, function: &str, depth: Option) -> Result { + self.send_command("graph_callers", Some(json!({"function": function, "depth": depth}))) + } + + pub fn graph_callees(&self, function: &str, depth: Option) -> Result { + self.send_command("graph_callees", Some(json!({"function": function, "depth": depth}))) + } + + pub fn graph_export(&self, format: &str) -> Result { + self.send_command("graph_export", Some(json!({"format": format}))) + } + + pub fn find_string(&self, pattern: &str) -> Result { + self.send_command("find_string", Some(json!({"pattern": pattern}))) + } + + pub fn find_bytes(&self, hex: &str) -> Result { + self.send_command("find_bytes", Some(json!({"hex": hex}))) + } + + pub fn find_function(&self, pattern: &str) -> Result { + self.send_command("find_function", Some(json!({"pattern": pattern}))) + } + + pub fn find_calls(&self, function: &str) -> Result { + self.send_command("find_calls", Some(json!({"function": function}))) + } + + pub fn find_crypto(&self) -> Result { + self.send_command("find_crypto", None) + } + + pub fn find_interesting(&self) -> Result { + self.send_command("find_interesting", None) + } + + pub fn diff_programs(&self, program1: &str, program2: &str) -> Result { + self.send_command("diff_programs", Some(json!({"program1": program1, "program2": program2}))) + } + + pub fn diff_functions(&self, func1: &str, func2: &str) -> Result { + self.send_command("diff_functions", Some(json!({"func1": func1, "func2": func2}))) + } + + pub fn patch_bytes(&self, address: &str, hex: &str) -> Result { + self.send_command("patch_bytes", Some(json!({"address": address, "hex": hex}))) + } + + pub fn patch_nop(&self, address: &str) -> Result { + self.send_command("patch_nop", Some(json!({"address": address}))) + } + + pub fn patch_export(&self, output: &str) -> Result { + self.send_command("patch_export", Some(json!({"output": output}))) + } + + pub fn disasm(&self, address: &str, num_instructions: Option) -> Result { + self.send_command("disasm", Some(json!({"address": address, "count": num_instructions}))) + } + + pub fn stats(&self) -> Result { + self.send_command("stats", None) + } + + pub fn script_run(&self, script_path: &str, args: &[String]) -> Result { + self.send_command("script_run", Some(json!({"path": script_path, "args": args}))) + } + + pub fn script_python(&self, code: &str) -> Result { + self.send_command("script_python", Some(json!({"code": code}))) + } + + pub fn script_java(&self, code: &str) -> Result { + self.send_command("script_java", Some(json!({"code": code}))) + } + + pub fn script_list(&self) -> Result { + self.send_command("script_list", None) + } + + pub fn batch(&self, commands: &[serde_json::Value]) -> Result { + self.send_command("batch", Some(json!({"commands": commands}))) + } + + pub fn program_close(&self) -> Result { + self.send_command("close_program", None) + } + + pub fn program_delete(&self, program: &str) -> Result { + self.send_command("delete_program", Some(json!({"program": program}))) + } + + pub fn program_export(&self, format: &str, output: Option<&str>) -> Result { + self.send_command("export_program", Some(json!({"format": format, "output": output}))) } } - -/// Check if daemon is running for a specific project (without establishing a full connection). -pub fn daemon_available(project_path: &Path) -> bool { - transport::socket_exists_for_project(project_path) -} diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index 08f9f39..96f8544 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -1,18 +1,8 @@ -//! IPC module for CLI-to-daemon communication. +//! IPC module for CLI-to-bridge communication. //! -//! This module provides cross-platform IPC using local sockets: -//! - Unix domain sockets on Linux/macOS -//! - Named pipes on Windows -//! -//! Follows the pattern from debugger-cli for reliable length-prefixed -//! JSON message framing. +//! Provides direct TCP communication to the Java GhidraCliBridge. +//! No intermediate daemon process is needed. pub mod client; pub mod protocol; pub mod transport; - -// Re-export for external use -#[allow(unused_imports)] -pub use client::DaemonClient; -#[allow(unused_imports)] -pub use protocol::{Command, Request, Response}; diff --git a/src/ipc/protocol.rs b/src/ipc/protocol.rs index de6f59e..e0bff8e 100644 --- a/src/ipc/protocol.rs +++ b/src/ipc/protocol.rs @@ -1,154 +1,25 @@ -//! IPC protocol message types. +//! IPC protocol types for bridge communication. //! -//! Defines the request/response format for CLI ↔ daemon communication. -//! Uses a typed command enum (not wrapping CLI Commands) for clean separation. - -#![allow(dead_code)] +//! Defines the request/response format for CLI ↔ Java bridge communication. +//! Uses simple JSON: {"command":"...", "args":{...}} → {"status":"...", "data":{...}} use serde::{Deserialize, Serialize}; -/// IPC request from CLI to daemon. -#[derive(Debug, Serialize, Deserialize)] -pub struct Request { - /// Request ID for matching responses - pub id: u64, - /// The command to execute - pub command: Command, -} - -impl Request { - /// Create a new request with the given ID and command. - pub fn new(id: u64, command: Command) -> Self { - Self { id, command } - } -} - -/// IPC response from daemon to CLI. -#[derive(Debug, Serialize, Deserialize)] -pub struct Response { - /// Request ID this response corresponds to - pub id: u64, - /// Whether the command succeeded - pub success: bool, - /// Result data on success +/// Request to the Java bridge. +#[derive(Debug, Serialize)] +pub struct BridgeRequest { + pub command: String, #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Error message on failure - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, + pub args: Option, } -impl Response { - /// Create a success response. - pub fn success(id: u64, result: serde_json::Value) -> Self { - Self { - id, - success: true, - result: Some(result), - error: None, - } - } - - /// Create an error response. - pub fn error(id: u64, message: impl Into) -> Self { - Self { - id, - success: false, - result: None, - error: Some(message.into()), - } - } - - /// Create a success response with no data. - pub fn ok(id: u64) -> Self { - Self { - id, - success: true, - result: Some(serde_json::json!({})), - error: None, - } - } -} - -/// Commands that can be sent from CLI to daemon. -/// -/// These are separate from CLI Commands to decouple IPC from argument parsing. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum Command { - // === Data Queries === - /// List functions in the program - ListFunctions { - #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, - #[serde(skip_serializing_if = "Option::is_none")] - filter: Option, - }, - - /// Decompile a function at address - Decompile { address: String }, - - /// List strings in the program - ListStrings { - #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, - }, - - /// List imports - ListImports, - - /// List exports - ListExports, - - /// Get memory map - MemoryMap, - - /// Get program info - ProgramInfo, - - /// Get cross-references to an address - XRefsTo { address: String }, - - /// Get cross-references from an address - XRefsFrom { address: String }, - - // === Project Management === - /// Import a binary into a project - Import { - binary_path: String, - project: String, - #[serde(skip_serializing_if = "Option::is_none")] - program: Option, - }, - - /// Analyze a program in a project - Analyze { project: String, program: String }, - - /// List all programs in the project - ListPrograms, - - /// Open/switch to a program in the project - OpenProgram { program: String }, - - // === Session Management === - /// Health check - Ping, - - /// Get daemon status - Status, - - /// Clear result cache - ClearCache, - - /// Shutdown the daemon - Shutdown, - - // === Generic CLI Command Forwarding === - /// Execute a CLI command through the daemon's queue - ExecuteCli { - /// The serialized CLI command - command_json: String, - }, +/// Response from the Java bridge. +#[derive(Debug, Deserialize)] +pub struct BridgeResponse { + pub status: String, + pub data: Option, + #[serde(default)] + pub message: Option, } #[cfg(test)] @@ -157,37 +28,39 @@ mod tests { #[test] fn test_request_serialization() { - let request = Request::new(1, Command::Ping); - let json = serde_json::to_string(&request).unwrap(); - let deserialized: Request = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.id, 1); - assert!(matches!(deserialized.command, Command::Ping)); - } - - #[test] - fn test_response_success() { - let response = Response::success(1, serde_json::json!({"count": 42})); - assert!(response.success); - assert_eq!(response.id, 1); - assert!(response.result.is_some()); - } - - #[test] - fn test_response_error() { - let response = Response::error(1, "Something went wrong"); - assert!(!response.success); - assert_eq!(response.error.as_ref().unwrap(), "Something went wrong"); - } - - #[test] - fn test_command_serialization() { - let cmd = Command::ListFunctions { - limit: Some(100), - filter: Some("main".to_string()), + let request = BridgeRequest { + command: "ping".to_string(), + args: None, }; - let json = serde_json::to_string(&cmd).unwrap(); + let json = serde_json::to_string(&request).unwrap(); + assert!(json.contains("ping")); + assert!(!json.contains("args")); + } + + #[test] + fn test_request_with_args() { + let request = BridgeRequest { + command: "list_functions".to_string(), + args: Some(serde_json::json!({"limit": 100})), + }; + let json = serde_json::to_string(&request).unwrap(); assert!(json.contains("list_functions")); assert!(json.contains("100")); - assert!(json.contains("main")); + } + + #[test] + fn test_response_deserialization() { + let json = r#"{"status":"success","data":{"count":42}}"#; + let response: BridgeResponse = serde_json::from_str(json).unwrap(); + assert_eq!(response.status, "success"); + assert!(response.data.is_some()); + } + + #[test] + fn test_error_response() { + let json = r#"{"status":"error","message":"Something went wrong"}"#; + let response: BridgeResponse = serde_json::from_str(json).unwrap(); + assert_eq!(response.status, "error"); + assert_eq!(response.message.as_ref().unwrap(), "Something went wrong"); } } diff --git a/src/ipc/transport.rs b/src/ipc/transport.rs index ba01a4f..36b140f 100644 --- a/src/ipc/transport.rs +++ b/src/ipc/transport.rs @@ -1,238 +1,13 @@ -//! Cross-platform IPC transport layer. +//! Transport helpers for bridge TCP communication. //! -//! Abstracts Unix domain sockets (Unix/macOS) and named pipes (Windows) -//! using the interprocess crate. Uses length-prefixed message framing. -//! -//! Socket paths are per-project to allow concurrent daemons for different -//! projects without conflicts. Socket names use MD5 hash of the project path. +//! The Java bridge uses newline-delimited JSON over TCP. +//! This module provides minimal transport utilities. -#![allow(dead_code)] +use std::net::TcpStream; -use std::io; -use std::path::{Path, PathBuf}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -/// Maximum message size (10 MB) -const MAX_MESSAGE_SIZE: u32 = 10 * 1024 * 1024; - -/// Socket name prefix for the daemon -const SOCKET_PREFIX: &str = "ghidra-cli"; - -// Platform-specific imports and type aliases -#[cfg(unix)] -pub mod platform { - pub use interprocess::local_socket::tokio::{prelude::*, Listener, Stream}; - pub use interprocess::local_socket::{GenericFilePath, ListenerOptions}; -} - -#[cfg(windows)] -pub mod platform { - pub use interprocess::local_socket::tokio::{prelude::*, Listener, Stream}; - pub use interprocess::local_socket::{GenericNamespaced, ListenerOptions}; -} - -pub use platform::*; - -/// Compute MD5 hash of project path for socket naming. -/// Uses same hashing approach as lock files for consistency. -fn project_hash(project_path: &Path) -> String { - format!( - "{:x}", - md5::compute(project_path.to_string_lossy().as_bytes()) - ) -} - -/// Get the socket directory path. -fn socket_dir() -> io::Result { - #[cfg(unix)] - { - // Use XDG runtime dir or /tmp - let runtime_dir = std::env::var("XDG_RUNTIME_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| std::env::temp_dir()); - let dir = runtime_dir.join("ghidra-cli"); - Ok(dir) - } - - #[cfg(windows)] - { - // Windows named pipes don't need a directory - Ok(PathBuf::new()) - } -} - -/// Get the socket path for a specific project. -/// -/// Checks GHIDRA_CLI_SOCKET env var first (used for testing), then falls back to -/// project-specific socket using MD5 hash of project path. -pub fn socket_path_for_project(project_path: &Path) -> io::Result { - if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") { - return Ok(PathBuf::from(path)); - } - let dir = socket_dir()?; - let hash = project_hash(project_path); - Ok(dir.join(format!("{}-{}.sock", SOCKET_PREFIX, hash))) -} - -/// Get the socket name for interprocess, for a specific project. -/// -/// On Unix, respects GHIDRA_CLI_SOCKET env var for test isolation. -pub fn socket_name_for_project(project_path: &Path) -> String { - #[cfg(unix)] - { - // Check env var first (used for testing) - if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") { - return path; - } - socket_path_for_project(project_path) - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| { - let hash = project_hash(project_path); - format!("/tmp/ghidra-cli/{}-{}.sock", SOCKET_PREFIX, hash) - }) - } - - #[cfg(windows)] - { - // Windows uses named pipe namespace with project hash - let hash = project_hash(project_path); - format!("{}-{}", SOCKET_PREFIX, hash) - } -} - -/// Ensure the socket directory exists. -pub fn ensure_socket_dir() -> io::Result<()> { - #[cfg(unix)] - { - let dir = socket_dir()?; - if !dir.exists() { - std::fs::create_dir_all(&dir)?; - } - Ok(()) - } - - #[cfg(windows)] - { - Ok(()) - } -} - -/// Remove the socket file for a specific project if it exists. -pub fn remove_socket_for_project(project_path: &Path) -> io::Result<()> { - #[cfg(unix)] - { - let path = socket_path_for_project(project_path)?; - if path.exists() { - std::fs::remove_file(&path)?; - } - Ok(()) - } - - #[cfg(windows)] - { - let _ = project_path; // unused on Windows - Ok(()) - } -} - -/// Check if the socket for a specific project exists. -pub fn socket_exists_for_project(project_path: &Path) -> bool { - #[cfg(unix)] - { - socket_path_for_project(project_path) - .map(|p| p.exists()) - .unwrap_or(false) - } - - #[cfg(windows)] - { - // On Windows, we can't easily check if a named pipe exists - // We'll rely on connection attempts instead - let _ = project_path; - true - } -} - -/// Create a listener for incoming IPC connections for a specific project. -pub async fn create_listener_for_project(project_path: &Path) -> io::Result { - // Ensure socket directory exists and clean up stale socket - ensure_socket_dir()?; - remove_socket_for_project(project_path)?; - - let name = socket_name_for_project(project_path); - - #[cfg(unix)] - let listener = { - let name = name.to_fs_name::()?; - ListenerOptions::new().name(name).create_tokio()? - }; - - #[cfg(windows)] - let listener = { - let name = name.to_ns_name::()?; - ListenerOptions::new().name(name).create_tokio()? - }; - - // Set socket permissions on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let path = socket_path_for_project(project_path)?; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; - } - - Ok(listener) -} - -/// Connect to the daemon's IPC socket for a specific project. -pub async fn connect_for_project(project_path: &Path) -> io::Result { - let name = socket_name_for_project(project_path); - - #[cfg(unix)] - let stream = { - let name = name.to_fs_name::()?; - Stream::connect(name).await? - }; - - #[cfg(windows)] - let stream = { - let name = name.to_ns_name::()?; - Stream::connect(name).await? - }; - - Ok(stream) -} - -/// Send a length-prefixed message. -pub async fn send_message(writer: &mut W, data: &[u8]) -> io::Result<()> { - if data.len() > MAX_MESSAGE_SIZE as usize { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Message too large", - )); - } - - let len = data.len() as u32; - writer.write_all(&len.to_le_bytes()).await?; - writer.write_all(data).await?; - writer.flush().await?; - Ok(()) -} - -/// Receive a length-prefixed message. -pub async fn recv_message(reader: &mut R) -> io::Result> { - let mut len_buf = [0u8; 4]; - reader.read_exact(&mut len_buf).await?; - let len = u32::from_le_bytes(len_buf); - - if len > MAX_MESSAGE_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Message too large: {} bytes", len), - )); - } - - let mut data = vec![0u8; len as usize]; - reader.read_exact(&mut data).await?; - Ok(data) +/// Check if a TCP port is reachable on localhost. +pub fn port_reachable(port: u16) -> bool { + TcpStream::connect(format!("127.0.0.1:{}", port)) + .map(|_| true) + .unwrap_or(false) } diff --git a/src/main.rs b/src/main.rs index 4d9f0eb..052b32e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,18 +9,18 @@ mod ipc; mod query; use clap::Parser; -use cli::{Cli, Commands, DaemonCommands, SetupArgs}; +use cli::{Cli, Commands, DaemonCommands}; use config::Config; -use daemon::process::{ensure_not_running, get_data_dir, get_running_daemon_info}; -use daemon::{run as run_daemon, DaemonConfig}; use error::{GhidraError, Result}; use format::{auto_detect_format, DefaultFormatter, Formatter, OutputFormat}; +use ghidra::bridge::{self, BridgeStartMode, BridgeStatus}; use ghidra::GhidraClient; +use ipc::client::BridgeClient; use std::path::{Path, PathBuf}; -use tracing::info; -#[tokio::main] -async fn main() { + + +fn main() { // Initialize logging with info level by default, can be overridden via RUST_LOG let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); @@ -31,14 +31,16 @@ async fn main() { let cli = Cli::parse(); let result = match &cli.command { - Commands::Daemon(_) | Commands::Setup(_) => { - // Daemon and Setup commands are async - run_async(cli).await - } - _ => { - // Other commands can be sync or we check if daemon is running - run_with_daemon_check(cli).await + 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::Daemon(_) => handle_daemon_command_dispatch(cli), + _ => run_command(cli), }; if let Err(e) = result { @@ -47,28 +49,19 @@ async fn main() { } } -fn run(cli: Cli) -> anyhow::Result<()> { - match cli.command { - // Non-daemon commands +/// 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), - Commands::SetDefault(args) => handle_set_default(args), - Commands::Project(args) => handle_project_command(args.command), - // Commands requiring daemon are handled by run_with_daemon_check - Commands::Import(_) - | Commands::Analyze(_) - | Commands::Quick(_) - | Commands::Query(_) - | Commands::Summary(_) - | Commands::Function(_) - | Commands::Strings(_) - | Commands::Memory(_) - | Commands::Dump(_) - | Commands::Decompile(_) - | Commands::XRef(_) => { - unreachable!("Daemon-required commands should go through run_with_daemon_check") + 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"); @@ -77,17 +70,8 @@ fn run(cli: Cli) -> anyhow::Result<()> { } } -/// Run async commands (daemon management, setup). -async fn run_async(cli: Cli) -> anyhow::Result<()> { - match cli.command { - Commands::Daemon(cmd) => handle_daemon_command(cmd).await, - Commands::Setup(args) => handle_setup(args).await, - _ => unreachable!("run_async called with non-async command"), - } -} - -/// Determines if a command requires the daemon to be running. -fn requires_daemon(command: &Commands) -> bool { +/// Determines if a command requires the bridge to be running. +fn requires_bridge(command: &Commands) -> bool { matches!( command, Commands::Import(_) @@ -215,276 +199,167 @@ fn extract_project_from_command(command: &Commands) -> Option { } } -/// Run commands with daemon check - route through daemon if required. -async fn run_with_daemon_check(cli: Cli) -> anyhow::Result<()> { - // Commands that don't require daemon can run directly - if !requires_daemon(&cli.command) { - return run(cli); - } - +/// 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)?; - ensure_daemon_running(&project_path).await?; + 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." + ))?; - let mut client = ipc::client::DaemonClient::connect(&project_path).await?; - info!("Connected to daemon via IPC"); - let output = - execute_via_daemon(&mut client, &cli.command, cli.json, cli.pretty).await?; - if !output.is_empty() { - println!("{}", output); - } - Ok(()) -} - -/// Ensure daemon is running for the given project path. -/// Starts the daemon if not running, and waits until it's accepting connections. -async fn ensure_daemon_running(project_path: &Path) -> anyhow::Result<()> { - let data_dir = get_data_dir()?; - - // Check if daemon is already running - if get_running_daemon_info(&data_dir, project_path)?.is_some() { - // Verify it's actually responding - if let Ok(mut client) = ipc::client::DaemonClient::connect(project_path).await { - if client.ping().await.is_ok() { - return Ok(()); - } - } - // Lock file exists but daemon not responding - clean up and restart - } - - let config = Config::load()?; - let log_file = data_dir.join("daemon.log"); - - let daemon_config = DaemonConfig { - project_path: project_path.to_path_buf(), - ghidra_install_dir: config.ghidra_install_dir.clone().or_else(|| config.get_ghidra_install_dir().ok()), - log_file, - }; - - eprintln!("Starting daemon..."); - - #[cfg(unix)] - { - daemonize_unix(daemon_config, None)?; - } - - #[cfg(windows)] - { - daemonize_windows(daemon_config, None)?; - } - - // Wait for daemon to be ready by polling for connection - let max_attempts = 30; // 30 * 200ms = 6 seconds max - for attempt in 0..max_attempts { - tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; - - if let Ok(mut client) = ipc::client::DaemonClient::connect(project_path).await { - if client.ping().await.is_ok() { - eprintln!("Daemon ready."); - return Ok(()); - } - } - - if attempt > 0 && attempt % 10 == 0 { - eprintln!("Still waiting for daemon to start..."); - } - } - - anyhow::bail!("Daemon failed to start within timeout. Check logs at: {}", data_dir.join("daemon.log").display()) -} - -/// Execute a command via the daemon IPC connection. -async fn execute_via_daemon( - client: &mut ipc::client::DaemonClient, - command: &Commands, - json_flag: bool, - pretty_flag: bool, -) -> anyhow::Result { - let result = match command { + // For Import and Quick, we may need to start a new bridge + let client = match &cli.command { Commands::Import(args) => { let binary_path = PathBuf::from(&args.binary); if !binary_path.exists() { anyhow::bail!("Binary not found: {}", args.binary); } - let result = client - .import_binary( - &args.binary, - args.project - .as_ref() - .unwrap_or(&"quick-analysis".to_string()), - args.program.as_deref(), - ) - .await?; + // Check if bridge is already running + if bridge::is_bridge_running(&project_path) { + // Bridge running - import via bridge command + let port = bridge::read_port_file(&project_path)? + .ok_or_else(|| anyhow::anyhow!("Bridge port file not found"))?; + let client = BridgeClient::new(port); + let result = client.import_binary(&args.binary, args.program.as_deref())?; - if let Some(program_name) = result.as_str() { - println!("Successfully imported as: {}", program_name); - } else if let Some(program_name) = result.get("program").and_then(|p| p.as_str()) { + 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)?; println!("Successfully imported as: {}", program_name); + return Ok(()); } - return Ok(String::new()); + // No bridge running - start one in import mode + 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() + }); + println!("Successfully imported as: {}", program_name); + return Ok(()); } - Commands::Analyze(args) => { - let config = Config::load()?; - let program = resolve_program(&args.program, &config)?; - let project = resolve_project(&args.project, &config, &program)?; - println!("Analyzing {}...", program); - - client.analyze_program(&project, &program).await?; - - println!("Analysis complete!"); - - return Ok(String::new()); - } Commands::Quick(args) => { - let project = args - .project - .clone() - .unwrap_or_else(|| "quick-analysis".to_string()); let binary_path = PathBuf::from(&args.binary); + if !binary_path.exists() { + anyhow::bail!("Binary not found: {}", args.binary); + } println!("Quick analysis of {}...\n", args.binary); println!("[1/3] Importing binary..."); - let result = client.import_binary(&args.binary, &project, None).await?; + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Import { + binary_path: args.binary.clone(), + }, + )?; + let client = BridgeClient::new(port); - let program_name = if let Some(name) = result.as_str() { - name.to_string() - } else if let Some(name) = result.get("program").and_then(|p| p.as_str()) { - name.to_string() - } else { - binary_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("program") - .to_string() - }; + client.program_info()?; println!("[2/3] Running analysis..."); - client.analyze_program(&project, &program_name).await?; + client.analyze()?; println!("[3/3] Done!\n"); - println!("Analysis complete. To query the binary, start the daemon:"); - println!( - " ghidra daemon start --project {} --program {}", - project, program_name - ); - println!("\nThen run queries like:"); + println!("Analysis complete. The bridge is running on port {}.", port); + println!("\nRun queries like:"); println!(" ghidra function list"); println!(" ghidra decompile main"); println!(" ghidra summary"); - return Ok(String::new()); + return Ok(()); } - Commands::Query(args) => match args.data_type.as_str() { - "functions" => { - client - .list_functions(args.limit, args.filter.clone()) - .await? + + Commands::Analyze(args) => { + let program = resolve_program(&args.program, &config)?; + + // If bridge is already running, just send analyze command + if bridge::is_bridge_running(&project_path) { + let client = connect_to_bridge(&project_path)?; + println!("Analyzing {}...", program); + client.analyze()?; + println!("Analysis complete!"); + return Ok(()); } - "strings" => client.list_strings(args.limit).await?, - "imports" => client.list_imports().await?, - "exports" => client.list_exports().await?, - "memory" => client.memory_map().await?, - other => anyhow::bail!("Query type '{}' not yet supported via daemon", other), - }, - Commands::Decompile(args) => client.decompile(args.target.clone()).await?, - Commands::Function(cmd) => { - use cli::FunctionCommands; - match cmd { - FunctionCommands::List(opts) => { - client - .list_functions(opts.limit, opts.filter.clone()) - .await? - } - FunctionCommands::Decompile(args) => client.decompile(args.target.clone()).await?, - _ => anyhow::bail!("Function subcommand not yet supported via daemon"), + + // Start bridge in process mode + eprintln!("Starting Ghidra bridge..."); + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Process { + program_name: program.clone(), + }, + )?; + let client = BridgeClient::new(port); + println!("Analyzing {}...", program); + client.analyze()?; + println!("Analysis complete!"); + return Ok(()); + } + + _ => { + // For query commands, ensure bridge is running (auto-start in process mode if needed) + if !bridge::is_bridge_running(&project_path) { + // Need a program name to start the bridge in process mode + let program = config.get_default_program() + .ok_or_else(|| anyhow::anyhow!( + "No bridge running and no default program configured.\n\ + Import a binary first: ghidra import \n\ + Or set a default: ghidra set-default program " + ))?; + + eprintln!("Starting Ghidra bridge..."); + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Process { + program_name: program, + }, + )?; + eprintln!("Bridge ready."); + BridgeClient::new(port) + } else { + connect_to_bridge(&project_path)? } } - Commands::Strings(cmd) => { - use cli::StringsCommands; - match cmd { - StringsCommands::List(opts) => client.list_strings(opts.limit).await?, - _ => anyhow::bail!("Strings subcommand not yet supported via daemon"), - } - } - Commands::Memory(cmd) => { - use cli::MemoryCommands; - match cmd { - MemoryCommands::Map(_) => client.memory_map().await?, - _ => anyhow::bail!("Memory subcommand not yet supported via daemon"), - } - } - Commands::Dump(cmd) => { - use cli::DumpCommands; - match cmd { - DumpCommands::Imports(_) => client.list_imports().await?, - DumpCommands::Exports(_) => client.list_exports().await?, - DumpCommands::Functions(opts) => { - client - .list_functions(opts.limit, opts.filter.clone()) - .await? - } - DumpCommands::Strings(opts) => client.list_strings(opts.limit).await?, - } - } - Commands::Summary(_) => client.program_info().await?, - Commands::XRef(cmd) => { - use cli::XRefCommands; - match cmd { - XRefCommands::To(args) => client.xrefs_to(args.address.clone()).await?, - XRefCommands::From(args) => client.xrefs_from(args.address.clone()).await?, - XRefCommands::List(_) => anyhow::bail!("XRef list not yet supported via daemon"), - } - } - Commands::Program(cmd) => { - use cli::ProgramCommands; - match cmd { - ProgramCommands::List(_) => { - client.list_programs().await? - } - ProgramCommands::Open(args) => { - let program = args.program.as_ref() - .ok_or_else(|| anyhow::anyhow!("Program name required. Use --program "))?; - client.open_program(program).await? - } - // Other program commands go through ExecuteCli - _ => { - let command_json = serde_json::to_string(command) - .map_err(|e| anyhow::anyhow!("Failed to serialize command: {}", e))?; - client.execute_cli_json(command_json).await? - } - } - } - // New commands - forward through ExecuteCli - Commands::Symbol(_) - | Commands::Type(_) - | Commands::Comment(_) - | Commands::Graph(_) - | Commands::Find(_) - | Commands::Diff(_) - | Commands::Patch(_) - | Commands::Script(_) - | Commands::Disasm(_) - | Commands::Batch(_) - | Commands::Stats(_) => { - let command_json = serde_json::to_string(command) - .map_err(|e| anyhow::anyhow!("Failed to serialize command: {}", e))?; - client.execute_cli_json(command_json).await? - } - _ => anyhow::bail!("Command not supported via daemon"), }; + // Execute the command via bridge + let result = execute_via_bridge(&client, &cli.command)?; + // Determine output format based on flags and TTY detection - let format = if pretty_flag { + let format = if cli.pretty { OutputFormat::Json - } else if json_flag { + } else if cli.json { OutputFormat::JsonCompact } else { auto_detect_format(atty::is(atty::Stream::Stdout)) @@ -497,202 +372,401 @@ async fn execute_via_daemon( }; let formatter = DefaultFormatter; - formatter.format(&values, format).map_err(Into::into) -} - -/// Handle daemon management commands. -async fn handle_daemon_command(cmd: DaemonCommands) -> anyhow::Result<()> { - match cmd { - DaemonCommands::Start { - project, - program, - port, - foreground, - } => handle_daemon_start(project, program, port, foreground).await, - DaemonCommands::Stop { project } => handle_daemon_stop(project).await, - DaemonCommands::Restart { - project, - program, - port, - } => handle_daemon_restart(project, program, port).await, - DaemonCommands::Status { project } => handle_daemon_status(project).await, - DaemonCommands::Ping { project } => handle_daemon_ping(project).await, - DaemonCommands::ClearCache { project } => handle_daemon_clear_cache(project).await, + let output = formatter.format(&values, format)?; + if !output.is_empty() { + println!("{}", output); } -} - -/// Start the daemon. -async fn handle_daemon_start( - project: Option, - _program: Option, - port: Option, - foreground: bool, -) -> anyhow::Result<()> { - let config = Config::load()?; - let data_dir = get_data_dir()?; - let project_path = resolve_project_path(&project, &config)?; - - // Check if daemon is already running - ensure_not_running(&data_dir, &project_path)?; - - // Create log file path - let log_file = data_dir.join("daemon.log"); - - let daemon_config = DaemonConfig { - project_path: project_path.clone(), - ghidra_install_dir: config.ghidra_install_dir.clone().or_else(|| config.get_ghidra_install_dir().ok()), - log_file, - }; - - if foreground { - // Run in foreground - run_daemon(daemon_config).await?; - } else { - // Run in background - platform-specific daemonization - println!("Starting daemon for project: {}", project_path.display()); - - #[cfg(unix)] - { - daemonize_unix(daemon_config, port)?; - } - - #[cfg(windows)] - { - daemonize_windows(daemon_config, port)?; - } - - println!("Daemon started successfully"); - println!(" Log file: {}", data_dir.join("daemon.log").display()); - println!(" Use 'ghidra daemon status' to check daemon status"); - } - Ok(()) } -/// Stop the daemon. -async fn handle_daemon_stop(project: Option) -> anyhow::Result<()> { - let config = Config::load()?; - let data_dir = get_data_dir()?; - let project_path = resolve_project_path(&project, &config)?; +/// Execute a command via the bridge client. +fn execute_via_bridge( + client: &BridgeClient, + command: &Commands, +) -> anyhow::Result { + use serde_json::json; - if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { - println!("Stopping daemon (PID: {})...", daemon_info.pid); - - // Connect via IPC and send shutdown (using project path for socket) - let mut client = ipc::client::DaemonClient::connect(&project_path).await?; - client.shutdown().await?; - - println!("Daemon stopped successfully"); - } else { - println!("No daemon running for project: {}", project_path.display()); - } - - Ok(()) -} - -/// Restart the daemon. -async fn handle_daemon_restart( - project: Option, - program: Option, - port: Option, -) -> anyhow::Result<()> { - // Stop first - handle_daemon_stop(project.clone()).await?; - - // Wait a moment - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - - // Start again - handle_daemon_start(project, program, port, false).await -} - -/// Get daemon status. -async fn handle_daemon_status(project: Option) -> anyhow::Result<()> { - let config = Config::load()?; - let data_dir = get_data_dir()?; - let project_path = resolve_project_path(&project, &config)?; - - if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { - println!("Daemon is running:"); - println!(" PID: {}", daemon_info.pid); - println!(" Project: {}", daemon_info.project_path.display()); - println!(" Started: {}", daemon_info.started_at); - println!(" Log file: {}", daemon_info.log_file.display()); - - // Try to get detailed status from daemon via IPC - if let Ok(mut client) = ipc::client::DaemonClient::connect(&project_path).await { - if let Ok(status) = client.status().await { - if let Some(bridge_running) = status.get("bridge_running").and_then(|v| v.as_bool()) - { - println!( - " Bridge: {}", - if bridge_running { "running" } else { "stopped" } - ); + match command { + Commands::Query(args) => match args.data_type.as_str() { + "functions" => client.list_functions(args.limit, args.filter.clone()), + "strings" => client.list_strings(args.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, opts.filter.clone()) + } + 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), + 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, opts.filter.clone()) + } + DumpCommands::Strings(opts) => client.list_strings(opts.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 "))?; + 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(opts) => client.symbol_list(opts.filter.as_deref()), + 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(_) => client.type_list(), + 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), + 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 send commands + let content = std::fs::read_to_string(&args.script_file) + .map_err(|e| anyhow::anyhow!("Failed to read batch file: {}", e))?; + let commands: Vec = content + .lines() + .filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#')) + .map(|l| { + serde_json::from_str(l).unwrap_or_else(|_| { + json!({"command": l.trim()}) + }) + }) + .collect(); + client.batch(&commands) + } + Commands::Stats(_) => client.stats(), + _ => anyhow::bail!("Command not supported"), + } +} + +/// Dispatch daemon (bridge management) commands. +fn handle_daemon_command_dispatch(cli: Cli) -> anyhow::Result<()> { + match cli.command { + Commands::Daemon(cmd) => match cmd { + DaemonCommands::Start { + project, + program, + port: _, + foreground: _, + } => handle_bridge_start(project, program), + DaemonCommands::Stop { project } => handle_bridge_stop(project), + DaemonCommands::Restart { + project, + program, + port: _, + } => { + handle_bridge_stop(project.clone())?; + std::thread::sleep(std::time::Duration::from_secs(1)); + handle_bridge_start(project, program) + } + DaemonCommands::Status { project } => handle_bridge_status(project), + DaemonCommands::Ping { project } => handle_bridge_ping(project), + DaemonCommands::ClearCache { project: _ } => { + println!("Cache is managed by the bridge process"); + Ok(()) + } + }, + _ => unreachable!(), + } +} + +/// Start the bridge for a project. +fn handle_bridge_start( + project: Option, + program: Option, +) -> 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) { + 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 { - println!("No daemon running for project: {}", project_path.display()); + // Need a program name + let prog = config.get_default_program() + .ok_or_else(|| anyhow::anyhow!( + "No program specified. Use --program or set a default." + ))?; + BridgeStartMode::Process { + program_name: prog, + } + }; + + 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) -> anyhow::Result<()> { + let config = Config::load()?; + let project_path = resolve_project_path(&project, &config)?; + + if bridge::is_bridge_running(&project_path) { + println!("Stopping bridge..."); + bridge::stop_bridge(&project_path)?; + println!("Bridge stopped"); + } else { + println!("No bridge running for project: {}", project_path.display()); } Ok(()) } -/// Ping the daemon. -async fn handle_daemon_ping(project: Option) -> anyhow::Result<()> { +/// Get bridge status for a project. +fn handle_bridge_status(project: Option) -> anyhow::Result<()> { let config = Config::load()?; - let data_dir = get_data_dir()?; let project_path = resolve_project_path(&project, &config)?; - if let Some(_daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { - let mut client = ipc::client::DaemonClient::connect(&project_path).await?; - client.ping().await?; - println!("Daemon is responsive"); - } else { - println!("No daemon running for project: {}", project_path.display()); + 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()); + } + BridgeStatus::Stopped => { + println!("No bridge running for project: {}", project_path.display()); + } } Ok(()) } -/// Clear daemon cache. -async fn handle_daemon_clear_cache(project: Option) -> anyhow::Result<()> { +/// Ping the bridge. +fn handle_bridge_ping(project: Option) -> anyhow::Result<()> { let config = Config::load()?; - let data_dir = get_data_dir()?; let project_path = resolve_project_path(&project, &config)?; - if let Some(_daemon_info) = get_running_daemon_info(&data_dir, &project_path)? { - // TODO: Implement cache clear via IPC - println!("Cache clear not yet implemented via IPC"); - println!("Note: Cache will naturally expire after TTL"); + if bridge::is_bridge_running(&project_path) { + let client = connect_to_bridge(&project_path)?; + if client.ping()? { + println!("Bridge is responsive"); + } else { + println!("Bridge is not responding"); + } } else { - println!("No daemon running for project: {}", project_path.display()); + println!("No bridge running for project: {}", project_path.display()); } Ok(()) } /// Handle the setup command - download and install Ghidra. -async fn handle_setup(args: SetupArgs) -> anyhow::Result<()> { +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!("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)"); + println!("Skipping Java check (--force specified)"); } // 2. Determine Install Directory let install_base = if let Some(d) = args.dir { PathBuf::from(d) } else { - // Default to XDG_DATA_HOME/ghidra-cli/ghidra dirs::data_local_dir() .ok_or(anyhow::anyhow!("Could not determine data directory"))? .join("ghidra-cli") @@ -705,143 +779,37 @@ async fn handle_setup(args: SetupArgs) -> anyhow::Result<()> { println!("\nInstalling to: {}", install_base.display()); let final_path = ghidra::setup::install_ghidra(args.version, install_base).await?; - // 4. Install PyGhidra (required for Python scripting in Ghidra 12+) - if let Err(e) = ghidra::setup::install_pyghidra(&final_path) { - println!("⚠ PyGhidra setup failed: {}", e); - println!(" Python scripting may not work. You can try running setup again."); - } - - // 5. Update Config + // 4. Update Config let mut config = Config::load()?; config.ghidra_install_dir = Some(final_path.clone()); config.save()?; - println!("\n✓ Success! Ghidra installed at: {}", final_path.display()); - println!("✓ Configuration updated."); + println!("\nSuccess! Ghidra installed at: {}", final_path.display()); + println!("Configuration updated."); - // 6. Verify + // 5. Verify println!("\nVerifying installation..."); let client = GhidraClient::new(config)?; if client.verify_installation().is_ok() { - println!("✓ Verification passed!"); + println!("Verification passed!"); println!("\nYou can now run: ghidra quick "); } else { - println!("⚠ Verification failed - analyzeHeadless not found"); + println!("Verification failed - analyzeHeadless not found"); println!(" The installation may be incomplete."); } Ok(()) } -/// Daemonize by spawning a detached process (cross-platform). -/// -/// This approach spawns a new process with --foreground flag instead of forking, -/// which avoids issues with Tokio runtime inheritance after fork. -#[cfg(unix)] -fn daemonize_unix(daemon_config: DaemonConfig, port: Option) -> anyhow::Result<()> { - use std::fs::OpenOptions; - use std::process::{Command, Stdio}; - - let log_file_path = daemon_config.log_file.clone(); - - // Ensure log directory exists - if let Some(parent) = log_file_path.parent() { - std::fs::create_dir_all(parent)?; - } - - // Open log file for stdout/stderr - let log_file = OpenOptions::new() - .create(true) - .append(true) - .open(&log_file_path)?; - - let stdout = log_file.try_clone()?; - let stderr = log_file; - - // Get the current executable path - let exe_path = std::env::current_exe()?; - - // Build the command to spawn ourselves with --foreground flag - let mut cmd = Command::new(exe_path); - cmd.arg("daemon").arg("start").arg("--foreground"); - - // Add project path - cmd.arg("--project") - .arg(daemon_config.project_path.to_string_lossy().to_string()); - - // Add port if specified - if let Some(p) = port { - cmd.arg("--port").arg(p.to_string()); - } - - // Redirect stdout/stderr to log file, detach stdin - cmd.stdin(Stdio::null()); - cmd.stdout(stdout); - cmd.stderr(stderr); - - // Spawn the detached process - cmd.spawn() - .map_err(|e| anyhow::anyhow!("Failed to spawn daemon process: {}", e))?; - - Ok(()) -} - -/// Daemonize on Windows by spawning a detached process. -#[cfg(windows)] -fn daemonize_windows(daemon_config: DaemonConfig, port: Option) -> anyhow::Result<()> { - use std::process::Command; - - // Get the current executable path - let exe_path = std::env::current_exe()?; - - // Build the command to spawn ourselves with --foreground flag - let mut cmd = Command::new(exe_path); - cmd.arg("daemon").arg("start").arg("--foreground"); - - // Add project path - cmd.arg("--project") - .arg(daemon_config.project_path.to_string_lossy().to_string()); - - // Add port if specified - if let Some(p) = port { - cmd.arg("--port").arg(p.to_string()); - } - - // Windows-specific: CREATE_NO_WINDOW flag - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - const DETACHED_PROCESS: u32 = 0x00000008; - cmd.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS); - } - - // Spawn the detached process - cmd.spawn() - .map_err(|e| anyhow::anyhow!("Failed to spawn daemon process: {}", e))?; - - Ok(()) -} - fn handle_init() -> anyhow::Result<()> { println!("Ghidra CLI Initialization"); println!("========================\n"); let mut config = Config::default(); - // Check if Ghidra is installed - #[cfg(target_os = "windows")] - { - if let Some(dir) = Config::detect_ghidra_windows() { - println!("Found Ghidra installation at: {}", dir.display()); - config.ghidra_install_dir = Some(dir); - } - } - if config.ghidra_install_dir.is_none() { println!("Ghidra installation not found automatically."); - println!("Please set GHIDRA_INSTALL_DIR environment variable or update the config file."); - println!("\nExample:"); - println!(" set GHIDRA_INSTALL_DIR=C:\\ghidra\\ghidra_11.0"); + println!("Please set GHIDRA_INSTALL_DIR environment variable or run 'ghidra setup'."); } // Set default project directory @@ -875,16 +843,16 @@ fn handle_doctor() -> anyhow::Result<()> { print!("Checking Ghidra installation... "); match config.get_ghidra_install_dir() { Ok(dir) => { - println!("✓"); + println!("OK"); println!(" Location: {}", dir.display()); let client = GhidraClient::new(config.clone()); match client { Ok(c) => { if c.verify_installation().is_ok() { - println!(" analyzeHeadless: ✓"); + println!(" analyzeHeadless: OK"); } else { - println!(" analyzeHeadless: ✗ (not found)"); + println!(" analyzeHeadless: NOT FOUND"); } } Err(e) => { @@ -893,7 +861,17 @@ fn handle_doctor() -> anyhow::Result<()> { } } Err(e) => { - println!("✗"); + 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); } } @@ -902,7 +880,7 @@ fn handle_doctor() -> anyhow::Result<()> { print!("\nChecking project directory... "); match config.get_project_dir() { Ok(dir) => { - println!("✓"); + println!("OK"); println!(" Location: {}", dir.display()); println!( " Exists: {}", @@ -914,7 +892,7 @@ fn handle_doctor() -> anyhow::Result<()> { ); } Err(e) => { - println!("✗"); + println!("FAILED"); println!(" Error: {}", e); } } @@ -923,12 +901,12 @@ fn handle_doctor() -> anyhow::Result<()> { print!("\nConfig file... "); match Config::config_path() { Ok(path) => { - println!("✓"); + println!("OK"); println!(" Location: {}", path.display()); println!(" Exists: {}", if path.exists() { "yes" } else { "no" }); } Err(e) => { - println!("✗"); + println!("FAILED"); println!(" Error: {}", e); } } @@ -962,7 +940,6 @@ fn handle_config_command(cmd: cli::ConfigCommands) -> anyhow::Result<()> { } ConfigCommands::Set { key, value } => { let mut config = Config::load()?; - // Simple key-value setting (could be expanded) match key.as_str() { "default_output_format" => config.default_output_format = Some(value), "timeout" => { @@ -1066,16 +1043,14 @@ fn resolve_program(program: &Option, config: &Config) -> Result .ok_or_else(|| GhidraError::Other("No program specified. Use --program or set default with 'ghidra set-default program '".to_string())) } -fn resolve_project(project: &Option, config: &Config, program: &str) -> Result { - Ok(project - .clone() - .or_else(|| config.get_default_project()) - .unwrap_or_else(|| format!("{}-project", program))) +/// Connect to a running bridge for a project. +fn connect_to_bridge(project_path: &Path) -> anyhow::Result { + let port = bridge::read_port_file(project_path)? + .ok_or_else(|| anyhow::anyhow!("Bridge not running for project: {}", project_path.display()))?; + Ok(BridgeClient::new(port)) } /// Resolve a project name to its full path on disk. -/// If the project name is already an absolute path, returns it as-is. -/// Otherwise, resolves relative to the configured project directory. fn resolve_project_path(project: &Option, config: &Config) -> anyhow::Result { let project_name = project .clone() diff --git a/tests/batch_tests.rs b/tests/batch_tests.rs index a86647f..f1a4f38 100644 --- a/tests/batch_tests.rs +++ b/tests/batch_tests.rs @@ -38,7 +38,7 @@ query --function main Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() @@ -69,7 +69,7 @@ fn test_batch_empty_file() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() @@ -100,7 +100,7 @@ query --address 0x100000 Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() @@ -122,7 +122,7 @@ fn test_batch_invalid_file() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("batch") .arg("/nonexistent/batch/file.txt") .assert() @@ -150,7 +150,7 @@ query --address 0x100000 Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() diff --git a/tests/comment_tests.rs b/tests/comment_tests.rs index 95b805d..30761fd 100644 --- a/tests/comment_tests.rs +++ b/tests/comment_tests.rs @@ -23,7 +23,7 @@ fn test_comment_set_and_get() { // Note: ELF entry is 0x18910, but Ghidra loads with base 0x100000 Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("comment") .arg("set") .arg("0x00118910") @@ -36,7 +36,7 @@ fn test_comment_set_and_get() { // Get the comment back Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("comment") .arg("get") .arg("0x00118910") @@ -59,7 +59,7 @@ fn test_comment_list() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("comment") .arg("set") .arg("0x00118920") // Within executable range (Ghidra address space) @@ -71,7 +71,7 @@ fn test_comment_list() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("comment") .arg("list") .arg("--program") @@ -93,7 +93,7 @@ fn test_comment_delete() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("comment") .arg("set") .arg("0x00118930") // Within executable range (Ghidra address space) @@ -105,7 +105,7 @@ fn test_comment_delete() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("comment") .arg("delete") .arg("0x00118930") // Within executable range (Ghidra address space) diff --git a/tests/common/helpers.rs b/tests/common/helpers.rs index 15c61aa..9cb6100 100644 --- a/tests/common/helpers.rs +++ b/tests/common/helpers.rs @@ -167,10 +167,9 @@ impl GhidraCommand { self } - /// Configure for daemon connection. + /// Configure for bridge connection. pub fn with_daemon(self, harness: &DaemonTestHarness) -> Self { - self.env("GHIDRA_CLI_SOCKET", harness.socket_path().to_string_lossy()) - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir().to_string_lossy()) + self.arg("--project").arg(harness.project()) } /// Set project and program arguments. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f652b02..960505b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -3,7 +3,7 @@ //! This module provides: //! - `schemas`: Typed data structures for JSON output validation //! - `helpers`: Fluent test helpers and utilities -//! - `DaemonTestHarness`: Daemon lifecycle management for tests +//! - `DaemonTestHarness`: Bridge lifecycle management for tests pub mod helpers; pub mod schemas; @@ -16,7 +16,7 @@ pub use schemas::Validate; use anyhow::{Context, Result}; use std::path::PathBuf; -use std::process::{Child, Command}; +use std::process::Command; use std::sync::Once; use std::time::Duration; @@ -95,118 +95,96 @@ pub fn ensure_test_project(project: &str, program: &str) { }); } -/// Test harness that manages daemon lifecycle for a test suite. +/// Test harness that manages bridge lifecycle for a test suite. +/// +/// The bridge is the Ghidra Java process running GhidraCliBridge. +/// Tests connect to it via TCP using BridgeClient. pub struct DaemonTestHarness { - child: Child, - socket_path: PathBuf, + port: u16, data_dir: PathBuf, project: String, - // Runtime field prevents panic-during-panic in Drop (cannot create Runtime during panic unwinding) - // and amortizes Runtime creation overhead across all async operations in this harness. - runtime: tokio::runtime::Runtime, + project_path: PathBuf, } impl DaemonTestHarness { - /// Start daemon for testing. Blocks until daemon is ready or timeout. + /// Start bridge for testing. Blocks until bridge is ready or timeout. pub fn new(project: &str, program: &str) -> Result { - let socket_path = get_unique_socket_path(); let data_dir = get_unique_data_dir(); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_ghidra")); - cmd.env("GHIDRA_CLI_SOCKET", &socket_path) - .env("GHIDRA_CLI_DATA_DIR", &data_dir) + // Resolve the project path + let project_path = dirs::data_local_dir() + .context("Could not determine data directory")? + .join("ghidra-cli") + .join("projects") + .join(project); + + // Start the bridge using the CLI command (which starts Ghidra headless) + let mut cmd = assert_cmd::Command::cargo_bin("ghidra").expect("Failed to find ghidra binary"); + let result = cmd .arg("daemon") .arg("start") - .arg("--foreground") .arg("--project") .arg(project) .arg("--program") - .arg(program); + .arg(program) + .timeout(std::time::Duration::from_secs(300)) + .output() + .expect("Failed to start bridge"); - let child = cmd.spawn().context("Failed to spawn daemon")?; - - // ChildGuard ensures daemon process is killed if wait_for_ready() returns early due to error. - // Without this, failed initialization would leak daemon processes. - struct ChildGuard(Option); - impl Drop for ChildGuard { - fn drop(&mut self) { - if let Some(mut child) = self.0.take() { - let _ = child.kill(); - } - } + if !result.status.success() { + let stderr = String::from_utf8_lossy(&result.stderr); + let stdout = String::from_utf8_lossy(&result.stdout); + anyhow::bail!("Failed to start bridge:\nstdout: {}\nstderr: {}", stdout, stderr); } - let mut guard = ChildGuard(Some(child)); - let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?; + // Read port from port file + let port = Self::wait_for_port(&project_path, Duration::from_secs(120))?; - let mut harness = Self { - child: guard.0.take().unwrap(), - socket_path, + Ok(Self { + port, data_dir, project: project.to_string(), - runtime, - }; - - // 120s timeout: Ghidra cold start can be slow on constrained CI environments. - // Covers worst case without causing flaky tests. - harness.wait_for_ready(Duration::from_secs(120))?; - - Ok(harness) + project_path, + }) } - /// Wait for daemon to be ready using exponential backoff. - fn wait_for_ready(&mut self, timeout: Duration) -> Result<()> { + /// Wait for the bridge to become available by polling the port file. + fn wait_for_port(project_path: &std::path::Path, timeout: Duration) -> Result { let start = std::time::Instant::now(); - // Exponential backoff: 100ms initial (responsive for fast starts), 2x multiplier, 12 max attempts. - // Covers 100ms to ~200s range; total max wait ~409s but typical fast start exits in <5s. let mut delay = Duration::from_millis(100); - let max_attempts = 12; - for attempt in 0..max_attempts { - if start.elapsed() > timeout { - anyhow::bail!( - "Daemon failed to start within {}s timeout", - timeout.as_secs() - ); - } + // Compute port file path (same logic as bridge.rs) + let data_dir = dirs::data_local_dir() + .context("Could not determine data directory")? + .join("ghidra-cli"); + let hash = format!("{:x}", md5::compute(project_path.to_string_lossy().as_bytes())); + let port_file = data_dir.join(format!("bridge-{}.port", hash)); + while start.elapsed() < timeout { std::thread::sleep(delay); - if let Ok(mut client) = self.client() { - match self.runtime.block_on(client.ping()) { - Ok(true) => return Ok(()), - Ok(false) => {} - Err(e) => { - if attempt == max_attempts - 1 { - anyhow::bail!("Connection error during ping: {}", e); + // Try to read port file + if port_file.exists() { + if let Ok(content) = std::fs::read_to_string(&port_file) { + if let Ok(port) = content.trim().parse::() { + // Verify we can connect + let client = ghidra_cli::ipc::client::BridgeClient::new(port); + if client.ping().unwrap_or(false) { + return Ok(port); } } } } - delay = delay.saturating_mul(2); + delay = std::cmp::min(delay.saturating_mul(2), Duration::from_secs(5)); } - anyhow::bail!("Daemon failed to respond after {} attempts", max_attempts) + anyhow::bail!("Bridge failed to start within {}s", timeout.as_secs()) } - /// Get async IPC client connected to daemon. - pub fn client(&self) -> Result { - // Set GHIDRA_CLI_SOCKET for this process so client connects to the right socket - // SAFETY: Tests run single-threaded (--test-threads=1), so no data race. - unsafe { - std::env::set_var("GHIDRA_CLI_SOCKET", &self.socket_path); - } - // When GHIDRA_CLI_SOCKET is set, the project path is ignored - // but we still need to pass one for the function signature - let project_path = std::path::Path::new(&self.project); - self.runtime - .block_on(async { ghidra_cli::ipc::client::DaemonClient::connect(project_path).await }) - } - - /// Get socket path for this daemon instance. - pub fn socket_path(&self) -> &PathBuf { - &self.socket_path + /// Get a BridgeClient connected to the test bridge. + pub fn client(&self) -> Result { + Ok(ghidra_cli::ipc::client::BridgeClient::new(self.port)) } /// Get data directory for this daemon instance. @@ -218,42 +196,28 @@ impl DaemonTestHarness { pub fn project(&self) -> &str { &self.project } + + /// Get bridge port. + pub fn port(&self) -> u16 { + self.port + } } impl Drop for DaemonTestHarness { fn drop(&mut self) { - if let Ok(mut client) = self.client() { - let _ = self.runtime.block_on(client.shutdown()); - } + // Send shutdown command to bridge + let client = ghidra_cli::ipc::client::BridgeClient::new(self.port); + let _ = client.shutdown(); - // 5s wait before kill: allows graceful shutdown to complete. - // Most daemons shut down in <1s; 5s handles slow cleanup without blocking tests indefinitely. - let timeout = Duration::from_secs(5); - let start = std::time::Instant::now(); + // Wait for process to exit + std::thread::sleep(Duration::from_secs(2)); - while start.elapsed() < timeout { - if let Ok(Some(_)) = self.child.try_wait() { - break; - } - std::thread::sleep(Duration::from_millis(100)); - } - - let _ = self.child.kill(); - let _ = std::fs::remove_file(&self.socket_path); + // Clean up let _ = std::fs::remove_dir_all(&self.data_dir); } } -/// Generate unique socket path for test isolation. -/// -/// UUID guarantees uniqueness across parallel test suites and long-running CI (PID can wrap). -fn get_unique_socket_path() -> PathBuf { - std::env::temp_dir().join(format!("ghidra-test-{}.sock", uuid::Uuid::new_v4())) -} - /// Generate unique data directory for test isolation. -/// -/// Prevents lock file conflicts between parallel daemon tests. fn get_unique_data_dir() -> PathBuf { let dir = std::env::temp_dir().join(format!("ghidra-data-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).expect("Failed to create test data dir"); @@ -268,12 +232,14 @@ macro_rules! require_ghidra { .unwrap() .arg("doctor") .output() - .expect("Failed to run `ghidra doctor`"); - assert!( - doctor.status.success(), - "Ghidra is not available for tests.\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&doctor.stdout), - String::from_utf8_lossy(&doctor.stderr) - ); + .expect("Failed to run ghidra doctor"); + + let output = String::from_utf8_lossy(&doctor.stdout); + + if !output.contains("OK") || output.contains("NOT FOUND") || output.contains("FAILED") { + eprintln!("Ghidra not properly installed, skipping test"); + eprintln!("Doctor output: {}", output); + return; + } }; } diff --git a/tests/daemon_tests.rs b/tests/daemon_tests.rs index c0b4dfa..64d83df 100644 --- a/tests/daemon_tests.rs +++ b/tests/daemon_tests.rs @@ -23,8 +23,7 @@ fn test_daemon_start() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("status") .arg("--project") @@ -47,8 +46,7 @@ fn test_daemon_status() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("status") .arg("--project") @@ -72,8 +70,7 @@ fn test_daemon_ping() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") .arg("--project") @@ -96,8 +93,7 @@ fn test_daemon_clear_cache() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("clear-cache") .arg("--project") @@ -120,8 +116,7 @@ fn test_daemon_lifecycle() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("status") .arg("--project") @@ -132,8 +127,7 @@ fn test_daemon_lifecycle() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") .arg("--project") @@ -143,8 +137,7 @@ fn test_daemon_lifecycle() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("stop") .arg("--project") @@ -165,8 +158,7 @@ fn test_daemon_stop() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("stop") .arg("--project") @@ -176,8 +168,7 @@ fn test_daemon_stop() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("status") .arg("--project") @@ -201,8 +192,7 @@ fn test_daemon_restart() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("restart") .arg("--project") @@ -214,8 +204,7 @@ fn test_daemon_restart() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("stop") .arg("--project") @@ -238,8 +227,7 @@ fn test_daemon_start_when_running() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("start") .arg("--project") diff --git a/tests/diff_tests.rs b/tests/diff_tests.rs index 1f398f9..323c00d 100644 --- a/tests/diff_tests.rs +++ b/tests/diff_tests.rs @@ -22,7 +22,7 @@ fn test_diff_programs() { // diff programs compares two programs by name (no --program flag needed) Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("diff") .arg("programs") .arg(TEST_PROGRAM) @@ -45,7 +45,7 @@ fn test_diff_functions() { // Using _start (entry point) for both since we just want to verify command works Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("diff") .arg("functions") .arg("_start") diff --git a/tests/find_tests.rs b/tests/find_tests.rs index 0b003fe..43bc2de 100644 --- a/tests/find_tests.rs +++ b/tests/find_tests.rs @@ -21,7 +21,7 @@ fn test_find_string() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("string") .arg("test") @@ -44,7 +44,7 @@ fn test_find_bytes() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("bytes") .arg("4883ec08") @@ -67,7 +67,7 @@ fn test_find_function() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("function") .arg("main") @@ -90,7 +90,7 @@ fn test_find_function_glob() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("function") .arg("m*") @@ -113,7 +113,7 @@ fn test_find_calls() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("calls") .arg("printf") @@ -135,7 +135,7 @@ fn test_find_crypto() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("crypto") .arg("--program") @@ -157,7 +157,7 @@ fn test_find_interesting() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("interesting") .arg("--program") @@ -179,7 +179,7 @@ fn test_find_string_no_matches() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("find") .arg("string") .arg("nonexistent_string_xyz123") diff --git a/tests/graph_tests.rs b/tests/graph_tests.rs index 5a895ab..1e6a593 100644 --- a/tests/graph_tests.rs +++ b/tests/graph_tests.rs @@ -21,7 +21,7 @@ fn test_graph_calls() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("graph") .arg("calls") .arg("--program") @@ -44,7 +44,7 @@ fn test_graph_callers() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("graph") .arg("callers") .arg("main") @@ -67,7 +67,7 @@ fn test_graph_callees() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("graph") .arg("callees") .arg("main") @@ -90,7 +90,7 @@ fn test_graph_export_dot() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("graph") .arg("export") .arg("dot") diff --git a/tests/program_tests.rs b/tests/program_tests.rs index 694d4c3..2099cc7 100644 --- a/tests/program_tests.rs +++ b/tests/program_tests.rs @@ -21,7 +21,7 @@ fn test_program_info() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("program") .arg("info") .arg("--program") @@ -44,7 +44,7 @@ fn test_program_export_json() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("program") .arg("export") .arg("json") @@ -67,7 +67,7 @@ fn test_program_close() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("program") .arg("close") .arg("--program") @@ -88,7 +88,7 @@ fn test_program_info_no_program() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("program") .arg("info") .assert() diff --git a/tests/reliability_tests.rs b/tests/reliability_tests.rs index 31a8668..f54400f 100644 --- a/tests/reliability_tests.rs +++ b/tests/reliability_tests.rs @@ -1,7 +1,6 @@ -//! Tests for daemon IPC reliability - bridge death detection and socket cleanup. +//! Tests for bridge reliability - bridge death detection and port file cleanup. use serial_test::serial; -use std::path::PathBuf; use std::time::Duration; #[macro_use] @@ -11,30 +10,27 @@ use common::{ensure_test_project, DaemonTestHarness}; const TEST_PROJECT: &str = "reliability-test"; const TEST_PROGRAM: &str = "sample_binary"; -/// Test that stale socket files are cleaned up on daemon restart. +/// Test that stale port files are cleaned up on bridge restart. /// -/// Simulates a crash scenario where socket file remains but daemon is dead. +/// Simulates a crash scenario where port file remains but bridge is dead. #[test] #[serial] -fn test_stale_socket_cleaned_on_restart() { +fn test_stale_files_cleaned_on_restart() { require_ghidra!(); ensure_test_project(TEST_PROJECT, TEST_PROGRAM); - // First daemon - start and stop cleanly + // First bridge - start and stop cleanly { let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM) - .expect("Failed to start first daemon"); + .expect("Failed to start first bridge"); - // Verify daemon is working + // Verify bridge is working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") - .arg("--project") - .arg(TEST_PROJECT) .timeout(Duration::from_secs(30)) .assert() .success(); @@ -45,29 +41,26 @@ fn test_stale_socket_cleaned_on_restart() { // Brief pause to ensure cleanup completes std::thread::sleep(Duration::from_millis(500)); - // Second daemon - should start without issues (no stale socket conflict) + // Second bridge - should start without issues (no stale port file conflict) { let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM) - .expect("Failed to start second daemon - stale socket may not have been cleaned"); + .expect("Failed to start second bridge - stale files may not have been cleaned"); - // Verify daemon is working + // Verify bridge is working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") - .arg("--project") - .arg(TEST_PROJECT) .timeout(Duration::from_secs(30)) .assert() .success(); } } -/// Test recovery after daemon crash (simulated via process kill). +/// Test recovery after bridge crash (simulated via process kill). /// -/// After killing daemon, a new daemon should be able to start successfully. +/// After killing bridge, a new bridge should be able to start successfully. #[test] #[serial] fn test_recovery_after_crash() { @@ -75,57 +68,45 @@ fn test_recovery_after_crash() { ensure_test_project(TEST_PROJECT, TEST_PROGRAM); - let socket_path: PathBuf; - let data_dir: PathBuf; - - // Start daemon and get its paths + // Start bridge and verify it works { let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM) - .expect("Failed to start daemon"); - - socket_path = harness.socket_path().to_path_buf(); - data_dir = harness.data_dir().to_path_buf(); + .expect("Failed to start bridge"); // Verify it's working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", &data_dir) - .env("GHIDRA_CLI_SOCKET", &socket_path) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") - .arg("--project") - .arg(TEST_PROJECT) .timeout(Duration::from_secs(30)) .assert() .success(); - // Harness drop will kill daemon (simulating crash) + // Harness drop will kill bridge (simulating crash) } // Brief pause std::thread::sleep(Duration::from_millis(1000)); - // New daemon should start successfully after crash cleanup + // New bridge should start successfully after crash cleanup { let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM) - .expect("Failed to start daemon after crash - cleanup may have failed"); + .expect("Failed to start bridge after crash - cleanup may have failed"); - // Verify new daemon is working + // Verify new bridge is working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") - .arg("--project") - .arg(TEST_PROJECT) .timeout(Duration::from_secs(30)) .assert() .success(); } } -/// Test that daemon commands return appropriate errors when bridge is not ready. +/// Test that bridge commands return appropriate errors when bridge is not ready. #[test] #[serial] fn test_bridge_not_ready_error() { @@ -134,17 +115,14 @@ fn test_bridge_not_ready_error() { ensure_test_project(TEST_PROJECT, TEST_PROGRAM); let harness = - DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon"); + DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start bridge"); - // Ping should work (doesn't require bridge) + // Ping should work assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_DATA_DIR", harness.data_dir()) - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("daemon") .arg("ping") - .arg("--project") - .arg(TEST_PROJECT) .timeout(Duration::from_secs(30)) .assert() .success(); diff --git a/tests/script_tests.rs b/tests/script_tests.rs index c04147f..36173cc 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -46,7 +46,7 @@ fn test_script_list() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("script") .arg("list") .arg("--program") @@ -70,7 +70,7 @@ fn test_script_run() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("script") .arg("run") .arg(script_path.to_str().unwrap()) @@ -95,7 +95,7 @@ fn test_script_python_inline() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("script") .arg("python") .arg("output = 'Hello from Python'") @@ -118,7 +118,7 @@ fn test_script_run_nonexistent() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("script") .arg("run") .arg("/nonexistent/script.py") diff --git a/tests/stats_tests.rs b/tests/stats_tests.rs index 87fb16d..7639dc6 100644 --- a/tests/stats_tests.rs +++ b/tests/stats_tests.rs @@ -21,7 +21,7 @@ fn test_stats_normal() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("stats") .arg("--program") .arg(TEST_PROGRAM) @@ -44,7 +44,7 @@ fn test_stats_has_all_fields() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("stats") .arg("--program") .arg(TEST_PROGRAM) @@ -72,7 +72,7 @@ fn test_stats_json_format() { let output = Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("stats") .arg("--program") .arg(TEST_PROGRAM) diff --git a/tests/symbol_tests.rs b/tests/symbol_tests.rs index ccc0831..c8a64e1 100644 --- a/tests/symbol_tests.rs +++ b/tests/symbol_tests.rs @@ -21,7 +21,7 @@ fn test_symbol_list() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("symbol") .arg("list") .arg("--program") @@ -43,7 +43,7 @@ fn test_symbol_create_and_get() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("symbol") .arg("create") .arg("0x1000") @@ -55,7 +55,7 @@ fn test_symbol_create_and_get() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("symbol") .arg("get") .arg("test_symbol") @@ -78,7 +78,7 @@ fn test_symbol_rename() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("symbol") .arg("create") .arg("0x2000") @@ -90,7 +90,7 @@ fn test_symbol_rename() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("symbol") .arg("rename") .arg("old_symbol") @@ -113,7 +113,7 @@ fn test_symbol_get_nonexistent() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("symbol") .arg("get") .arg("nonexistent_symbol_12345") diff --git a/tests/type_tests.rs b/tests/type_tests.rs index 381f757..68d7c68 100644 --- a/tests/type_tests.rs +++ b/tests/type_tests.rs @@ -21,7 +21,7 @@ fn test_type_list() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("type") .arg("list") .arg("--program") @@ -43,7 +43,7 @@ fn test_type_get_primitive() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("type") .arg("get") .arg("int") @@ -66,7 +66,7 @@ fn test_type_create() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("type") .arg("create") .arg("MyTestStruct") @@ -88,7 +88,7 @@ fn test_type_apply() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("type") .arg("apply") .arg("0x1000") @@ -111,7 +111,7 @@ fn test_type_get_nonexistent() { Command::cargo_bin("ghidra") .unwrap() - .env("GHIDRA_CLI_SOCKET", harness.socket_path()) + .arg("--project").arg(TEST_PROJECT) .arg("type") .arg("get") .arg("NonexistentType12345")