idakit 0.2.0

Idiomatic Rust bindings for IDA Pro's idalib kernel
Documentation
//! Shared helpers for the kernel-touching integration tests.
// Each test binary pulls in this whole module but uses only a subset of it.
#![allow(dead_code, unused_imports, unused_macros)]

pub mod checks;
pub mod kernel;
mod macros;
pub mod registry;

pub(crate) use macros::assert_type_write_err;

// Corpus resolution lives in the crate's shared `idakit::corpus` (one source of truth with the
// doctest harness); re-export the test-facing surface so callers reach it through `common`.
pub use idakit::corpus::{Fixture, WorkingCopy, canonical, display_name, fixtures, working_copy};

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};

use idakit::prelude::{Database, Ida};

/// Runs `body` on the kernel thread against the worker's open database.
///
/// This is the body of essentially every `#[kernel_test]`, so the marshalling and the panic-resume
/// live here once: a caught assertion panic re-raises with its real message, keeping the failure's
/// own location rather than this line. The worker owns the open and the `save = false` close, and
/// resets between tests that declare they write, so nothing here touches the database's lifetime.
///
/// # Panics
/// If called outside a worker. The database comes from the harness, so there is nothing to run
/// against; a bare `#[test]` that wants a database of its own opens one itself, as the tests
/// directly under `tests/` do.
pub fn with_canonical_db(body: impl FnOnce(&mut Database) + Send + 'static) {
    registry::with_warm_kernel(|ida| {
        ida.call(move |idb| body(idb))
            .unwrap_or_else(|e| e.resume());
    })
    .expect("with_canonical_db needs the kernel harness; is this a #[kernel_test]?");
}

/// A private, disposable copy of the test database, removed on drop.
///
/// IDA takes an exclusive lock on a `.i64` while it is open, so every kernel test opens its
/// *own* copy via [`TestDb::acquire`] rather than the shared [`source`](TestDb::source) file;
/// otherwise tests flake against a live GUI session and against each other. Hold the guard
/// for as long as the database is open; dropping it deletes the copy. Pass it straight to
/// [`Database::open`](idakit::Database::open), since it is [`AsRef<str>`], or via [`path`](TestDb::path).
///
/// The conversion trait is `AsRef`, deliberately, not `From`/`Into`: a by-value `From<TestDb>`
/// would move the guard out and drop it, deleting the copy the caller is about to open.
pub struct TestDb {
    scratch: PathBuf,
    db: PathBuf,
}

impl TestDb {
    /// A private copy of the canonical [`source`](Self::source) database to open, or `None`
    /// to skip when no source exists.
    pub fn acquire() -> Option<Self> {
        Self::source().map(Self::copy_of)
    }

    /// The shared canonical database path: an explicit `IDAKIT_TEST_DB` override (an absolute
    /// `.i64`, rarely used), else the corpus manifest's [`canonical`](idakit::corpus::canonical) fixture,
    /// the one binary the dedicated tests open, identical on every platform so their
    /// assertions never depend on the host. `None` when no corpus is configured, which skips the
    /// dedicated tests. Read this directly only for lock-free byte access (advisory locks don't
    /// block plain reads), e.g. a truncated fixture; to *open* a database, take a copy with
    /// [`acquire`](Self::acquire).
    pub fn source() -> Option<PathBuf> {
        if let Ok(db) = std::env::var("IDAKIT_TEST_DB")
            && !db.is_empty()
        {
            return Some(PathBuf::from(db));
        }
        canonical()
    }

    /// A private copy of `src` in a scratch dir, removed on drop. Panics if the source is
    /// present but the copy fails: out of scratch space is a real error, not a skip.
    pub fn copy_of(src: impl AsRef<Path>) -> Self {
        let src = src.as_ref();
        let file_name = src.file_name().expect("source db has a file name");
        let unique = format!(
            "idakit-testdb-{}-{}",
            std::process::id(),
            NEXT.fetch_add(1, Ordering::Relaxed)
        );

        // Try each root in turn; on any failure (e.g. a RAM-backed dir out of space) drop the
        // partial copy and fall through to the next.
        let mut last_err = None;
        for root in scratch_roots() {
            let scratch = root.join(&unique);
            let db = scratch.join(file_name);
            if std::fs::create_dir_all(&scratch).is_err() {
                continue;
            }
            // Masters are write-protected and `fs::copy` preserves the mode, so the copy must be
            // made writable before idalib, which rewrites a database in place, can open it.
            match std::fs::copy(src, &db).and_then(|_| idakit::corpus::make_writable(&db)) {
                Ok(()) => return Self { scratch, db },
                Err(e) => {
                    let _ = std::fs::remove_dir_all(&scratch);
                    last_err = Some(e);
                }
            }
        }
        panic!("could not copy test db {src:?} into any scratch dir: {last_err:?}");
    }

    /// Path to the private copy, to hand to [`Database::open`](idakit::Database::open).
    #[must_use]
    pub fn path(&self) -> &str {
        self.db.to_str().expect("scratch db path is valid UTF-8")
    }
}

impl AsRef<str> for TestDb {
    fn as_ref(&self) -> &str {
        self.path()
    }
}

impl Drop for TestDb {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.scratch);
    }
}

/// Per-process suffix so several copies held at once never collide (`process::id` alone
/// isn't enough: one test may hold two).
static NEXT: AtomicU32 = AtomicU32::new(0);

/// Scratch roots in preference order. A RAM-backed dir avoids disk thrash when the platform
/// offers one at a well-known path (Linux `/dev/shm`); every other OS falls through to the
/// portable temp dir ([`std::env::temp_dir`] is `%TEMP%`/`$TMPDIR`), so nothing here is
/// Linux-only by dependency; `/dev/shm` is just an opportunistic fast path. Override both
/// with `IDAKIT_TEST_SCRATCH`.
fn scratch_roots() -> Vec<PathBuf> {
    if let Ok(dir) = std::env::var("IDAKIT_TEST_SCRATCH")
        && !dir.is_empty()
    {
        return vec![PathBuf::from(dir)];
    }
    let mut roots = Vec::new();
    let shm = PathBuf::from("/dev/shm");
    if shm.is_dir() {
        roots.push(shm);
    }
    roots.push(std::env::temp_dir());
    roots
}