idakit-sys 0.2.0

Raw FFI bindings to IDA Pro's idalib runtime and the idakit C++ facade
Documentation
//! The fatal-trap mechanism in isolation. [`test_fatal`] runs a chosen fatal inside the
//! facade's `guarded<>` wrapper; the trap must convert it to [`idakit_sys::EXIT_TRAPPED`] rather
//! than terminate the process. nextest isolates each test in its own process, so a fatal that
//! escapes the trap fails only that test.

use idakit_sys::{
    EXIT_TRAPPED, FATAL_ABORT, FATAL_EXIT, FATAL_INTERR, drop_probe_count, drop_probe_make,
    ext_throw_coded, ext_throw_interr, ext_throw_plain_int, last_exit_code, probe_throw,
    test_fatal, test_fatal_through_cxx, was_trapped,
};
use rstest::rstest;

/// `trap_exit`'s fixed status (`runtime.cpp`'s `test_fatal`/`trigger_fatal`, `kind ==
/// FATAL_EXIT`).
const TRAP_EXIT_CODE: i32 = 42;
/// `128 + SIGABRT`, `trap_abort`'s fixed status (`runtime.cpp`'s `ABORT_EXIT_CODE`).
const TRAP_ABORT_CODE: i32 = 134;

#[rstest]
#[case::exit(FATAL_EXIT, TRAP_EXIT_CODE)]
#[case::abort(FATAL_ABORT, TRAP_ABORT_CODE)]
fn fatal_inside_guarded_call_is_trapped(#[case] kind: i32, #[case] expected_exit_code: i32) {
    // SAFETY: the shim calls exit()/abort() inside guarded<>; the trap longjmps back instead of
    // tearing down the process.
    let rc = unsafe { test_fatal(kind) };
    assert_eq!(rc, EXIT_TRAPPED);
    // SAFETY: reads thread-local state guarded<> just wrote; no invariants beyond that.
    assert_eq!(
        unsafe { was_trapped() },
        1,
        "guarded<> must record that it trapped a fatal"
    );
    assert_eq!(
        unsafe { last_exit_code() },
        expected_exit_code,
        "the trapped status/signal code must survive the longjmp"
    );
}

// Unlike exit/abort (trapped by the shim's direct longjmp), interr relies on idalib throwing
// interr_exc_t across the libida boundary for guarded<> to catch. That throw is only trapped on
// Linux; off it the exception is not caught and the process aborts, so the check runs there only.
#[test]
#[cfg_attr(
    not(target_os = "linux"),
    ignore = "interr trapping needs a catchable throw across the libida boundary (Linux-only)"
)]
fn interr_inside_guarded_call_is_trapped() {
    // SAFETY: the shim calls interr() inside guarded<>; set_interr_throws makes it a catchable
    // interr_exc_t, so the guard returns instead of terminating.
    let rc = unsafe { test_fatal(FATAL_INTERR) };
    assert_eq!(rc, EXIT_TRAPPED);
    // SAFETY: reads thread-local state guarded<> just wrote; no invariants beyond that.
    assert_eq!(
        unsafe { was_trapped() },
        1,
        "guarded<>'s interr_exc_t catch must record a trap the same as the longjmp paths"
    );
}

// Force the dangerous topology the ordinary trap tests never hit: a
// guarded<> setjmp ABOVE a cxx `Result`-shim, with the fatal firing from C++ BELOW the shim, so
// the trap's longjmp must unwind *through* the cxx-generated try/catch landing-pad frame. If a
// longjmp past a cxx shim corrupted the C++ runtime or the stack, these would crash or hang
// rather than return the sentinel cleanly. (idakit's real code never builds this topology; the
// probe is synthetic, to establish the empirical boundary.)

#[rstest]
#[case::exit(FATAL_EXIT, TRAP_EXIT_CODE)]
#[case::abort(FATAL_ABORT, TRAP_ABORT_CODE)]
fn fatal_longjmp_across_cxx_shim_is_trapped(#[case] kind: i32, #[case] expected_exit_code: i32) {
    // SAFETY: the guard arms guarded<>, then reaches exit()/abort() through the cxx shim; the
    // trap longjmps back across that shim frame instead of tearing down the process.
    let rc = unsafe { test_fatal_through_cxx(kind) };
    assert_eq!(rc, EXIT_TRAPPED);
    // SAFETY: reads thread-local state guarded<> just wrote; no invariants beyond that.
    assert_eq!(
        unsafe { was_trapped() },
        1,
        "guarded<> must record the trap even when the longjmp crossed a cxx shim frame"
    );
    assert_eq!(
        unsafe { last_exit_code() },
        expected_exit_code,
        "the trapped status/signal code must survive a longjmp across a cxx shim frame"
    );
}

