use crate::Result;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub(crate) mod headless;
pub trait Lifter {
fn lift(&self, input: &Path, output: &Path) -> Result<()>;
}
struct Verifying<L> {
inner: L,
ir: Ir,
}
impl<L: Lifter> Lifter for Verifying<L> {
fn lift(&self, input: &Path, output: &Path) -> Result<()> {
self.inner.lift(input, output)?;
let program = crate::program::AnyProgram::load(output)
.map_err(|e| crate::Error::UnreadableOutput(input.to_path_buf(), Box::new(e)))?;
if program.is_empty() {
log::warn!("{}", no_functions_message(input, self.ir));
}
Ok(())
}
}
fn no_functions_message(input: &Path, ir: Ir) -> String {
let binary = input.display();
let hint = if ir == Ir::GhidraPcode {
". Ghidra produces exactly this when its decompiler native binary is \
missing: run support/gradle/gradlew buildNatives in the Ghidra installation"
} else {
""
};
format!(
"{binary}: the {ir} lifted from it has no functions in it, so every birthmark of it \
will be empty -- and two empty birthmarks score as a perfect match. Either the binary \
really has no functions, or the tool found none{hint}"
)
}
#[derive(Debug, ValueEnum, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
#[clap(rename_all = "kebab-case")]
pub enum Ir {
#[default]
GhidraPcode,
IdaMicrocode,
BinaryNinjaLlil,
BinaryNinjaMlil,
BinaryNinjaHlil,
}
impl Ir {
pub fn tool(&self) -> &'static str {
match self {
Ir::GhidraPcode => "Ghidra",
Ir::IdaMicrocode => "IDA Pro",
Ir::BinaryNinjaLlil | Ir::BinaryNinjaMlil | Ir::BinaryNinjaHlil => "Binary Ninja",
}
}
pub fn home_spec(&self) -> HomeSpec {
match self {
Ir::GhidraPcode => HomeSpec {
tool: self.tool(),
env: "GHIDRA_HOME",
candidates: if cfg!(windows) {
&[]
} else {
&[
"/opt/homebrew/opt/ghidra/libexec",
"/usr/local/opt/ghidra/libexec",
"/opt/ghidra/libexec",
]
},
},
Ir::IdaMicrocode => HomeSpec {
tool: self.tool(),
env: "IDA_HOME",
candidates: if cfg!(windows) {
&[]
} else {
&[
"/Applications/IDA Professional 9.4.app/Contents/MacOS",
"/Applications/IDA Classroom 9.4.app/Contents/MacOS",
"/opt/ida",
]
},
},
Ir::BinaryNinjaLlil | Ir::BinaryNinjaMlil | Ir::BinaryNinjaHlil => HomeSpec {
tool: self.tool(),
env: "BINARY_NINJA_HOME",
candidates: if cfg!(windows) {
&[]
} else {
&[
"/Applications/Binary Ninja.app/Contents/MacOS",
"/opt/binaryninja",
]
},
},
}
}
pub fn find_home(&self, home_opt: Option<&Path>) -> Result<PathBuf> {
if let Some(h) = home_opt {
return Ok(h.to_path_buf());
}
self.home_spec()
.find_in(|k| std::env::var(k).ok(), |p| p.exists())
}
}
impl std::fmt::Display for Ir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl Ir {
fn as_str(&self) -> &'static str {
match self {
Ir::GhidraPcode => "ghidra-pcode",
Ir::IdaMicrocode => "ida-microcode",
Ir::BinaryNinjaLlil => "binary-ninja-llil",
Ir::BinaryNinjaMlil => "binary-ninja-mlil",
Ir::BinaryNinjaHlil => "binary-ninja-hlil",
}
}
}
impl std::str::FromStr for Ir {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"ghidra-pcode" => Ok(Ir::GhidraPcode),
"ida-microcode" => Ok(Ir::IdaMicrocode),
"binary-ninja-llil" => Ok(Ir::BinaryNinjaLlil),
"binary-ninja-mlil" => Ok(Ir::BinaryNinjaMlil),
"binary-ninja-hlil" => Ok(Ir::BinaryNinjaHlil),
_ => Err(crate::Error::Parse(format!(
"{s}: unknown intermediate representation"
))),
}
}
}
pub struct HomeSpec {
pub tool: &'static str,
pub env: &'static str,
pub candidates: &'static [&'static str],
}
impl HomeSpec {
pub(crate) fn find_in(
&self,
env: impl Fn(&str) -> Option<String>,
exists: impl Fn(&Path) -> bool,
) -> Result<PathBuf> {
if let Some(h) = env(self.env) {
return Ok(PathBuf::from(h));
}
for c in self.candidates {
let p = PathBuf::from(c);
if exists(&p) {
return Ok(p);
}
}
let looked_in = if self.candidates.is_empty() {
String::new()
} else {
format!(", or install it in one of: {}", self.candidates.join(", "))
};
Err(crate::Error::Parse(format!(
"{} not found. Specify it with --home, set {}{looked_in}",
self.tool, self.env
)))
}
}
pub struct LifterBuilder {
ir: Ir,
home: Option<PathBuf>,
script: Option<PathBuf>,
intermediate_dir: Option<PathBuf>,
}
impl LifterBuilder {
pub fn new(ir: Ir) -> Self {
Self {
ir,
home: None,
script: None,
intermediate_dir: None,
}
}
pub fn home(mut self, home: Option<PathBuf>) -> Self {
self.home = home;
self
}
pub fn script(mut self, script: Option<PathBuf>) -> Self {
self.script = script;
self
}
pub fn intermediate_dir(mut self, dir: Option<PathBuf>) -> Self {
self.intermediate_dir = dir;
self
}
pub fn build(self) -> Result<Box<dyn Lifter + Sync>> {
match self.ir {
Ir::GhidraPcode => {
let home = self.ir.find_home(self.home.as_deref())?;
Ok(Box::new(Verifying {
inner: crate::ghidra::lifter::GhidraLifter::new(
home,
self.script,
self.intermediate_dir,
),
ir: self.ir,
}))
}
Ir::IdaMicrocode => {
let home = self.ir.find_home(self.home.as_deref())?;
Ok(Box::new(Verifying {
inner: crate::ida::lifter::IdaLifter::new(
home,
self.script,
self.intermediate_dir,
),
ir: self.ir,
}))
}
Ir::BinaryNinjaLlil | Ir::BinaryNinjaMlil | Ir::BinaryNinjaHlil => {
let Some(level) = crate::binaryninja::level(self.ir) else {
return Err(not_implemented(self.ir));
};
let home = self.ir.find_home(self.home.as_deref())?;
Ok(Box::new(Verifying {
inner: crate::binaryninja::lifter::BinaryNinjaLifter::new(
home,
level,
self.script,
self.intermediate_dir,
),
ir: self.ir,
}))
}
}
}
}
fn not_implemented(ir: Ir) -> crate::Error {
crate::Error::Parse(format!(
"no lifter writes {ir} yet: the {} backend is not implemented",
ir.tool()
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_every_representation_spells_itself_the_same_way_three_times() {
for ir in Ir::value_variants() {
let json = serde_json::to_string(ir).unwrap();
let via_serde = json.trim_matches('"');
assert_eq!(via_serde, ir.to_string(), "serde and Display disagree");
let via_clap = ir.to_possible_value().unwrap();
assert_eq!(
via_clap.get_name(),
ir.to_string(),
"clap and Display disagree"
);
assert_eq!(
&<Ir as FromStr>::from_str(via_serde).unwrap(),
ir,
"FromStr does not read what serde writes: {via_serde}"
);
}
}
#[test]
fn test_every_representation_names_a_tool() {
for ir in Ir::value_variants() {
assert!(!ir.tool().is_empty(), "{ir} names no tool");
assert!(!ir.home_spec().env.is_empty(), "{ir} names no variable");
}
}
#[test]
fn test_the_home_the_user_passed_wins() {
let opt = PathBuf::from("/custom/ghidra/home");
assert_eq!(Ir::GhidraPcode.find_home(Some(&opt)).unwrap(), opt);
}
#[test]
fn test_the_environment_variable_is_read_when_no_home_was_passed() {
let spec = Ir::GhidraPcode.home_spec();
let home = spec
.find_in(
|k| (k == "GHIDRA_HOME").then(|| "/env/ghidra/home".to_string()),
|_| panic!("the usual locations were searched despite GHIDRA_HOME being set"),
)
.unwrap();
assert_eq!(home, PathBuf::from("/env/ghidra/home"));
}
#[test]
fn test_the_environment_variable_beats_an_installed_ghidra() {
let spec = Ir::GhidraPcode.home_spec();
let home = spec
.find_in(|_| Some("/env/ghidra/home".to_string()), |_| true)
.unwrap();
assert_eq!(home, PathBuf::from("/env/ghidra/home"));
}
fn a_spec_with_candidates() -> HomeSpec {
HomeSpec {
tool: "Ghidra",
env: "GHIDRA_HOME",
candidates: &["/first/ghidra", "/second/ghidra", "/third/ghidra"],
}
}
#[test]
fn test_the_usual_locations_are_searched_when_the_variable_is_unset() {
let spec = a_spec_with_candidates();
let last = Path::new(spec.candidates.last().unwrap());
let home = spec.find_in(|_| None, |p| p == last).unwrap();
assert_eq!(home, last);
}
#[test]
fn test_the_first_of_several_installations_wins() {
let spec = a_spec_with_candidates();
let home = spec.find_in(|_| None, |_| true).unwrap();
assert_eq!(home, PathBuf::from(spec.candidates[0]));
}
#[test]
fn test_a_ghidra_that_is_nowhere_says_what_to_set_and_where_it_looked() {
let spec = Ir::GhidraPcode.home_spec();
let err = spec.find_in(|_| None, |_| false).unwrap_err().to_string();
assert!(err.contains("--home"), "does not offer --home: {err}");
assert!(
err.contains("GHIDRA_HOME"),
"does not name the variable: {err}"
);
for c in spec.candidates {
assert!(err.contains(c), "does not say it looked in {c}: {err}");
}
}
#[test]
fn test_a_backend_with_no_usual_locations_does_not_offer_an_empty_list() {
let spec = HomeSpec {
tool: "Nowhere",
env: "NOWHERE_HOME",
candidates: &[],
};
let err = spec
.find_in(|_| None, |_| panic!("there is nothing to look at"))
.unwrap_err()
.to_string();
assert!(
err.contains("NOWHERE_HOME"),
"does not name the variable: {err}"
);
assert!(
!err.contains("install it in one of"),
"offers an empty list: {err}"
);
}
#[cfg(not(windows))]
#[test]
fn test_every_representation_names_somewhere_to_look() {
for ir in Ir::value_variants() {
assert!(
!ir.home_spec().candidates.is_empty(),
"{ir} offers no install locations"
);
}
}
#[cfg(windows)]
#[test]
fn test_no_representation_guesses_where_windows_keeps_things() {
for ir in Ir::value_variants() {
assert!(
ir.home_spec().candidates.is_empty(),
"{ir} offers an install location this platform does not have"
);
}
}
}
#[cfg(test)]
mod verifying_tests {
use super::*;
use crate::Error;
struct Writes(&'static str);
impl Lifter for Writes {
fn lift(&self, _input: &Path, output: &Path) -> Result<()> {
std::fs::write(output, self.0).map_err(|e| Error::Io(output.to_path_buf(), e))
}
}
struct Fails;
impl Lifter for Fails {
fn lift(&self, _input: &Path, _output: &Path) -> Result<()> {
Err(Error::Parse("the decompiler said no".to_string()))
}
}
const A_READABLE_PROGRAM: &str = r#"{
"program": "sample",
"path": "bin/sample",
"ir": "ghidra-pcode",
"symbols": {},
"functions": [
{"name": "main", "ops": [{"op": "COPY", "inputs": ["r0"]}]}
]
}"#;
const A_PROGRAM_WITH_NO_FUNCTIONS: &str = r#"{
"program": "sample",
"path": "bin/sample",
"ir": "ghidra-pcode",
"symbols": {},
"functions": []
}"#;
fn lift_with<L: Lifter>(lifter: L, input: &str) -> (Result<()>, tempfile::TempDir) {
lift_as(lifter, input, Ir::GhidraPcode)
}
fn lift_written(json: &str, ir: Ir) -> (Result<()>, tempfile::TempDir) {
struct Nothing;
impl Lifter for Nothing {
fn lift(&self, _i: &Path, _o: &Path) -> Result<()> {
Ok(())
}
}
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("sample.json");
std::fs::write(&output, json).unwrap();
let r = Verifying { inner: Nothing, ir }.lift(Path::new("bin/sample"), &output);
(r, dir)
}
fn lift_as<L: Lifter>(lifter: L, input: &str, ir: Ir) -> (Result<()>, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("sample.json");
let r = Verifying { inner: lifter, ir }.lift(Path::new(input), &output);
(r, dir)
}
#[test]
fn test_a_readable_output_is_a_successful_lift() {
let (r, _dir) = lift_with(Writes(A_READABLE_PROGRAM), "bin/sample");
assert!(r.is_ok(), "{:?}", r.unwrap_err().to_string());
}
#[test]
fn test_an_unparseable_output_fails_the_lift() {
let (r, _dir) = lift_with(Writes("{\"functions\": ["), "bin/sample");
let e = r.expect_err("a file that does not parse is not a successful lift");
let rendered = e.to_string();
assert!(rendered.contains("bin/sample:"), "{rendered}");
assert!(rendered.contains("sample.json"), "{rendered}");
assert!(rendered.contains("reported success"), "{rendered}");
}
#[test]
fn test_the_underlying_error_is_kept_as_the_cause() {
use std::error::Error as _;
let (r, _dir) = lift_with(Writes("{\"functions\": ["), "bin/sample");
let e = r.unwrap_err();
let source = e.source().expect("the parse failure is the cause");
assert!(source.to_string().contains("JSON error"), "{source}");
assert!(e.to_string().contains("JSON error"), "{e}");
}
#[test]
fn test_an_output_in_an_unknown_representation_fails_the_lift() {
let json = A_READABLE_PROGRAM.replace("ghidra-pcode", "llvm-ir");
let (r, _dir) = lift_written(&json, Ir::GhidraPcode);
let e = r.expect_err("a representation with no reader is not a successful lift");
assert!(e.to_string().contains("llvm-ir"), "{e}");
}
#[test]
fn test_an_output_with_no_functions_is_still_a_successful_lift() {
let (r, _dir) = lift_with(Writes(A_PROGRAM_WITH_NO_FUNCTIONS), "bin/sample");
assert!(
r.is_ok(),
"a program with no functions is not a failure: {:?}",
r.map_err(|e| e.to_string())
);
}
#[test]
fn test_the_no_functions_warning_names_both_readings() {
let m = no_functions_message(Path::new("bin/sample"), Ir::GhidraPcode);
assert!(m.contains("bin/sample:"), "{m}");
assert!(m.contains("no functions"), "{m}");
assert!(m.contains("really has no functions"), "{m}");
assert!(m.contains("the tool found none"), "{m}");
assert!(m.contains("perfect match"), "{m}");
}
#[test]
fn test_only_ghidra_is_told_about_the_decompiler() {
let ghidra = no_functions_message(Path::new("bin/sample"), Ir::GhidraPcode);
assert!(ghidra.contains("buildNatives"), "{ghidra}");
for ir in [Ir::BinaryNinjaHlil, Ir::IdaMicrocode] {
let other = no_functions_message(Path::new("bin/sample"), ir);
assert!(other.contains("no functions"), "{other}");
assert!(other.contains(&ir.to_string()), "{other}");
assert!(!other.contains("buildNatives"), "{other}");
}
}
#[test]
fn test_a_failing_lifter_is_not_reported_as_an_unreadable_output() {
let (r, _dir) = lift_with(Fails, "bin/sample");
let e = r.unwrap_err();
assert_eq!(e.to_string(), "Parse error: the decompiler said no");
assert!(!e.to_string().contains("cannot be read back"), "{e}");
}
}