1use std::env;
2use std::ffi::OsString;
3use std::fs::File;
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::process;
7
8use fugue_db::backend::{Backend, Imported};
9use fugue_db::Error as ExportError;
10
11use tempfile::tempdir;
12use thiserror::Error;
13use url::Url;
14use which::{which, which_in};
15
16#[derive(Debug, Error)]
17pub enum Error {
18 #[error("Ghidra is not available as a backend")]
19 NotAvailable,
20 #[error("invalid path to Ghidra: {0}")]
21 InvalidPath(#[from] which::Error),
22 #[error("invalid input path: {0}")]
23 InvalidInputPath(PathBuf),
24 #[error("error launching Ghidra exporter: {0}")]
25 Launch(#[from] std::io::Error),
26 #[error("Ghidra reported I/O error")]
27 InputOutput,
28 #[error("Ghidra reported error on import")]
29 Import,
30 #[error("Ghidra reported unsupported file type")]
31 Unsupported,
32 #[error("Ghidra encountered a generic failure")]
33 Failure,
34 #[error("could not fix ownership on project file: {0}")]
35 Ownership(#[source] std::io::Error),
36 #[error("could not create temporary directory for export: {0}")]
37 TempDirectory(#[source] std::io::Error),
38 #[error("`{0}` is not a supported URL scheme")]
39 UnsupportedScheme(String),
40}
41
42impl From<Error> for ExportError {
43 fn from(e: Error) -> Self {
44 Self::importer_error("ghidra", e)
45 }
46}
47
48#[derive(Debug)]
49pub struct GhidraProject {
50 project_root: PathBuf,
51 project_name: OsString,
52 project_path: PathBuf,
53 project_file: PathBuf,
54 previous_prp: Vec<u8>,
55}
56
57macro_rules! PRP_TEMPLATE {
58 () => {
59 r#"<?xml version="1.0" encoding="UTF-8"?>
60<FILE_INFO>
61 <BASIC_INFO>
62 <STATE NAME="OWNER" TYPE="string" VALUE="{}" />
63 </BASIC_INFO>
64</FILE_INFO>"#
65 };
66}
67
68impl GhidraProject {
69 fn toggle_ownership(&mut self, restore: bool) -> Result<(), Error> {
70 let prp_path = self.project_path.join("project.prp");
71 if !prp_path.exists() {
72 return Ok(());
73 }
74
75 let old_prp = {
76 let mut prp = File::open(&prp_path).map_err(Error::Ownership)?;
77 let mut buf = Vec::new();
78 prp.read_to_end(&mut buf).map_err(Error::Ownership)?;
79 buf
80 };
81
82 let mut prp = File::create(&prp_path).map_err(Error::Ownership)?;
83
84 if restore {
85 prp.write_all(&self.previous_prp)
86 } else {
87 self.previous_prp = old_prp;
88 write!(prp, PRP_TEMPLATE!(), whoami::username())
89 }
90 .map_err(Error::Ownership)?;
91
92 Ok(())
93 }
94
95 pub fn modify_ownership(&mut self) -> Result<(), Error> {
96 self.toggle_ownership(false)
97 }
98
99 pub fn restore_ownership(&mut self) -> Result<(), Error> {
100 self.toggle_ownership(true)
101 }
102
103 pub fn resolve<P: AsRef<Path>>(path: P) -> Option<Self> {
104 let path = path.as_ref();
105
106 if path.exists() {
108 return None;
109 }
110
111 let mut parent = path.parent();
112 while let Some(project_path) = parent {
113 let rep_path = project_path.with_extension("rep");
114 if rep_path.exists() {
116 if project_path.exists() && rep_path != project_path {
117 if !project_path
118 .extension()
119 .map(|ext| ext == "gpr")
120 .unwrap_or(false)
121 {
122 return None;
124 }
125 }
126
127 if project_path.extension().is_some() {
128 if !project_path
129 .extension()
130 .map(|ext| ext == "gpr" || ext == "rep")
131 .unwrap_or(false)
132 {
133 return None;
135 }
136 }
137
138 let prp_path = rep_path.join("project.prp");
139 if !prp_path.exists() {
140 return None;
142 }
143
144 return Some(GhidraProject {
146 project_root: rep_path.parent()?.to_owned(),
147 project_name: rep_path.file_stem()?.to_owned(),
148 project_path: rep_path,
149 project_file: path.strip_prefix(project_path).ok()?.to_owned(),
150 previous_prp: Vec::default(),
151 });
152 }
153
154 if project_path.exists() {
155 return None;
156 }
157 parent = project_path.parent();
158 }
159 None
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164pub struct Ghidra {
165 path: Option<PathBuf>,
166 fdb_path: Option<PathBuf>,
167 overwrite: bool,
168 fix_ownership: bool,
169}
170
171impl Default for Ghidra {
172 fn default() -> Self {
173 Self {
174 path: None,
175 fdb_path: None,
176 overwrite: false,
177 fix_ownership: false,
178 }
179 }
180}
181
182impl Ghidra {
183 fn find_ghidra<F: Fn(&str) -> Result<PathBuf, Error>>(f: F) -> Result<PathBuf, Error> {
184 if cfg!(target_os = "windows") {
185 f("ghidraRun.bat")
186 } else {
187 f("ghidraRun")
188 }
189 }
190
191 fn find_headless<F: Fn(&str) -> Result<PathBuf, Error>>(f: F) -> Result<PathBuf, Error> {
192 if cfg!(target_os = "windows") {
193 f("analyzeHeadless.bat")
194 } else {
195 f("analyzeHeadless")
196 }
197 }
198
199 pub fn new() -> Result<Self, ExportError> {
200 let root = if let Ok(root_dir) = env::var("GHIDRA_INSTALL_DIR") {
201 PathBuf::from(root_dir).join("support")
202 } else {
203 let base = Self::find_ghidra(|p| which(p).map_err(Error::InvalidPath))
204 .map_err(ExportError::from)?;
205 base.parent().unwrap().join("support")
206 };
207
208 let path =
209 Self::find_headless(|p| which_in(p, Some(&root), ".").map_err(Error::InvalidPath))
210 .map_err(ExportError::from)?;
211
212 Ok(Self {
213 path: Some(path),
214 ..Default::default()
215 })
216 }
217
218 pub fn export_path<P: AsRef<Path>>(mut self, path: P, overwrite: bool) -> Self {
219 self.fdb_path = Some(path.as_ref().to_owned());
220 self.overwrite = overwrite;
221 self
222 }
223
224 pub fn force_ownership(mut self, force: bool) -> Self {
225 self.fix_ownership = force;
226 self
227 }
228}
229
230impl Backend for Ghidra {
231 type Error = Error;
232
233 fn name(&self) -> &'static str {
234 "fugue-ghidra"
235 }
236
237 fn is_available(&self) -> bool {
238 self.path.is_some()
239 }
240
241 fn is_preferred_for(&self, path: &Url) -> Option<bool> {
242 if path.scheme() != "file" {
243 return None;
244 }
245
246 if let Ok(path) = path.to_file_path() {
247 Some(GhidraProject::resolve(&path).is_some())
248 } else {
249 None
250 }
251 }
252
253 fn import(&self, program: &Url) -> Result<Imported, Error> {
254 if program.scheme() != "file" {
255 return Err(Error::UnsupportedScheme(program.scheme().to_owned()));
256 }
257
258 let path = program
259 .to_file_path()
260 .map_err(|_| Error::UnsupportedScheme(program.scheme().to_owned()))?;
261
262 let headless = self.path.as_ref().ok_or_else(|| Error::NotAvailable)?;
263 let mut process = process::Command::new(headless);
264
265 let mut project = GhidraProject::resolve(&path);
267
268 if let Some(ref project) = project {
269 let location = project.project_root.clone();
272 let project_name = project.project_name.clone();
273 let project_file = project.project_file.clone();
274
275 process.arg(location);
276 process.arg(project_name);
277
278 process.arg("-process");
279 process.arg(project_file);
280 } else {
281 let tmp = tempdir().map_err(Error::TempDirectory)?.into_path();
283 process.arg(tmp);
284 process.arg("fugue-temp-project");
285
286 process.arg("-import");
287 process.arg(path);
288
289 process.arg("-deleteProject");
290 }
291
292 process.arg("-preScript");
293 process.arg("FugueAnalysisOptions.java");
294
295 process.arg("-postScript");
296 process.arg("FugueExport.java");
297
298 process.arg(format!("FugueForceOverwrite:{}", self.overwrite));
299
300 let output = if let Some(ref fdb_path) = self.fdb_path {
301 process.arg(format!("FugueOutput:{}", fdb_path.display()));
302 Imported::File(fdb_path.to_owned())
303 } else {
304 let mut tmp = tempdir().map_err(Error::TempDirectory)?.into_path();
305 tmp.push("fugue-temp-export.fdb");
306 process.arg(format!("FugueOutput:{}", tmp.display()));
307 Imported::File(tmp)
308 };
309
310 if self.fix_ownership {
312 if let Some(ref mut project) = project {
313 project.modify_ownership()?;
314 }
315 }
316
317 let result = match process
318 .output()
319 .map_err(Error::Launch)
320 .map(|output| output.status.code())
321 {
322 Ok(Some(100)) => Ok(output),
323 Ok(Some(101)) => Err(Error::InputOutput),
324 Ok(Some(102)) => Err(Error::Import),
325 Ok(Some(103)) => Err(Error::Unsupported),
326 Ok(_) => Err(Error::Failure),
327 Err(e) => Err(e),
328 };
329
330 if self.fix_ownership {
331 if let Some(ref mut project) = project {
332 project.restore_ownership()?;
333 }
334 }
335
336 result
337 }
338}
339
340#[cfg(test)]
341mod test {
342 use super::*;
343
344 #[test]
345 fn test_clean_import() -> Result<(), Box<dyn std::error::Error>> {
346 let ghidra = Ghidra::new()?.export_path("/tmp/exported.fdb", true);
347 let url = Url::from_file_path(Path::new("./tests/true").canonicalize()?).unwrap();
348
349 let _ = ghidra.import(&url)?;
350
351 Ok(())
352 }
353}