cradle-plugin-api 0.1.3

API for cradle plugins
Documentation
use crate::ffi::*;
use crate::{AgentEvent, CheckResult, Severity};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

/// FFI-safe(-ish) version of the [`AgentEvent`] enum
pub struct FfiEventData {
    event: FfiEvent,
    _strings: Vec<CString>,
}

impl FfiEventData {
    /// Returns the inner event as a pointer
    pub fn as_ptr(&self) -> *const FfiEvent {
        &self.event
    }
}

impl AsRef<FfiEvent> for FfiEventData {
    fn as_ref(&self) -> &FfiEvent {
        &self.event
    }
}

/// Converts an [`AgentEvent`] to an [`FfiEventData`] struct
pub fn agent_event_to_ffi(event: &AgentEvent) -> FfiEventData {
    let mut ffi = FfiEvent {
        kind: 0,
        pid: 0,
        process: 0,
        dll_name: std::ptr::null(),
        dll_base: 0,
        tid: 0,
        thread_handle: 0,
        dbg_string: std::ptr::null(),
        target_path: std::ptr::null(),
        target_bytes: std::ptr::null(),
        target_bytes_len: 0,
    };
    let mut strings = Vec::new();

    match event {
        AgentEvent::ProcessCreated {
            pid,
            process,
            target_bytes,
            target_path,
        } => {
            ffi.kind = EVENT_PROCESS_CREATED;
            ffi.pid = *pid;
            ffi.process = *process as isize;
            ffi.target_bytes = target_bytes.as_ptr();
            ffi.target_bytes_len = target_bytes.len();
            ffi.target_path = push_c_string(&mut strings, target_path) as *const u8;
        }
        AgentEvent::DllLoaded { name, base } => {
            ffi.kind = EVENT_DLL_LOADED;
            ffi.dll_base = *base;
            ffi.dll_name = push_c_string(&mut strings, name);
        }
        AgentEvent::InitialBreakpoint => {
            ffi.kind = EVENT_INITIAL_BREAKPOINT;
        }
        AgentEvent::ThreadCreated { tid, handle } => {
            ffi.kind = EVENT_THREAD_CREATED;
            ffi.tid = *tid;
            ffi.thread_handle = *handle as isize;
        }
        AgentEvent::Finished => {
            ffi.kind = EVENT_FINISHED;
        }
        AgentEvent::ThreadFinished { tid } => {
            ffi.kind = EVENT_THREAD_FINISHED;
            ffi.tid = *tid;
        }
        AgentEvent::DebugString { string } => {
            ffi.kind = EVENT_DEBUG_STRING;
            ffi.dbg_string = push_c_string(&mut strings, string) as *const u8;
        }
        AgentEvent::AgentInitialized { target_path } => {
            ffi.kind = EVENT_AGENT_INITIALIZED;
            ffi.target_path = push_c_string(&mut strings, target_path) as *const u8;
        }
        AgentEvent::AgentFinished => {
            ffi.kind = EVENT_AGENT_FINISHED;
            ffi.target_path = std::ptr::null();
        }
        _ => {}
    }
    FfiEventData {
        event: ffi,
        _strings: strings,
    }
}

fn push_c_string(strings: &mut Vec<CString>, value: &str) -> *const c_char {
    let c_string = into_c_string(value.to_string());
    let ptr = c_string.as_ptr();
    strings.push(c_string);
    ptr
}

fn into_c_string(value: String) -> CString {
    CString::new(value).unwrap_or_else(|err| {
        let sanitized = err
            .into_vec()
            .into_iter()
            .map(|byte| if byte == 0 { b' ' } else { byte })
            .collect::<Vec<_>>();
        CString::new(sanitized).expect("NUL bytes were replaced")
    })
}

/// Converts a Rust string into an owned C string pointer
///
/// The returned pointer must be released with [`free_ffi_string`]
pub fn string_to_ffi_ptr(value: impl Into<String>) -> *mut c_char {
    into_c_string(value.into()).into_raw()
}

/// Frees a string allocated by [`string_to_ffi_ptr`]
///
/// # Safety
///
/// `ptr` must either be null or a pointer returned by [`string_to_ffi_ptr`] that has not already
/// been freed
pub unsafe fn free_ffi_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe {
            let _ = CString::from_raw(ptr);
        }
    }
}

/// Converts an [`FfiEvent`] to an [`AgentEvent`] enum value
///
/// # Safety
///
/// Any non-null pointer fields inside `event` must remain valid and point to properly
/// null-terminated strings or byte buffers for the returned event's lifetime
pub unsafe fn ffi_event_to_agent_event(event: &FfiEvent) -> AgentEvent<'_> {
    match event.kind {
        EVENT_PROCESS_CREATED => {
            let path = unsafe { ptr_to_str(event.target_path) };
            let bytes = if event.target_bytes.is_null() || event.target_bytes_len == 0 {
                &[]
            } else {
                unsafe { std::slice::from_raw_parts(event.target_bytes, event.target_bytes_len) }
            };
            AgentEvent::ProcessCreated {
                pid: event.pid,
                process: event.process as _,
                target_path: path,
                target_bytes: bytes,
            }
        }
        EVENT_DLL_LOADED => {
            let name = unsafe { c_char_ptr_to_str(event.dll_name) };
            AgentEvent::DllLoaded {
                name,
                base: event.dll_base,
            }
        }
        EVENT_INITIAL_BREAKPOINT => AgentEvent::InitialBreakpoint,
        EVENT_THREAD_CREATED => AgentEvent::ThreadCreated {
            tid: event.tid,
            handle: event.thread_handle as _,
        },
        EVENT_THREAD_FINISHED => AgentEvent::ThreadFinished { tid: event.tid },
        EVENT_DEBUG_STRING => {
            let str = unsafe { ptr_to_str(event.dbg_string) };
            AgentEvent::DebugString { string: str }
        }
        EVENT_AGENT_INITIALIZED => {
            let path = unsafe { ptr_to_str(event.target_path) };
            AgentEvent::AgentInitialized { target_path: path }
        }
        EVENT_AGENT_FINISHED => AgentEvent::AgentFinished,
        EVENT_FINISHED => AgentEvent::Finished,
        _ => AgentEvent::Unknown,
    }
}

