use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use assert2::assert;
use idakit::prelude::*;
use rstest::rstest;
mod common;
struct Scratch(PathBuf);
impl Scratch {
fn new(name: &str, bytes: &[u8]) -> Self {
let path = std::env::temp_dir().join(name);
fs::write(&path, bytes).expect("write scratch file");
Self(path)
}
fn missing(name: &str) -> Self {
Self(std::env::temp_dir().join(name))
}
fn directory() -> Self {
Self(std::env::temp_dir())
}
fn truncated(name: &str, src: impl AsRef<Path>, len: usize) -> Self {
let mut file = fs::File::open(src).expect("open source db");
let mut buf = vec![0u8; len];
let n = file.read(&mut buf).expect("read source prefix");
buf.truncate(n);
Self::new(name, &buf)
}
fn path(&self) -> String {
self.0.to_str().expect("utf-8 temp path").to_owned()
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
fn open_is_rejected(idb: &mut Database, path: &str) {
let err = match idb.open(path).call() {
Ok(()) => panic!("open of {path:?} should be rejected, but it opened"),
Err(e) => e,
};
match &err {
Error::Open {
path: reported,
reason,
..
} => {
assert!(
reported == path,
"Error::Open.path {reported:?} disagrees with the requested {path:?}"
);
assert!(
!reason.is_empty(),
"Error::Open.reason is empty for {path:?}"
);
}
Error::KernelExit { code, .. } => {
assert!(*code != 0, "KernelExit.code is 0 for a rejected open");
}
other => panic!("open of {path:?} rejected with an unexpected error shape: {other:?}"),
}
}
fn assert_open_rejected(path: String) {
Ida::run(move |ida| {
ida.call(move |idb| open_is_rejected(idb, &path))
.unwrap_or_else(|e| e.resume());
})
.expect("kernel init failed");
}
#[rstest]
#[case::nonexistent_path(Scratch::missing("idakit-faults-missing.i64"))]
#[case::empty_file(Scratch::new("idakit-faults-empty.i64", b""))]
#[case::garbage_bytes(Scratch::new("idakit-faults-garbage.i64", &[0xABu8; 4096]))]
#[case::directory_path(Scratch::directory())]
fn bad_input_is_rejected(#[case] scratch: Scratch) {
assert_open_rejected(scratch.path());
}
#[test]
fn parked_32bit_database_is_rejected() {
let Some(source) = idakit::corpus::parked_fixture() else {
eprintln!("skipping: no parked (32-bit) fixture in the corpus manifest");
return;
};
let copy = common::TestDb::copy_of(&source);
assert_open_rejected(copy.path().to_owned());
}
#[test]
#[cfg_attr(
not(target_os = "linux"),
ignore = "the rejection makes idalib exit(); trapping it needs the Linux-only exit trap"
)]
fn unsupported_java_class_reports_reason() {
const TOO_NEW_CLASS: &[u8] = &[
0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x45, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let scratch = Scratch::new("idakit-faults-toonew.class", TOO_NEW_CLASS);
let path = scratch.path();
Ida::run(move |ida| {
ida.call(move |idb| {
let err = idb
.open(&path)
.call()
.expect_err("too-new class should be rejected");
assert!(let Error::KernelExit { diagnostic, .. } = &err);
let diag = diagnostic.as_deref().unwrap_or("");
assert!(
diag.contains("Java file format"),
"KernelExit diagnostic should carry the loader reason, got {diagnostic:?}"
);
})
.unwrap_or_else(|e| e.resume());
})
.expect("kernel init failed");
}
#[test]
#[cfg_attr(
not(target_os = "linux"),
ignore = "a corrupt-header database makes idalib call exit(); trapping that needs the \
GOT-redirect exit trap, which is Linux-only (elsewhere the process just exits)"
)]
fn truncated_database_is_rejected() {
let Some(db) = common::TestDb::source() else {
return;
};
let scratch = Scratch::truncated("idakit-faults-truncated.i64", &db, 4096);
assert_open_rejected(scratch.path());
}