1#![doc = include_str!("../README.md")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/0xdea/haruspex/master/.img/logo.png")]
3
4use std::fs;
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::Path;
8use std::sync::atomic::{AtomicUsize, Ordering};
9
10use anyhow::Context;
11use idalib::IDAError;
12use idalib::decompiler::HexRaysErrorCode;
13use idalib::func::{Function, FunctionFlags};
14use idalib::idb::IDB;
15use thiserror::Error;
16
17#[cfg(unix)]
19const RESERVED_CHARS: &[char] = &['.', '/'];
20#[cfg(windows)]
21const RESERVED_CHARS: &[char] = &['.', '/', '<', '>', ':', '"', '\\', '|', '?', '*'];
22
23const MAX_FILENAME_LEN: usize = 64;
25
26static COUNTER: AtomicUsize = AtomicUsize::new(0);
28
29#[derive(Error, Debug)]
31pub enum HaruspexError {
32 #[error(transparent)]
34 DecompileFailed(#[from] IDAError),
35 #[error(transparent)]
37 FileWriteFailed(#[from] std::io::Error),
38}
39
40pub fn run(filepath: &Path) -> anyhow::Result<usize> {
46 println!("[*] Analyzing binary file `{}`", filepath.display());
48 let idb = IDB::open(filepath)
49 .with_context(|| format!("Failed to analyze binary file `{}`", filepath.display()))?;
50 println!("[+] Successfully analyzed binary file");
51 println!();
52
53 println!("[-] Processor: {}", idb.processor().long_name());
55 println!("[-] Compiler: {:?}", idb.meta().cc_id());
56 println!("[-] File type: {:?}", idb.meta().filetype());
57 println!();
58
59 anyhow::ensure!(idb.decompiler_available(), "Decompiler is not available");
61
62 let dirpath = filepath.with_extension("dec");
64 println!("[*] Preparing output directory `{}`", dirpath.display());
65 if dirpath.exists() {
66 fs::remove_dir(&dirpath).map_err(|_| anyhow::anyhow!("Output directory already exists"))?;
67 }
68 fs::create_dir_all(&dirpath)
69 .with_context(|| format!("Failed to create directory `{}`", dirpath.display()))?;
70 println!("[+] Output directory is ready");
71
72 println!();
74 println!("[*] Extracting pseudocode of functions...");
75 println!();
76 for (_id, f) in idb.functions() {
77 if f.flags().contains(FunctionFlags::THUNK) {
79 continue;
80 }
81
82 let func_name = f.name().unwrap_or_else(|| "<no name>".into());
84 let output_file = format!(
85 "{}@{:X}",
86 func_name
87 .replace(RESERVED_CHARS, "_")
88 .chars()
89 .take(MAX_FILENAME_LEN)
90 .collect::<String>(),
91 f.start_address()
92 );
93 let output_path = dirpath.join(output_file).with_extension("c");
94
95 match decompile_to_file(&idb, &f, &output_path) {
96 Ok(()) => println!("{func_name} -> `{}`", output_path.display()),
98
99 Err(HaruspexError::DecompileFailed(IDAError::HexRays(e)))
101 if e.code() == HexRaysErrorCode::License =>
102 {
103 return Err(e.into());
104 }
105
106 Err(HaruspexError::DecompileFailed(_)) => continue,
108
109 Err(e) => return Err(e.into()),
111 }
112
113 COUNTER.fetch_add(1, Ordering::Relaxed);
114 }
115
116 if COUNTER.load(Ordering::Relaxed) == 0 {
118 fs::remove_dir(&dirpath)
119 .with_context(|| format!("Failed to remove directory `{}`", dirpath.display()))?;
120 anyhow::bail!("No functions were decompiled, check your input file");
121 }
122
123 println!();
124 println!(
125 "[+] Decompiled {COUNTER:?} functions into `{}`",
126 dirpath.display()
127 );
128 println!("[+] Done processing binary file `{}`", filepath.display());
129 Ok(COUNTER.load(Ordering::Relaxed))
130}
131
132pub fn decompile_to_file(
160 idb: &IDB,
161 func: &Function,
162 filepath: impl AsRef<Path>,
163) -> Result<(), HaruspexError> {
164 let decomp = idb.decompile(func)?;
166 let source = decomp.pseudocode();
167
168 let mut writer = BufWriter::new(File::create(&filepath)?);
171 writer.write_all(source.as_bytes())?;
172 writer.flush()?;
173
174 Ok(())
175}