radare2 0.2.2

Rust integration helpers for radare2 core plugins
//! Core-plugin ABI aliases, session-state helpers, and export support.

use crate::core::Core;
use crate::sys;
use std::ffi::CStr;
use std::os::raw::c_int;

/// Core-plugin descriptor passed to radare2.
pub type CorePlugin = sys::RCorePlugin;
/// Metadata embedded in a core-plugin descriptor.
pub type PluginMeta = sys::RPluginMeta;
/// Session supplied to core-plugin callbacks.
pub type PluginSession = sys::RCorePluginSession;
/// Top-level descriptor exported by a plugin shared object.
pub type LibraryPlugin = sys::RLibStruct;

/// Successful/usable plugin status from `r_lib.h`.
pub const PLUGIN_STATUS_OK: c_int = 3;
/// Core plugin class from `r_lib.h`.
pub const LIB_TYPE_CORE: c_int = 12;

/// radare2 version against which this crate was built.
pub static R2_VERSION: &CStr =
    unsafe { CStr::from_bytes_with_nul_unchecked(concat!(env!("R2_VERSION"), "\0").as_bytes()) };

/// radare2 plugin ABI version against which this crate was built.
pub const R2_ABI_VERSION: u32 = parse_decimal(env!("R2_ABIVERSION"));

/// Construct plugin metadata from static C strings.
#[macro_export]
macro_rules! plugin_meta {
    ($name:literal, $description:literal, $author:literal, $version:literal, $license:literal) => {
        $crate::PluginMeta {
            name: concat!($name, "\0").as_ptr() as *mut ::std::os::raw::c_char,
            desc: concat!($description, "\0").as_ptr() as *mut ::std::os::raw::c_char,
            author: concat!($author, "\0").as_ptr() as *mut ::std::os::raw::c_char,
            version: concat!($version, "\0").as_ptr() as *mut ::std::os::raw::c_char,
            license: concat!($license, "\0").as_ptr() as *mut ::std::os::raw::c_char,
            contact: ::std::ptr::null_mut(),
            copyright: ::std::ptr::null_mut(),
            status: $crate::plugin::PLUGIN_STATUS_OK,
        }
    };
}

/// Export a static core-plugin descriptor as radare2's `radare_plugin` symbol.
#[macro_export]
macro_rules! export_core_plugin {
    ($plugin:expr, $package:literal) => {
        /// Plugin descriptor discovered by radare2's dynamic loader.
        #[unsafe(no_mangle)]
        pub static radare_plugin: $crate::plugin::LibraryPlugin = $crate::plugin::LibraryPlugin {
            type_: $crate::plugin::LIB_TYPE_CORE,
            data: $plugin as *const $crate::CorePlugin as *mut ::std::ffi::c_void,
            version: $crate::plugin::R2_VERSION.as_ptr(),
            free: None,
            pkgname: concat!($package, "\0").as_ptr() as *const ::std::os::raw::c_char,
            abiversion: $crate::plugin::R2_ABI_VERSION,
        };
    };
}

/// Access the core attached to a plugin session.
///
/// # Safety
///
/// `session` must point to a live session supplied by radare2.
pub unsafe fn session_core(session: *mut PluginSession) -> Option<Core> {
    unsafe { Core::from_session(session) }
}

/// Store newly allocated typed state in an empty plugin session.
///
/// Returns `Err(state)` if the session is null or already contains state.
///
/// # Safety
///
/// The session must be live and its `data` field must either be null or hold
/// state managed using the helpers in this module.
pub unsafe fn install_state<T>(session: *mut PluginSession, state: T) -> Result<(), T> {
    let Some(session) = (unsafe { session.as_mut() }) else {
        return Err(state);
    };
    if !session.data.is_null() {
        return Err(state);
    }
    session.data = Box::into_raw(Box::new(state)).cast();
    Ok(())
}

/// Borrow typed immutable state from a plugin session.
///
/// # Safety
///
/// `session.data` must be null or point to a live `T` installed by
/// [`install_state`], and no mutable reference to the same state may exist.
pub unsafe fn state<T>(session: *const PluginSession) -> Option<&'static T> {
    let session = unsafe { session.as_ref()? };
    unsafe { session.data.cast::<T>().as_ref() }
}

/// Borrow typed mutable state from a plugin session.
///
/// # Safety
///
/// `session.data` must be null or point to a live `T` installed by
/// [`install_state`], and this call must have exclusive access to it.
pub unsafe fn state_mut<T>(session: *mut PluginSession) -> Option<&'static mut T> {
    let session = unsafe { session.as_mut()? };
    unsafe { session.data.cast::<T>().as_mut() }
}

/// Remove and return typed state from a plugin session.
///
/// # Safety
///
/// A non-null `session.data` must point to a `T` installed by [`install_state`]
/// and must not have been removed previously.
pub unsafe fn take_state<T>(session: *mut PluginSession) -> Option<T> {
    let session = unsafe { session.as_mut()? };
    if session.data.is_null() {
        return None;
    }
    let state = unsafe { Box::from_raw(session.data.cast::<T>()) };
    session.data = std::ptr::null_mut();
    Some(*state)
}

const fn parse_decimal(value: &str) -> u32 {
    let bytes = value.as_bytes();
    let mut result = 0u32;
    let mut index = 0;
    while index < bytes.len() {
        let digit = bytes[index];
        assert!(digit >= b'0' && digit <= b'9', "invalid decimal value");
        result = result * 10 + (digit - b'0') as u32;
        index += 1;
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::ffi::c_void;

    fn session() -> PluginSession {
        PluginSession {
            core: std::ptr::null_mut(),
            plugin: std::ptr::null_mut::<c_void>(),
            data: std::ptr::null_mut(),
        }
    }

    #[test]
    fn owns_typed_session_state() {
        let mut session = session();
        unsafe {
            install_state(&mut session, String::from("state")).unwrap();
            state_mut::<String>(&mut session).unwrap().push('!');
            assert_eq!(
                state::<String>(&session).map(String::as_str),
                Some("state!")
            );
            assert_eq!(take_state::<String>(&mut session), Some("state!".into()));
            assert!(session.data.is_null());
        }
    }

    #[test]
    fn parses_abi_version() {
        assert_eq!(parse_decimal("0"), 0);
        assert_eq!(parse_decimal("42"), 42);
    }
}