#![warn(missing_docs)]
use std::path::PathBuf;
use crate::birthmarks::BirthmarkType;
mod assignment;
mod binaryninja;
pub mod birthmarks;
pub mod compare;
pub mod extract;
mod ghidra;
mod ida;
pub mod lift;
mod oinkie;
mod program;
pub use crate::oinkie::{Oinkie, OinkieBuilder};
pub use crate::program::Program;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{}", render_group(.0))]
Array(Vec<Self>),
#[error("{0}: unknown birthmark type")]
BirthmarkType(String),
#[error("CSV error: {0}")]
Csv(#[source] csv::Error),
#[error("{}", render_incompatible(.0, .1))]
IncompatibleAnalysis(BirthmarkType, crate::compare::Algorithm),
#[error(
"cannot compare {0} against {1}: the two are lifted to different intermediate representations, whose operation vocabularies do not correspond"
)]
IrMismatch(crate::lift::Ir, crate::lift::Ir),
#[error(
"{binary}: the lifter reported success, but what it wrote cannot be read back: {cause}",
binary = .0.display(),
cause = .1
)]
UnreadableOutput(PathBuf, #[source] Box<Error>),
#[error("invalid pcode: {0}")]
InvalidPcode(u32),
#[error("IO error for {path}: {cause}", path = .0.display(), cause = .1)]
Io(PathBuf, #[source] std::io::Error),
#[error("IO error for {path}: {cause}", path = .0.display(), cause = .1)]
ToolIo(PathBuf, #[source] std::io::Error),
#[error("{path}: JSON error: {cause}", path = .0.display(), cause = .1)]
Json(PathBuf, #[source] serde_json::Error),
#[error("Mismatched birthmark types: {0} and {1}")]
Mismatch(BirthmarkType, BirthmarkType),
#[error("Parse error: {0}")]
Parse(String),
#[error("{}", render_function_sizes(.matrix, .given))]
FunctionSizes {
matrix: (usize, usize),
given: Option<(usize, usize)>,
},
#[error("{0}: Parse float error {1}")]
ParseFloat(String, #[source] std::num::ParseFloatError),
#[error("{0}: Parse int error {1}")]
ParseInt(String, #[source] std::num::ParseIntError),
#[error("could not start {0} threads: {1}")]
ThreadPool(std::num::NonZeroUsize, String),
#[error("{}", render_tool_not_found(.tool, .env, .candidates))]
ToolNotFound {
tool: &'static str,
env: &'static str,
candidates: &'static [&'static str],
},
}
fn render_function_sizes(matrix: &(usize, usize), given: &Option<(usize, usize)>) -> String {
let (rows, cols) = matrix;
let given = match given {
None => "none were given".to_string(),
Some((r, c)) => format!("{r} and {c} were given"),
};
format!(
"the weighted aggregator needs the element count of every function: the matrix is \
{rows} by {cols}, and {given}"
)
}
fn render_tool_not_found(tool: &str, env: &str, candidates: &[&str]) -> String {
let looked_in = if candidates.is_empty() {
String::new()
} else {
format!(", or install it in one of: {}", candidates.join(", "))
};
format!("{tool} not found. Give its home directory, set {env}{looked_in}")
}
fn render_group(errs: &[Error]) -> String {
let mut s = String::from("Multiple errors:");
for (i, err) in errs.iter().enumerate() {
s.push_str(&format!("\n {}. {}", i + 1, err));
}
s
}
fn render_incompatible(bt: &BirthmarkType, algorithm: &crate::compare::Algorithm) -> String {
let name = algorithm.name();
format!(
"{bt}-{name}: {name} operates on {}; use {}-{name}",
algorithm.shape().description(),
bt.with_shape(algorithm.shape())
)
}
impl Error {
pub fn is_caller_fault(&self) -> bool {
match self {
Error::BirthmarkType(_)
| Error::IncompatibleAnalysis(_, _)
| Error::Mismatch(_, _)
| Error::IrMismatch(_, _)
| Error::ParseFloat(_, _)
| Error::ParseInt(_, _) => true,
Error::Io(_, _) | Error::Json(_, _) => true,
Error::FunctionSizes { .. } => true,
Error::ToolNotFound { .. }
| Error::ThreadPool(_, _)
| Error::ToolIo(_, _)
| Error::Csv(_)
| Error::InvalidPcode(_)
| Error::UnreadableOutput(_, _) => false,
Error::Parse(_) => false,
Error::Array(errs) => errs.iter().all(Error::is_caller_fault),
}
}
pub fn vec_result_to_result_vec<T>(vec: Vec<Result<T>>) -> Result<Vec<T>> {
let mut results = Vec::new();
let mut errs = Vec::new();
for r in vec {
match r {
Ok(v) => results.push(v),
Err(e) => errs.push(e),
}
}
Self::error_or(results, errs)
}
pub(crate) fn error_or<T>(result: T, errs: Vec<Self>) -> Result<T> {
if errs.is_empty() {
Ok(result)
} else if errs.len() == 1 {
Err(errs.into_iter().next().unwrap())
} else {
Err(Self::Array(errs))
}
}
}
pub(crate) trait Op {
fn mnemonic(&self) -> &str;
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "no birthmark reads operands yet; each lifter's tests pin how its IR fills them"
)
)]
fn inputs(&self) -> &[String];
fn is_call(&self) -> bool;
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "no birthmark reads results yet; each lifter's tests pin how its IR fills them"
)
)]
fn ret(&self) -> Option<&str>;
fn symbol_key(&self) -> Option<String>;
}
pub(crate) trait Iterable {
type Item;
fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compare::Algorithm;
use crate::lift::Ir;
fn variant_name(e: &Error) -> &'static str {
match e {
Error::Array(_) => "Array",
Error::BirthmarkType(_) => "BirthmarkType",
Error::Csv(_) => "Csv",
Error::IncompatibleAnalysis(_, _) => "IncompatibleAnalysis",
Error::IrMismatch(_, _) => "IrMismatch",
Error::UnreadableOutput(_, _) => "UnreadableOutput",
Error::InvalidPcode(_) => "InvalidPcode",
Error::Io(_, _) => "Io",
Error::ThreadPool(_, _) => "ThreadPool",
Error::ToolIo(_, _) => "ToolIo",
Error::Json(_, _) => "Json",
Error::Mismatch(_, _) => "Mismatch",
Error::Parse(_) => "Parse",
Error::ParseFloat(_, _) => "ParseFloat",
Error::ParseInt(_, _) => "ParseInt",
Error::ToolNotFound { .. } => "ToolNotFound",
Error::FunctionSizes { .. } => "FunctionSizes",
}
}
fn rendered_errors() -> Vec<(Error, String)> {
let csv_err = csv::ReaderBuilder::new()
.has_headers(false)
.from_reader("a,b\nc\n".as_bytes())
.records()
.nth(1)
.unwrap()
.unwrap_err();
let json_err = serde_json::from_str::<i32>("nope").unwrap_err();
let float_err = "x".parse::<f64>().unwrap_err();
let int_err = "x".parse::<i32>().unwrap_err();
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
let tool_io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
let (csv_msg, json_msg) = (csv_err.to_string(), json_err.to_string());
let (float_msg, int_msg) = (float_err.to_string(), int_err.to_string());
let io_msg = io_err.to_string();
vec![
(
Error::Array(vec![
Error::Parse("first".to_string()),
Error::Parse("second".to_string()),
]),
"Multiple errors:\n 1. Parse error: first\n 2. Parse error: second".to_string(),
),
(
Error::BirthmarkType("nonsense".to_string()),
"nonsense: unknown birthmark type".to_string(),
),
(Error::Csv(csv_err), format!("CSV error: {csv_msg}")),
(
Error::IncompatibleAnalysis(BirthmarkType::OpSeq, Algorithm::Euclidean),
"op-seq-euclidean: euclidean operates on frequency vectors; use op-freq-euclidean"
.to_string(),
),
(
Error::IrMismatch(Ir::GhidraPcode, Ir::IdaMicrocode),
"cannot compare ghidra-pcode against ida-microcode: the two are lifted to different intermediate representations, whose operation vocabularies do not correspond".to_string(),
),
(
Error::UnreadableOutput(
PathBuf::from("bin/sample"),
Box::new(Error::Json(
PathBuf::from("pcodes/sample.json"),
serde_json::from_str::<i32>("nope").unwrap_err(),
)),
),
format!(
"bin/sample: the lifter reported success, but what it wrote cannot be read back: pcodes/sample.json: JSON error: {json_msg}"
),
),
(
Error::InvalidPcode(9999),
"invalid pcode: 9999".to_string(),
),
(
Error::Io(PathBuf::from("missing.json"), io_err),
format!("IO error for missing.json: {io_msg}"),
),
(
Error::ToolIo(PathBuf::from("ghidra/support/analyzeHeadless"), tool_io_err),
format!("IO error for ghidra/support/analyzeHeadless: {io_msg}"),
),
(
Error::Json(PathBuf::from("broken.json"), json_err),
format!("broken.json: JSON error: {json_msg}"),
),
(
Error::Mismatch(BirthmarkType::OpSeq, BirthmarkType::OpKgramSet(3)),
"Mismatched birthmark types: op-seq and op-3gram-set".to_string(),
),
(
Error::Parse("something went wrong".to_string()),
"Parse error: something went wrong".to_string(),
),
(
Error::ParseFloat("x".to_string(), float_err),
format!("x: Parse float error {float_msg}"),
),
(
Error::ParseInt("x".to_string(), int_err),
format!("x: Parse int error {int_msg}"),
),
(
Error::ThreadPool(
std::num::NonZeroUsize::new(4).unwrap(),
"Resource temporarily unavailable".to_string(),
),
"could not start 4 threads: Resource temporarily unavailable".to_string(),
),
(
Error::ToolNotFound {
tool: "Ghidra",
env: "GHIDRA_HOME",
candidates: &["/opt/ghidra", "/usr/local/ghidra"],
},
"Ghidra not found. Give its home directory, set GHIDRA_HOME, or install it in one of: /opt/ghidra, /usr/local/ghidra".to_string(),
),
(
Error::FunctionSizes {
matrix: (2, 3),
given: None,
},
"the weighted aggregator needs the element count of every function: the matrix is 2 by 3, and none were given".to_string(),
),
]
}
#[test]
fn test_the_catch_all_does_not_claim_the_caller_is_at_fault() {
let e = Error::Parse("could not start 4 lift jobs".to_string());
assert!(!e.is_caller_fault());
}
#[test]
fn test_a_bad_name_is_the_callers_fault_and_a_broken_file_of_ours_is_not() {
assert!(Error::BirthmarkType("nonsense".to_string()).is_caller_fault());
assert!(!Error::InvalidPcode(9999).is_caller_fault());
let missing = Error::ToolNotFound {
tool: "Ghidra",
env: "GHIDRA_HOME",
candidates: &[],
};
assert!(!missing.is_caller_fault(), "no argument installs a tool");
let not_started = Error::ToolIo(
PathBuf::from("ghidra/support/analyzeHeadless"),
std::io::Error::new(std::io::ErrorKind::NotFound, "no such file"),
);
assert!(
!not_started.is_caller_fault(),
"a path oinkie chose is not one the caller can name differently"
);
}
#[test]
fn test_a_file_the_caller_named_is_the_callers_fault() {
let missing = Error::Io(
PathBuf::from("no/such.json"),
std::io::Error::new(std::io::ErrorKind::NotFound, "no such file"),
);
assert!(missing.is_caller_fault());
let json = serde_json::from_str::<i32>("nope").unwrap_err();
assert!(Error::Json(PathBuf::from("bad.json"), json).is_caller_fault());
}
#[test]
fn test_a_group_of_the_callers_mistakes_is_the_callers_fault() {
let e = Error::Array(vec![
Error::BirthmarkType("nonsense".to_string()),
Error::BirthmarkType("also nonsense".to_string()),
]);
assert!(e.is_caller_fault());
}
#[test]
fn test_one_internal_failure_makes_the_whole_group_internal() {
let e = Error::Array(vec![
Error::BirthmarkType("the caller's".to_string()),
Error::UnreadableOutput(
PathBuf::from("bin/sample"),
Box::new(Error::Parse("ours".to_string())),
),
]);
assert!(!e.is_caller_fault());
}
#[test]
fn test_every_error_says_what_it_is() {
for (err, expected) in rendered_errors() {
assert_eq!(err.to_string(), expected, "{}", variant_name(&err));
}
}
#[test]
fn test_no_variant_is_in_the_table_twice() {
let mut names = rendered_errors()
.iter()
.map(|(e, _)| variant_name(e))
.collect::<Vec<_>>();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "a variant is covered twice: {names:?}");
}
#[test]
fn test_a_group_of_errors_numbers_its_children_from_one() {
let e = Error::Array(vec![
Error::Parse("a".to_string()),
Error::Parse("b".to_string()),
Error::Parse("c".to_string()),
]);
let rendered = e.to_string();
assert!(rendered.contains("\n 1. Parse error: a"), "{rendered}");
assert!(rendered.contains("\n 3. Parse error: c"), "{rendered}");
assert!(!rendered.contains("0."), "numbered from zero: {rendered}");
}
#[test]
fn test_a_wrapped_error_is_reachable_as_a_source() {
use std::error::Error as _;
let inner = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let inner_msg = inner.to_string();
let e = Error::Io(PathBuf::from("locked.json"), inner);
let source = e.source().expect("the io::Error is the cause");
assert_eq!(source.to_string(), inner_msg);
let e = Error::Json(
PathBuf::from("broken.json"),
serde_json::from_str::<i32>("nope").unwrap_err(),
);
assert!(e.source().is_some(), "the serde_json::Error is the cause");
}
#[test]
fn test_an_error_of_our_own_has_no_source() {
use std::error::Error as _;
assert!(Error::Parse("ours".to_string()).source().is_none());
assert!(Error::InvalidPcode(1).source().is_none());
assert!(
Error::Array(vec![Error::Parse("child".to_string())])
.source()
.is_none()
);
}
#[test]
fn test_no_errors_is_not_an_error() {
let r: Result<Vec<i32>> = Error::vec_result_to_result_vec(vec![Ok(1), Ok(2)]);
assert_eq!(r.unwrap(), vec![1, 2]);
assert_eq!(Error::error_or("kept", vec![]).unwrap(), "kept");
}
#[test]
fn test_one_error_is_not_wrapped_in_a_group() {
let r: Result<Vec<i32>> =
Error::vec_result_to_result_vec(vec![Ok(1), Err(Error::Parse("only".to_string()))]);
let err = r.unwrap_err();
assert_eq!(variant_name(&err), "Parse");
assert_eq!(err.to_string(), "Parse error: only");
}
#[test]
fn test_several_errors_are_grouped_and_none_is_dropped() {
let r: Result<Vec<i32>> = Error::vec_result_to_result_vec(vec![
Err(Error::Parse("first".to_string())),
Ok(1),
Err(Error::Parse("second".to_string())),
]);
let err = r.unwrap_err();
assert_eq!(variant_name(&err), "Array");
let rendered = err.to_string();
assert!(rendered.contains("first"), "{rendered}");
assert!(rendered.contains("second"), "{rendered}");
}
}