1use std::env;
17use std::path::{Path, PathBuf};
18use std::process;
19
20use fugue_db::Error as ExportError;
21use fugue_db::backend::{Backend, Imported};
22
23use tempfile::tempdir;
24use which::{which, which_in};
25use url::Url;
26
27use thiserror::Error;
28
29#[derive(Debug, Error)]
30pub enum Error {
31 #[error("IDA Pro is not available as a backend")]
32 NotAvailable,
33 #[error("invalid path to IDA Pro: {0}")]
34 InvalidPath(which::Error),
35 #[error("error launching IDA Pro: {0}")]
36 Launch(std::io::Error),
37 #[error("IDA Pro reported I/O error")]
38 InputOutput,
39 #[error("IDA Pro reported error on import")]
40 Import,
41 #[error("IDA Pro reported unsupported file type")]
42 Unsupported,
43 #[error("IDA Pro reported error when attempting to rebase")]
44 Rebase,
45 #[error("IDA Pro encountered a generic failure")]
46 Failure,
47 #[error("could not create temporary directory to store exported database: {0}")]
48 TempDirectory(#[source] std::io::Error),
49 #[error("`{0}` is not a supported URL scheme")]
50 UnsupportedScheme(String),
51}
52
53impl From<Error> for ExportError {
54 fn from(e: Error) -> Self {
55 ExportError::importer_error("ida-pro", e)
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct IDA {
61 ida_path: Option<PathBuf>,
62 fdb_path: Option<PathBuf>,
63 overwrite: bool,
64 wine: bool,
65}
66
67impl Default for IDA {
68 fn default() -> Self {
69 Self {
70 ida_path: None,
71 fdb_path: None,
72 overwrite: false,
73 wine: false,
74 }
75 }
76}
77
78impl IDA {
79 fn find_ida<F: Fn(&str) -> Result<PathBuf, Error>>(f: F) -> Result<(PathBuf, bool), Error> {
80 if let Ok(local) = f("idat64").or_else(|_| f("ida64")) {
81 Ok((local, false))
82 } else {
83 f("idat64.exe").or_else(|_| f("ida64.exe")).map(|path| (path, true))
84 }
85 }
86
87 pub fn new() -> Result<Self, Error> {
88 if let Ok(root_dir) = env::var("IDA_INSTALL_DIR") {
89 if let Ok(v) = Self::from_path(root_dir) {
90 return Ok(v);
91 }
92 }
93
94 if let Ok((ida_path, wine)) = Self::find_ida(|p| which(p).map_err(Error::InvalidPath)) {
95 Ok(Self { ida_path: Some(ida_path), wine, ..Default::default() })
96 } else {
97 Err(Error::NotAvailable)
98 }
99 }
100
101 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
102 let root_dir = path.as_ref();
103 let (ida_path, wine) =
104 Self::find_ida(|p| which_in(p, Some(root_dir), ".").map_err(Error::InvalidPath))?;
105
106 Ok(Self { ida_path: Some(ida_path), wine, ..Default::default() })
107 }
108
109 pub fn export_path<P: AsRef<Path>>(mut self, path: P, overwrite: bool) -> Self {
110 self.fdb_path = Some(path.as_ref().to_owned());
111 self.overwrite = overwrite;
112 self
113 }
114}
115
116impl Backend for IDA {
117 type Error = Error;
118
119 fn name(&self) -> &'static str {
120 "fugue-idapro"
121 }
122
123 fn is_available(&self) -> bool {
124 self.ida_path.is_some()
125 }
126
127 fn is_preferred_for(&self, path: &Url) -> Option<bool> {
128 if path.scheme() != "file" {
129 return None
130 }
131
132 if let Ok(path) = path.to_file_path() {
133 path.extension()
134 .map(|ext| Some(ext == "i64" || ext == "idb"))
135 .unwrap_or(Some(false))
136 } else {
137 None
138 }
139 }
140
141 fn import(&self, program: &Url) -> Result<Imported, Self::Error> {
142 if program.scheme() != "file" {
143 return Err(Error::UnsupportedScheme(program.scheme().to_owned()))
144 }
145
146 let program = program.to_file_path()
147 .map_err(|_| Error::UnsupportedScheme(program.scheme().to_owned()))?;
148
149 let ida_path = self.ida_path.as_ref().ok_or_else(|| Error::NotAvailable)?;
150
151 let load_existing = program.exists()
152 && program
153 .extension()
154 .map(|e| e == "i64" || e == "idb")
155 .unwrap_or(false);
156
157 let force_32bit =
158 load_existing && program.extension().map(|e| e == "idb").unwrap_or(false);
159
160 let mut cmd = if !self.wine {
161 if force_32bit {
162 process::Command::new(format!(
163 "{}",
164 ida_path.with_file_name("idat").display()
165 ))
166 } else {
167 process::Command::new(format!("{}", ida_path.display()))
168 }
169 } else {
170 let mut process = process::Command::new("wine");
171 if force_32bit {
172 process.arg(format!(
173 "{}",
174 ida_path.with_file_name("idat.exe").display()
175 ));
176 } else {
177 process.arg(format!("{}", ida_path.display()));
178 }
179 process
180 };
181
182 cmd.arg("-A");
183
184 let mut tmp = tempdir()
185 .map_err(Error::TempDirectory)?
186 .into_path();
187
188 let output = if let Some(ref fdb_path) = self.fdb_path {
189 fdb_path.to_owned()
190 } else {
191 tmp.join("fugue-temp-export.fdb")
192 };
193
194 let opts = vec![
195 format!("-OFugueOutput:{}", output.display()),
196 format!("-OFugueForceOverwrite:{}", self.overwrite),
197 ];
198
199 if load_existing {
212 cmd.args(&opts);
213 cmd.arg(&format!("{}", program.display()));
214 } else {
215 tmp.push("fugue-import-tmp.ida");
216 cmd.arg(&format!("-o{}", tmp.display()));
217 cmd.args(&opts);
218 cmd.arg(&format!("{}", program.display()));
219 }
220
221 match cmd
222 .output()
223 .map_err(Error::Launch)
224 .map(|output| output.status.code())?
225 {
226 Some(100) => Ok(Imported::File(output)),
227 Some(101) => Err(Error::InputOutput)?,
228 Some(102) => Err(Error::Import)?,
229 Some(103) => Err(Error::Unsupported)?,
230 Some(104) => Err(Error::Rebase)?,
231 _ => Err(Error::Failure)?,
232 }
233 }
234}