1use ghidra_worker_ctl::config::WorkerConfig;
7use std::path::PathBuf;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
12pub struct ServerConfig {
13 pub ghidra_install_dir: PathBuf,
14 pub project_dir: PathBuf,
15 pub project_name: String,
16 pub bootstrap_program: String,
17 pub bootstrap_program_path: String,
20 pub max_heap: Option<String>,
21 pub boot_timeout: Duration,
22 pub rpc_deadline: Duration,
24 pub warming_deadline: Duration,
26}
27
28#[derive(Debug, Default, Clone)]
30pub struct RawConfig {
31 pub ghidra_install_dir: Option<String>,
32 pub project_dir: Option<String>,
33 pub project_name: Option<String>,
34 pub bootstrap_program: Option<String>,
35 pub bootstrap_program_path: Option<String>,
36 pub max_heap: Option<String>,
37}
38
39#[derive(Debug, thiserror::Error)]
40pub enum ConfigError {
41 #[error("{field} is required but was not provided (set --{flag} or ${env})")]
42 Missing {
43 field: &'static str,
44 flag: &'static str,
45 env: &'static str,
46 },
47 #[error("GHIDRA_INSTALL_DIR does not look like a Ghidra install: {0} (missing support/analyzeHeadless)")]
48 NotGhidra(PathBuf),
49 #[error("project_dir does not exist: {0}")]
50 NoProjectDir(PathBuf),
51}
52
53impl RawConfig {
54 pub fn resolve(self) -> Result<ServerConfig, ConfigError> {
57 let ghidra_install_dir =
58 PathBuf::from(self.ghidra_install_dir.ok_or(ConfigError::Missing {
59 field: "ghidra_install_dir",
60 flag: "ghidra-install-dir",
61 env: "GHIDRA_INSTALL_DIR",
62 })?);
63 let project_dir = PathBuf::from(self.project_dir.ok_or(ConfigError::Missing {
64 field: "project_dir",
65 flag: "project-dir",
66 env: "GHIDRA_MCP_PROJECT_DIR",
67 })?);
68 let project_name = self.project_name.ok_or(ConfigError::Missing {
69 field: "project_name",
70 flag: "project-name",
71 env: "GHIDRA_MCP_PROJECT_NAME",
72 })?;
73 let bootstrap_program = self.bootstrap_program.ok_or(ConfigError::Missing {
74 field: "bootstrap_program",
75 flag: "bootstrap-program",
76 env: "GHIDRA_MCP_BOOTSTRAP_PROGRAM",
77 })?;
78 let launcher = ghidra_install_dir.join("support").join(if cfg!(windows) {
80 "analyzeHeadless.bat"
81 } else {
82 "analyzeHeadless"
83 });
84 if !launcher.exists() {
85 return Err(ConfigError::NotGhidra(ghidra_install_dir));
86 }
87 if !project_dir.exists() {
88 return Err(ConfigError::NoProjectDir(project_dir));
89 }
90 let bootstrap_program_path = self
91 .bootstrap_program_path
92 .unwrap_or_else(|| format!("/{bootstrap_program}"));
93 Ok(ServerConfig {
94 ghidra_install_dir,
95 project_dir,
96 project_name,
97 bootstrap_program,
98 bootstrap_program_path,
99 max_heap: self.max_heap,
100 boot_timeout: Duration::from_secs(120),
101 rpc_deadline: Duration::from_secs(45),
102 warming_deadline: Duration::from_secs(8),
103 })
104 }
105}
106
107impl ServerConfig {
108 pub fn worker_config(&self, script_dir: PathBuf) -> WorkerConfig {
110 WorkerConfig {
111 ghidra_install_dir: self.ghidra_install_dir.clone(),
112 project_dir: self.project_dir.clone(),
113 project_name: self.project_name.clone(),
114 bootstrap_program: self.bootstrap_program.clone(),
115 script_dir,
116 script_name: crate::worker_asset::WORKER_SCRIPT_NAME.to_string(),
117 max_heap: self.max_heap.clone(),
118 boot_timeout: self.boot_timeout,
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn raw() -> RawConfig {
128 RawConfig {
129 ghidra_install_dir: Some(std::env::temp_dir().to_string_lossy().into_owned()),
130 project_dir: Some(std::env::temp_dir().to_string_lossy().into_owned()),
131 project_name: Some("p".into()),
132 bootstrap_program: Some("add.exe".into()),
133 ..Default::default()
134 }
135 }
136
137 #[test]
138 fn missing_required_field_is_a_clean_error() {
139 let mut r = raw();
140 r.bootstrap_program = None;
141 let err = r.resolve().unwrap_err();
142 assert!(matches!(
143 err,
144 ConfigError::Missing {
145 field: "bootstrap_program",
146 ..
147 }
148 ));
149 }
150
151 #[test]
152 fn non_ghidra_install_dir_is_rejected() {
153 let err = raw().resolve().unwrap_err();
155 assert!(matches!(err, ConfigError::NotGhidra(_)));
156 }
157
158 #[test]
159 fn bootstrap_program_path_defaults_to_slash_prefixed_name() {
160 let mut r = raw();
163 r.bootstrap_program = Some("add.exe".into());
164 assert_eq!(
166 format!("/{}", r.bootstrap_program.clone().unwrap()),
167 "/add.exe"
168 );
169 }
170}