Skip to main content

ghidra_mcp/
paths.rs

1//! Filesystem locations for the server: the ghidra-mcp data dir, the per-version worker-script dir, and
2//! the per-instance log path (spec §5). Base dir = $GHIDRA_MCP_HOME, else %USERPROFILE%/$HOME + ".ghidra-mcp",
3//! else the system temp dir. No external dir crate (keeps the deny gate small).
4
5use crate::worker_asset::WORKER_JAVA;
6use sha2::{Digest, Sha256};
7use std::path::PathBuf;
8
9/// The ghidra-mcp base data dir (created by callers as needed).
10pub fn data_dir() -> PathBuf {
11    if let Ok(h) = std::env::var("GHIDRA_MCP_HOME") {
12        if !h.is_empty() {
13            return PathBuf::from(h);
14        }
15    }
16    let home = std::env::var("USERPROFILE")
17        .or_else(|_| std::env::var("HOME"))
18        .ok()
19        .filter(|s| !s.is_empty());
20    match home {
21        Some(h) => PathBuf::from(h).join(".ghidra-mcp"),
22        None => std::env::temp_dir().join("ghidra-mcp"),
23    }
24}
25
26/// The per-version worker-script directory: `<data>/scripts/<hash16>/`. `<hash16>` is the first 16
27/// hex chars of the SHA-256 of the embedded `WORKER_JAVA`, so a binary always extracts into a dir
28/// unique to its worker version (spec §5 per-version script dir). The file inside MUST stay named
29/// `GhidraMcpWorker.java` (Ghidra: filename == class name).
30pub fn versioned_script_dir() -> PathBuf {
31    let mut h = Sha256::new();
32    h.update(WORKER_JAVA.as_bytes());
33    let digest = h.finalize();
34    let hash16: String = digest.iter().take(8).map(|b| format!("{b:02x}")).collect();
35    data_dir().join("scripts").join(hash16)
36}
37
38/// The per-instance log path: `<data>/logs/worker-<pid>.log` (spec §5 per-instance filename).
39pub fn instance_log_path() -> PathBuf {
40    data_dir()
41        .join("logs")
42        .join(format!("worker-{}.log", std::process::id()))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
50
51    /// Hold while mutating `GHIDRA_MCP_HOME`: the env is process-global and
52    /// the test runner executes tests on parallel threads, so an unguarded
53    /// `remove_var` in one test can yank the value out from under another.
54    /// Restores the prior value (or absence) on drop, so a panicking assert
55    /// cannot leak a mutation into the next test either.
56    struct HomeGuard {
57        _lock: std::sync::MutexGuard<'static, ()>,
58        prior: Option<String>,
59    }
60
61    impl HomeGuard {
62        fn set(value: &str) -> Self {
63            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
64            let prior = std::env::var("GHIDRA_MCP_HOME").ok();
65            std::env::set_var("GHIDRA_MCP_HOME", value);
66            Self { _lock: lock, prior }
67        }
68    }
69
70    impl Drop for HomeGuard {
71        fn drop(&mut self) {
72            match self.prior.take() {
73                Some(v) => std::env::set_var("GHIDRA_MCP_HOME", v),
74                None => std::env::remove_var("GHIDRA_MCP_HOME"),
75            }
76        }
77    }
78
79    #[test]
80    fn ghidra_mcp_home_env_overrides_base() {
81        let _guard = HomeGuard::set(r"C:\tmp\gh");
82        assert_eq!(data_dir(), PathBuf::from(r"C:\tmp\gh"));
83    }
84
85    #[test]
86    fn versioned_script_dir_is_under_scripts_and_hash_named() {
87        let _guard = HomeGuard::set(r"C:\tmp\gh");
88        let d = versioned_script_dir();
89        assert!(d.starts_with(r"C:\tmp\gh"));
90        assert!(d.to_string_lossy().contains("scripts"));
91        // leaf is 16 hex chars
92        let leaf = d.file_name().unwrap().to_string_lossy().into_owned();
93        assert_eq!(leaf.len(), 16);
94        assert!(leaf.chars().all(|c| c.is_ascii_hexdigit()));
95    }
96
97    #[test]
98    fn instance_log_path_names_this_pid() {
99        let _guard = HomeGuard::set(r"C:\tmp\gh");
100        let p = instance_log_path();
101        assert!(p
102            .to_string_lossy()
103            .contains(&format!("worker-{}", std::process::id())));
104    }
105}