use std::path::{Path, PathBuf};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::lift::Ir;
use crate::{Error, Iterable, Result};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct TypedProgram<T> {
#[serde(rename = "program")]
name: String,
path: PathBuf,
#[serde(default)]
ir: crate::lift::Ir,
symbols: FxHashMap<String, String>,
functions: Vec<TypedFunction<T>>,
#[serde(skip)]
pub(crate) json_path: Option<PathBuf>,
}
impl<T> TryFrom<PathBuf> for TypedProgram<T>
where
T: DeserializeOwned + crate::Op,
{
type Error = Error;
fn try_from(path: PathBuf) -> Result<Self> {
let bytes = std::fs::read(&path).map_err(|e| Error::Io(path.clone(), e))?;
serde_json::from_slice(&bytes).map_err(|e| Error::Json(path, e))
}
}
impl<T> TryFrom<&Path> for TypedProgram<T>
where
T: DeserializeOwned + crate::Op,
{
type Error = Error;
fn try_from(path: &Path) -> Result<Self> {
Self::try_from(path.to_path_buf())
}
}
#[derive(Debug)]
pub struct Program(pub(crate) Lifted);
#[derive(Debug)]
pub(crate) enum Lifted {
GhidraPcode(TypedProgram<crate::ghidra::Op>),
BinaryNinjaLlil(TypedProgram<crate::binaryninja::Op<crate::binaryninja::Llil>>),
BinaryNinjaMlil(TypedProgram<crate::binaryninja::Op<crate::binaryninja::Mlil>>),
BinaryNinjaHlil(TypedProgram<crate::binaryninja::Op<crate::binaryninja::Hlil>>),
IdaMicrocode(TypedProgram<crate::ida::Op>),
}
#[derive(Deserialize)]
struct IrProbe {
#[serde(default)]
ir: Ir,
}
macro_rules! dispatch {
($self:expr, $p:ident => $body:expr) => {
match $self {
Lifted::GhidraPcode($p) => $body,
Lifted::BinaryNinjaLlil($p) => $body,
Lifted::BinaryNinjaMlil($p) => $body,
Lifted::BinaryNinjaHlil($p) => $body,
Lifted::IdaMicrocode($p) => $body,
}
};
}
impl Program {
pub fn load(path: &Path) -> Result<Self> {
let bytes = std::fs::read(path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
let json_err = |e| Error::Json(path.to_path_buf(), e);
let probe: IrProbe = serde_json::from_slice(&bytes).map_err(json_err)?;
match probe.ir {
Ir::GhidraPcode => serde_json::from_slice(&bytes)
.map(|p| Self(Lifted::GhidraPcode(p)))
.map_err(json_err),
Ir::BinaryNinjaLlil => serde_json::from_slice(&bytes)
.map(|p| Self(Lifted::BinaryNinjaLlil(p)))
.map_err(json_err),
Ir::BinaryNinjaMlil => serde_json::from_slice(&bytes)
.map(|p| Self(Lifted::BinaryNinjaMlil(p)))
.map_err(json_err),
Ir::BinaryNinjaHlil => serde_json::from_slice(&bytes)
.map(|p| Self(Lifted::BinaryNinjaHlil(p)))
.map_err(json_err),
Ir::IdaMicrocode => serde_json::from_slice(&bytes)
.map(|p| Self(Lifted::IdaMicrocode(p)))
.map_err(json_err),
}
}
pub fn ir(&self) -> Ir {
dispatch!(&self.0, p => p.ir())
}
pub fn name(&self) -> &str {
dispatch!(&self.0, p => p.name())
}
pub fn path(&self) -> &Path {
dispatch!(&self.0, p => p.path())
}
pub fn len(&self) -> usize {
dispatch!(&self.0, p => p.len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn set_json_path(&mut self, path: PathBuf) {
dispatch!(&mut self.0, p => p.set_json_path(path))
}
pub fn json_path(&self) -> Option<&Path> {
dispatch!(&self.0, p => p.json_path.as_deref())
}
pub fn symbol_count(&self) -> usize {
dispatch!(&self.0, p => p.symbols.len())
}
pub fn function_names(&self) -> Vec<&str> {
dispatch!(&self.0, p => p.functions.iter().map(|f| f.name.as_str()).collect())
}
}
impl<T> TypedProgram<T> {
pub fn set_json_path(&mut self, path: PathBuf) {
self.json_path = Some(path);
}
pub fn ir(&self) -> crate::lift::Ir {
self.ir
}
pub fn name(&self) -> &str {
&self.name
}
pub fn len(&self) -> usize {
self.functions.len()
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn symbol(&self, addr: &str) -> Option<&String> {
self.symbols.get(addr)
}
}
impl<T> Iterable for TypedProgram<T> {
type Item = TypedFunction<T>;
fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_> {
Box::new(self.functions.iter())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct TypedFunction<T> {
name: String,
ops: Vec<T>,
}
impl<T: crate::Op> TypedFunction<T> {
pub fn name(&self) -> &str {
&self.name
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.ops.iter()
}
pub fn ops(&self) -> impl Iterator<Item = &str> {
self.ops.iter().map(crate::Op::mnemonic)
}
pub fn ops_freq(&self) -> rustc_hash::FxHashMap<String, usize> {
crate::extract::seq_to_freq(self.ops().map(ToString::to_string))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lift::Ir;
#[test]
fn test_ir_defaults_to_ghidra_pcode_when_absent() {
let json = r#"{
"program": "sample",
"path": "bin/sample",
"symbols": {},
"functions": []
}"#;
let program: TypedProgram<crate::ghidra::Op> =
serde_json::from_str(json).expect("a file without the field must still load");
assert_eq!(program.ir(), Ir::GhidraPcode);
}
#[test]
fn test_ir_is_read_from_the_file() {
let json = r#"{
"program": "sample",
"path": "bin/sample",
"ir": "ghidra-pcode",
"symbols": {},
"functions": []
}"#;
let program: TypedProgram<crate::ghidra::Op> = serde_json::from_str(json).unwrap();
assert_eq!(program.ir(), Ir::GhidraPcode);
}
#[test]
fn test_ir_survives_a_round_trip() {
let program: TypedProgram<crate::ghidra::Op> = TypedProgram {
name: "sample".to_string(),
path: PathBuf::from("bin/sample"),
ir: Ir::GhidraPcode,
symbols: FxHashMap::default(),
functions: vec![],
json_path: None,
};
let json = serde_json::to_string(&program).unwrap();
assert!(
json.contains("\"ir\":\"ghidra-pcode\""),
"the field must be written, not only read: {json}"
);
let back: TypedProgram<crate::ghidra::Op> = serde_json::from_str(&json).unwrap();
assert_eq!(back.ir(), Ir::GhidraPcode);
}
#[test]
fn test_any_program_reads_ghidra_pcode() {
let program = Program::load(Path::new("testdata/lifted/pcodes/hello_clang.json"))
.expect("the fixture must load");
assert_eq!(program.ir(), Ir::GhidraPcode);
assert_eq!(program.name(), "hello_clang");
assert_eq!(program.len(), 1);
}
#[test]
fn test_any_program_reads_a_file_without_the_field() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("legacy.json");
std::fs::write(
&path,
r#"{"program":"legacy","path":"bin/legacy","symbols":{},"functions":[]}"#,
)
.unwrap();
let program = Program::load(&path).expect("a file without the field must still load");
assert_eq!(program.ir(), Ir::GhidraPcode);
}
#[test]
fn test_any_program_refuses_a_representation_it_does_not_know() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("foreign.json");
std::fs::write(
&path,
r#"{"program":"foreign","path":"bin/foreign","ir":"llvm-ir",
"symbols":{},"functions":[{"name":"main","ops":[
{"op":"call","inputs":["r0"]}]}]}"#,
)
.unwrap();
let err = match Program::load(&path) {
Err(e) => e.to_string(),
Ok(_) => panic!("a representation with no reader was loaded"),
};
assert!(
err.contains("llvm-ir"),
"does not name what it could not read: {err}"
);
}
#[test]
fn test_an_unknown_opcode_is_still_an_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bogus.json");
std::fs::write(
&path,
r#"{"program":"bogus","path":"bin/bogus","ir":"ghidra-pcode",
"symbols":{},"functions":[{"name":"main","ops":[
{"op":"LLIL_SET_REG","inputs":["(register, 0x0, 8)"]}]}]}"#,
)
.unwrap();
assert!(matches!(Program::load(&path), Err(Error::Json(_, _))));
}
#[test]
fn test_loading_a_program_reports_the_right_kind_of_failure() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("missing.json");
assert!(matches!(
TypedProgram::<crate::ghidra::Op>::try_from(missing),
Err(Error::Io(..))
));
let broken = dir.path().join("broken.json");
std::fs::write(&broken, b"{ not json").unwrap();
assert!(matches!(
TypedProgram::<crate::ghidra::Op>::try_from(broken),
Err(Error::Json(..))
));
let not_utf8 = dir.path().join("not-utf8.json");
std::fs::write(¬_utf8, b"{\"program\": \"\xff\xfe\"}").unwrap();
assert!(matches!(
TypedProgram::<crate::ghidra::Op>::try_from(not_utf8),
Err(Error::Json(..))
));
}
#[test]
fn test_reading_the_file_whole_parses_what_the_text_says() {
let fixture = Path::new("testdata/lifted/pcodes/hello_clang.json");
let loaded: TypedProgram<crate::ghidra::Op> = fixture.try_into().unwrap();
let parsed: TypedProgram<crate::ghidra::Op> =
serde_json::from_str(&std::fs::read_to_string(fixture).unwrap()).unwrap();
assert_eq!(loaded.name(), parsed.name());
assert_eq!(loaded.ir(), parsed.ir());
assert_eq!(loaded.len(), parsed.len());
assert_eq!(loaded.path(), parsed.path());
}
#[test]
fn test_fixtures_record_their_ir() {
for fixture in [
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/hello_gcc.json",
] {
let program: TypedProgram<crate::ghidra::Op> = Path::new(fixture)
.try_into()
.unwrap_or_else(|e| panic!("{fixture}: {e}"));
assert_eq!(program.ir(), Ir::GhidraPcode, "fixture: {fixture}");
let raw = std::fs::read_to_string(fixture).unwrap();
assert!(
raw.contains("\"ir\""),
"{fixture}: the fixture predates the field and should be regenerated"
);
}
}
}