use std::{
env, fs, io,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use jni::{InitArgsBuilder, JNIVersion, JavaVM};
use thiserror::Error;
static RUNTIME: Mutex<Option<Arc<RuntimeState>>> = Mutex::new(None);
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("GHIDRA_INSTALL_DIR is not set and no Ghidra lastrun file was found")]
MissingInstallDir,
#[error("failed to canonicalize Ghidra install directory {path}: {source}")]
CanonicalizeInstallDir {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("Ghidra install directory is missing {path}")]
MissingInstallFile { path: PathBuf },
#[error("failed to read Ghidra lastrun file {path}: {source}")]
ReadLastrun {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to build JVM classpath: {source}")]
Classpath {
#[source]
source: env::JoinPathsError,
},
#[error("failed to read {path}: {source}")]
ReadDir {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to read {path}: {source}")]
ReadFile {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to build JVM args: {source}")]
BuildJvmArgs {
#[source]
source: jni::JvmError,
},
#[error("failed to start embedded JVM: {source}")]
StartJvm {
#[source]
source: jni::errors::StartJvmError,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LaunchOptions {
pub install_dir: Option<PathBuf>,
pub extra_classpaths: Vec<PathBuf>,
pub verbose: bool,
}
#[derive(Clone)]
pub struct GhidraRuntime {
state: Arc<RuntimeState>,
}
struct RuntimeState {
jvm: JavaVM,
install_dir: PathBuf,
}
impl GhidraRuntime {
pub fn install_dir(&self) -> &Path {
&self.state.install_dir
}
pub fn with_attached<T, E>(
&self,
operation: impl FnOnce(&mut jni::Env<'_>) -> std::result::Result<T, E>,
) -> std::result::Result<T, E>
where
E: From<jni::errors::Error>,
{
self.state.jvm.attach_current_thread(operation)
}
}
pub fn start(options: LaunchOptions) -> Result<GhidraRuntime> {
let mut runtime = RUNTIME.lock().expect("runtime lock poisoned");
if let Some(state) = runtime.as_ref() {
return Ok(GhidraRuntime {
state: Arc::clone(state),
});
}
let install_dir = resolve_install_dir(options.install_dir.as_deref())?;
let jvm = create_jvm(&install_dir, &options.extra_classpaths, options.verbose)?;
let state = Arc::new(RuntimeState { jvm, install_dir });
*runtime = Some(Arc::clone(&state));
Ok(GhidraRuntime { state })
}
pub fn started() -> bool {
RUNTIME.lock().expect("runtime lock poisoned").is_some()
}
fn create_jvm(install_dir: &Path, extra_classpaths: &[PathBuf], verbose: bool) -> Result<JavaVM> {
let mut builder = InitArgsBuilder::new()
.version(JNIVersion::V1_8)
.ignore_unrecognized(true)
.option(format!(
"-Djava.class.path={}",
classpath(install_dir, extra_classpaths)?
));
for arg in vmargs(install_dir)? {
if !arg.trim().is_empty() {
builder = builder.option(arg);
}
}
if !verbose {
builder = builder.option("-Djava.awt.headless=true");
}
let args = builder
.build()
.map_err(|source| Error::BuildJvmArgs { source })?;
JavaVM::new(args).map_err(|source| Error::StartJvm { source })
}
fn resolve_install_dir(explicit: Option<&Path>) -> Result<PathBuf> {
if let Some(path) = explicit {
return validate_install_dir(path);
}
if let Some(path) = env::var_os("GHIDRA_INSTALL_DIR") {
return validate_install_dir(Path::new(&path));
}
if let Some(path) = lastrun_install_dir()? {
return validate_install_dir(&path);
}
Err(Error::MissingInstallDir)
}
fn validate_install_dir(path: &Path) -> Result<PathBuf> {
let path = path
.canonicalize()
.map_err(|source| Error::CanonicalizeInstallDir {
path: path.to_path_buf(),
source,
})?;
let properties = path.join("Ghidra/application.properties");
if !properties.is_file() {
return Err(Error::MissingInstallFile { path: properties });
}
let utility = path.join("Ghidra/Framework/Utility/lib/Utility.jar");
if !utility.is_file() {
return Err(Error::MissingInstallFile { path: utility });
}
Ok(path)
}
fn lastrun_install_dir() -> Result<Option<PathBuf>> {
let Some(home) = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
else {
return Ok(None);
};
let path = home.join("ghidra/lastrun");
if !path.is_file() {
return Ok(None);
}
let value = fs::read_to_string(&path).map_err(|source| Error::ReadLastrun {
path: path.clone(),
source,
})?;
Ok(Some(PathBuf::from(value.trim())))
}
fn classpath(install_dir: &Path, extra_classpaths: &[PathBuf]) -> Result<String> {
let mut paths = ghidra_jars(install_dir)
.unwrap_or_else(|_| vec![install_dir.join("Ghidra/Framework/Utility/lib/Utility.jar")]);
paths.extend(extra_classpaths.iter().cloned());
Ok(env::join_paths(paths)
.map_err(|source| Error::Classpath { source })?
.to_string_lossy()
.into_owned())
}
fn ghidra_jars(install_dir: &Path) -> Result<Vec<PathBuf>> {
let mut jars = Vec::new();
visit_jars(&install_dir.join("Ghidra"), &mut jars)?;
jars.sort();
Ok(jars)
}
fn visit_jars(path: &Path, jars: &mut Vec<PathBuf>) -> Result<()> {
for entry in fs::read_dir(path).map_err(|source| Error::ReadDir {
path: path.to_path_buf(),
source,
})? {
let entry = entry.map_err(|source| Error::ReadDir {
path: path.to_path_buf(),
source,
})?;
let path = entry.path();
if path.is_dir() {
visit_jars(&path, jars)?;
} else if path.extension().is_some_and(|ext| ext == "jar") {
jars.push(path);
}
}
Ok(())
}
fn vmargs(install_dir: &Path) -> Result<Vec<String>> {
let properties = install_dir.join("support/launch.properties");
let content = fs::read_to_string(&properties).map_err(|source| Error::ReadFile {
path: properties.clone(),
source,
})?;
let platform_suffix = if cfg!(target_os = "macos") {
"MACOS"
} else if cfg!(target_os = "windows") {
"WINDOWS"
} else {
"LINUX"
};
let mut args = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(value) = line.strip_prefix("VMARGS=") {
args.push(unquote(value));
} else if let Some((prefix, value)) = line.split_once('=')
&& prefix == format!("VMARGS_{platform_suffix}")
{
args.push(unquote(value));
}
}
Ok(args)
}
fn unquote(value: &str) -> String {
let value = value.trim();
value
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(value)
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unquotes_launch_values() {
assert_eq!(unquote("\"-Dfoo=bar\""), "-Dfoo=bar");
assert_eq!(unquote("-Dfoo=bar"), "-Dfoo=bar");
}
#[test]
fn classpath_uses_platform_separator() {
let joined = env::join_paths(["alpha", "beta"]).expect("paths join");
assert_eq!(
env::split_paths(&joined).collect::<Vec<_>>(),
vec![PathBuf::from("alpha"), PathBuf::from("beta")]
);
}
}