unsafe fn ptr_to_str<'a>(ptr: *const u8) -> &'a str {
    if ptr.is_null() {
        ""
    } else {
        unsafe { CStr::from_ptr(ptr as *const c_char).to_str().unwrap_or("") }
    }
}

unsafe fn c_char_ptr_to_str<'a>(ptr: *const c_char) -> &'a str {
    if ptr.is_null() {
        ""
    } else {
        unsafe { CStr::from_ptr(ptr).to_str().unwrap_or("") }
    }
}

/// Converts a vector of [`CheckResult`] into the FFI-safe [`FfiResultArray`]
pub fn check_results_to_ffi(results: Vec<CheckResult>) -> FfiResultArray {
    if results.is_empty() {
        return FfiResultArray {
            ptr: std::ptr::null_mut(),
            len: 0,
        };
    }
    let mut ffi: Vec<FfiCheckResult> = results
        .into_iter()
        .map(|r| FfiCheckResult {
            plugin: into_c_string(r.plugin).into_raw(),
            check: into_c_string(r.check).into_raw(),
            severity: match r.severity {
                Severity::Pass => 0,
                Severity::Info => 1,
                Severity::Warn => 2,
                Severity::Fail => 3,
                Severity::Critical => 4,
            },
            message: into_c_string(r.message).into_raw(),
            details: r
                .details
                .map(|d| into_c_string(d).into_raw())
                .unwrap_or(std::ptr::null_mut()),
        })
        .collect();
    let arr = FfiResultArray {
        ptr: ffi.as_mut_ptr(),
        len: ffi.len(),
    };
    std::mem::forget(ffi);
    arr
}

/// Frees an [`FfiResultArray`] object
///
/// # Safety
/// Uses unsafe functions such as [`Vec::from_raw_parts`], and [`CString::from_raw`], which are unsafe
pub unsafe fn free_ffi_results(arr: FfiResultArray) {
    unsafe {
        if arr.ptr.is_null() {
            return;
        }
        let results = Vec::from_raw_parts(arr.ptr, arr.len, arr.len);
        for r in results {
            free_ffi_string(r.plugin);
            free_ffi_string(r.check);
            free_ffi_string(r.message);
            free_ffi_string(r.details);
        }
    }
}

/// Converts Rust strings into an FFI-safe string array.
pub fn strings_to_ffi_array(strings: Vec<String>) -> FfiStringArray {
    if strings.is_empty() {
        return FfiStringArray {
            ptr: std::ptr::null_mut(),
            len: 0,
        };
    }

    let mut ffi = strings
        .into_iter()
        .map(|s| into_c_string(s).into_raw())
        .collect::<Vec<_>>();
    let arr = FfiStringArray {
        ptr: ffi.as_mut_ptr(),
        len: ffi.len(),
    };
    std::mem::forget(ffi);
    arr
}

/// Frees an [`FfiStringArray`] allocated by [`strings_to_ffi_array`].
///
/// # Safety
///
/// `arr` must either be empty/null or must have been returned by [`strings_to_ffi_array`].
pub unsafe fn free_ffi_string_array(arr: FfiStringArray) {
    unsafe {
        if arr.ptr.is_null() {
            return;
        }

        let strings = Vec::from_raw_parts(arr.ptr, arr.len, arr.len);
        for ptr in strings {
            free_ffi_string(ptr);
        }
    }
}

/// Converts an FFI string array into owned Rust strings.
///
/// # Safety
///
/// `arr.ptr` must point to `arr.len` valid string pointers.
pub unsafe fn ffi_string_array_to_vec(arr: &FfiStringArray) -> Vec<String> {
    unsafe {
        if arr.ptr.is_null() || arr.len == 0 {
            return Vec::new();
        }

        std::slice::from_raw_parts(arr.ptr, arr.len)
            .iter()
            .map(|ptr| string_from_ptr(*ptr))
            .collect()
    }
}

/// Creates a Vector of [`CheckResult`] from the given [`FfiResultArray`]
///
/// # Safety
/// Uses unsafe functions such as [`std::slice::from_raw_parts`] and [`CStr::from_ptr`].
pub unsafe fn ffi_array_to_results(arr: &FfiResultArray) -> Vec<CheckResult> {
    unsafe {
        if arr.ptr.is_null() || arr.len == 0 {
            return Vec::new();
        }
        std::slice::from_raw_parts(arr.ptr, arr.len)
            .iter()
            .map(|r| CheckResult {
                plugin: string_from_ptr(r.plugin),
                check: string_from_ptr(r.check),
                severity: match r.severity {
                    0 => Severity::Pass,
                    1 => Severity::Info,
                    2 => Severity::Warn,
                    3 => Severity::Fail,
                    _ => Severity::Critical,
                },
                message: string_from_ptr(r.message),
                details: (!r.details.is_null()).then(|| string_from_ptr(r.details)),
            })
            .collect()
    }
}

unsafe fn string_from_ptr(ptr: *const c_char) -> String {
    if ptr.is_null() {
        String::new()
    } else {
        unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
    }
}