// An interr is a *throw* (interr_exc_t : std::exception), not a longjmp. Fired below a cxx shim,
// cxx's own `catch (std::exception const&)` intercepts it first, so it becomes a bridge Err (the
// guard sees the shim return, not a trap) rather than reaching guarded<>'s interr catch. The guard
// entry reports this interception as rc == 1. Linux-only: interr throws across the libida boundary
// only there.
#[test]
#[cfg_attr(
    not(target_os = "linux"),
    ignore = "interr throws across the libida boundary only on Linux"
)]
fn interr_across_cxx_shim_is_intercepted_by_cxx() {
    // SAFETY: the guard reaches interr() through the cxx shim; cxx catches the throw and the shim
    // returns an Err, so the guard returns 1 (intercepted) rather than EXIT_TRAPPED.
    let rc = unsafe { test_fatal_through_cxx(FATAL_INTERR) };
    assert_eq!(
        rc, 1,
        "cxx should intercept the interr throw before the trap"
    );
    // SAFETY: reads thread-local state guarded<> just wrote; no invariants beyond that.
    assert_eq!(
        unsafe { was_trapped() },
        0,
        "the throw never reaches guarded<>'s own catch, so it must not be recorded as a trap"
    );
}

// What cxx's default trycatch actually surfaces. Its generated shim
// catches only `std::exception const&`, giving back a flat `cxx::Exception` whose `what()` is the
// C++ message and nothing else (no qerrno, no structured context). A non-std::exception throw
// (kind 2) escapes the catch and std::terminate()s the process, so it is documented, not run.
#[rstest]
#[case::runtime_error(0, "probe_throw: runtime_error from C++")]
#[case::out_of_range(1, "probe_throw: out_of_range from C++")]
fn cxx_surfaces_std_exception_as_flat_err(#[case] kind: i32, #[case] expected: &str) {
    let err = probe_throw(kind).unwrap_err();
    assert_eq!(err.what(), expected);
}

// The custom `rust::behavior::trycatch` in testonly_probe_ext.h widens what the
// bridge shims catch. cxx's default terminates on a non-`std::exception` throw; the `catch (...)`
// arm turns it into an ordinary `Err`. (If the override had failed to take, `throw 42` would
// std::terminate and abort this test process instead of returning here.)
#[test]
fn custom_trycatch_catches_non_std_exception() {
    let err = ext_throw_plain_int().unwrap_err();
    assert!(
        err.what().contains("non-std::exception"),
        "catch(...) arm should surface a non-std::exception throw as Err, got: {}",
        err.what()
    );
}

// An interr_exc_t carries only an int code and inherits the uninformative base what()
// ("std::exception"). The custom `catch (const interr_exc_t&)` arm is what makes the code legible.
#[test]
fn custom_trycatch_enriches_interr_with_its_code() {
    let err = ext_throw_interr(42).unwrap_err();
    assert!(
        err.what().contains("code=42"),
        "interr arm should format the code into the message, got: {}",
        err.what()
    );
}

// The enrichment boundary: a `cxx::Exception` carries only `what()`, one flat string, no
// qerrno/typed context. Structured data can ride only AS that string. Here a code is encoded into
// the throw message and re-parsed Rust-side. idakit's production path instead treats the throw as a
// bare failure signal and re-derives qerrno/reason from the kernel (Database::last_reason()), so the
// C++ body needs no message at all (see crates/idakit/tests/cxx_error.rs).
#[test]
fn cxx_exception_only_carries_a_string_code_must_be_encoded() {
    let err = ext_throw_coded(7).unwrap_err();
    let msg = err.what();
    let code: i32 = msg
        .strip_prefix("idakit:qerrno=")
        .expect("encoded code prefix")
        .parse()
        .expect("code parses back out of the message");
    assert_eq!(code, 7, "the only channel is the what() string");
}

// Dropping a cxx `UniquePtr<T>` runs the C++ deleter. DropProbe's destructor bumps a
// process-global counter (nextest gives each test its own process, so it starts at 0). This is the
// same generated `$unique_ptr$...$drop` glue that backs the cfg bridge's `FlowChart`, so a
// UniquePtr field needs no manual free function or hand-written Drop impl.
#[test]
fn cxx_unique_ptr_runs_cpp_deleter_on_drop() {
    assert_eq!(drop_probe_count(), 0, "counter should start clean");
    let probe = drop_probe_make();
    assert!(!probe.is_null(), "make should hand back a live handle");
    assert_eq!(
        drop_probe_count(),
        0,
        "the deleter must not run while the UniquePtr is alive"
    );
    drop(probe);
    assert_eq!(
        drop_probe_count(),
        1,
        "dropping the UniquePtr must run the C++ destructor exactly once"
    );
}