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 as _};
7use std::path::Path;
8
9use anyhow::Context as _;
10use idalib::IDAError;
11use idalib::decompiler::HexRaysErrorCode;
12use idalib::func::{Function, FunctionFlags};
13use idalib::idb::IDB;
14use thiserror::Error;
15
16#[cfg(unix)]
18const RESERVED_CHARS: &[char] = &['.', '/'];
19#[cfg(windows)]
20const RESERVED_CHARS: &[char] = &['.', '/', '<', '>', ':', '"', '\\', '|', '?', '*'];
21
22const MAX_FILENAME_LEN: usize = 64;
24
25#[derive(Error, Debug)]
27#[non_exhaustive]
28pub enum HaruspexError {
29 #[error(transparent)]
31 DecompileFailed(#[from] IDAError),
32 #[error(transparent)]
34 FileWriteFailed(#[from] std::io::Error),
35}
36
37pub fn run(filepath: &Path) -> anyhow::Result<usize> {
43 println!("[*] Analyzing binary file `{}`", filepath.display());
45 let idb = IDB::open(filepath)
46 .with_context(|| format!("Failed to analyze binary file `{}`", filepath.display()))?;
47 println!("[+] Successfully analyzed binary file");
48 println!();
49
50 println!("[-] Processor: {}", idb.processor().long_name());
52 println!("[-] Compiler: {:?}", idb.meta().cc_id());
53 println!("[-] File type: {:?}", idb.meta().filetype());
54 println!();
55
56 anyhow::ensure!(idb.decompiler_available(), "Decompiler is not available");
58
59 let dirpath = filepath.with_extension("dec");
61 println!("[*] Preparing output directory `{}`", dirpath.display());
62 if dirpath.exists() {
63 fs::remove_dir(&dirpath).map_err(|_| anyhow::anyhow!("Output directory already exists"))?;
64 }
65 fs::create_dir_all(&dirpath)
66 .with_context(|| format!("Failed to create directory `{}`", dirpath.display()))?;
67 println!("[+] Output directory is ready");
68
69 let mut decompiled_count = 0;
70
71 println!();
73 println!("[*] Extracting pseudocode of functions...");
74 println!();
75 for (_id, f) in idb.functions() {
76 if f.flags().contains(FunctionFlags::THUNK) {
78 continue;
79 }
80
81 let func_name = f.name().unwrap_or_else(|| "[no name]".into());
83 let output_file = format!(
84 "{}@{:X}",
85 func_name
86 .replace(RESERVED_CHARS, "_")
87 .chars()
88 .take(MAX_FILENAME_LEN)
89 .collect::<String>(),
90 f.start_address()
91 );
92 let output_path = dirpath.join(output_file).with_extension("c");
93
94 match decompile_to_file(&idb, &f, &output_path) {
95 Ok(()) => {
97 println!("{func_name} -> `{}`", output_path.display());
98 decompiled_count += 1;
99 }
100
101 Err(HaruspexError::DecompileFailed(IDAError::HexRays(e)))
103 if e.code() == HexRaysErrorCode::License =>
104 {
105 return Err(e.into());
106 }
107
108 Err(HaruspexError::DecompileFailed(_)) => (),
110
111 Err(e) => return Err(e.into()),
113 }
114 }
115
116 if decompiled_count == 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 {decompiled_count} functions into `{}`",
126 dirpath.display()
127 );
128 println!("[+] Done processing binary file `{}`", filepath.display());
129 Ok(decompiled_count)
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}