From eb27cbc0ea8efc6f752c909fe6e7fc679ed3eac4 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 1 Nov 2026 22:49:34 +0700 Subject: [PATCH] Find Ghidra installed from a Linux package Linux packages put Ghidra in /opt/ghidra (Arch and its derivatives) or /usr/share/ghidra (Debian, Kali) with only a start script on PATH, so Deep mode said Ghidra wasn't there (#85). The engine table lists both folders and the Ghidra lookup falls back to them. --- crates/engines/src/ghidra/get.rs | 10 +++++++--- crates/engines/src/lib.rs | 13 +++++++++++-- crates/engines/src/table.rs | 10 +++++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/engines/src/ghidra/get.rs b/crates/engines/src/ghidra/get.rs index f8bf9ff0..16c4df8b 100644 --- a/crates/engines/src/ghidra/get.rs +++ b/crates/engines/src/ghidra/get.rs @@ -26,7 +26,7 @@ pub fn can_install() -> bool { } /// Ghidra's headless launcher: in the folder the person picked (Ghidra's own folder, or one that -/// holds it), then the managed install, then PATH. +/// holds it), then the managed install, then PATH, then where system packages put it. pub fn headless(install_dir: Option<&Path>, store: &Path) -> Option { if let Some(dir) = install_dir { if dir.is_file() { @@ -36,7 +36,9 @@ pub fn headless(install_dir: Option<&Path>, store: &Path) -> Option { return Some(p); } } - in_child(&store.join(ID), launcher).or_else(|| crate::find_on_path(&["analyzeHeadless.bat", "analyzeHeadless"])) + in_child(&store.join(ID), launcher) + .or_else(|| crate::find_on_path(&["analyzeHeadless.bat", "analyzeHeadless"])) + .or_else(|| crate::well_known(ID).into_iter().find(|p| p.is_file())) } /// The Java runtime to run Ghidra with: the one the person set, else the managed one. `None` lets @@ -150,7 +152,9 @@ mod tests { let d = tempfile::tempdir().unwrap(); let store = d.path().join("store"); let exe = if cfg!(windows) { "analyzeHeadless.bat" } else { "analyzeHeadless" }; - assert!(headless(None, &store).is_none() || crate::find_on_path(&[exe]).is_some()); + // None, unless this computer has Ghidra on PATH or from a system package. + let installed = crate::find_on_path(&[exe]).is_some() || crate::well_known(ID).iter().any(|p| p.is_file()); + assert!(headless(None, &store).is_none() || installed); // The managed install: the zip's own top folder under /ghidra. let managed = store.join("ghidra/ghidra_12.1.4_PUBLIC/support").join(exe); touch(&managed); diff --git a/crates/engines/src/lib.rs b/crates/engines/src/lib.rs index f0468db6..9e393bb6 100644 --- a/crates/engines/src/lib.rs +++ b/crates/engines/src/lib.rs @@ -82,7 +82,7 @@ fn in_managed(dir: &Path, exes: &[&str]) -> Option { } /// Standard install locations for programs that don't put themselves on PATH (browsers on -/// Windows and macOS), most common first. +/// Windows and macOS, Ghidra from Linux packages), most common first. pub fn well_known(id: &str) -> Vec { engine(id).map(ExternalEngine::known_install_paths).unwrap_or_default() } @@ -135,7 +135,9 @@ mod tests { // Nothing but folders: still none, not a panic. let empty = tempfile::tempdir().unwrap(); std::fs::create_dir_all(empty.path().join("ghidra").join("ghidra_12.1.4_PUBLIC")).unwrap(); - assert!(locate("ghidra", &NoOverrides, empty.path()).is_none()); + // (Unless this computer has Ghidra on PATH or from a system package.) + let installed = find_on_path(&[exe]).is_some() || well_known("ghidra").iter().any(|p| p.is_file()); + assert!(locate("ghidra", &NoOverrides, empty.path()).is_none() || installed); } #[test] @@ -146,5 +148,12 @@ mod tests { assert!(paths.iter().any(|p| p.to_string_lossy().contains("Chrome"))); assert!(paths.iter().any(|p| p.to_string_lossy().contains("Edge"))); } + // #85: Linux packages put Ghidra in its own folder with only a start script on PATH. + let ghidra = well_known("ghidra"); + if cfg!(target_os = "linux") { + assert_eq!(ghidra, [PathBuf::from("/opt/ghidra/support/analyzeHeadless"), PathBuf::from("/usr/share/ghidra/support/analyzeHeadless")]); + } else { + assert!(ghidra.is_empty()); + } } } diff --git a/crates/engines/src/table.rs b/crates/engines/src/table.rs index 878bfe26..9849fdd6 100644 --- a/crates/engines/src/table.rs +++ b/crates/engines/src/table.rs @@ -144,8 +144,16 @@ const fn mac(base: Base, rel: &'static str) -> KnownPath { KnownPath { os: "macos", base, rel } } +const fn linux(dir: &'static str, rel: &'static str) -> KnownPath { + KnownPath { os: "linux", base: Base::Dir(dir), rel } +} + const NO_PATHS: &[KnownPath] = &[]; +/// Linux packages put Ghidra in a folder of its own and only a start script on PATH (`/usr/bin/ghidra`): +/// Arch and its derivatives in `/opt/ghidra`, Debian and Kali in `/usr/share/ghidra` (#85). +const GHIDRA_PATHS: &[KnownPath] = &[linux("/opt/ghidra", "support/analyzeHeadless"), linux("/usr/share/ghidra", "support/analyzeHeadless")]; + /// Browsers on Windows and macOS rarely put themselves on PATH. const CHROMIUM_PATHS: &[KnownPath] = &[ win("ProgramFiles", "Google/Chrome/Application/chrome.exe"), @@ -322,7 +330,7 @@ pub const ENGINES: &[ExternalEngine] = &[ needed_for: "Deep mode: richer decompiled code", exes: &["analyzeHeadless"], exes_windows: &["analyzeHeadless.bat"], - known_paths: NO_PATHS, + known_paths: GHIDRA_PATHS, path_setting: false, install: Install::Guided { pins: GHIDRA_PINS, how: GHIDRA_SETUP }, },