Skip to main content

cradle_plugin_api/
convert.rs

1use crate::ffi::*;
2use crate::{AgentEvent, CheckResult, Severity};
3use std::ffi::{CStr, CString};
4use std::os::raw::c_char;
5
6/// FFI-safe(-ish) version of the [`AgentEvent`] enum
7pub struct FfiEventData {
8    event: FfiEvent,
9    _strings: Vec<CString>,
10}
11
12impl FfiEventData {
13    /// Returns the inner event as a pointer
14    pub fn as_ptr(&self) -> *const FfiEvent {
15        &self.event
16    }
17}
18
19impl AsRef<FfiEvent> for FfiEventData {
20    fn as_ref(&self) -> &FfiEvent {
21        &self.event
22    }
23}
24
25/// Converts an [`AgentEvent`] to an [`FfiEventData`] struct
26pub fn agent_event_to_ffi(event: &AgentEvent) -> FfiEventData {
27    let mut ffi = FfiEvent {
28        kind: 0,
29        pid: 0,
30        process: 0,
31        dll_name: std::ptr::null(),
32        dll_base: 0,
33        tid: 0,
34        thread_handle: 0,
35        dbg_string: std::ptr::null(),
36        target_path: std::ptr::null(),
37        target_bytes: std::ptr::null(),
38        target_bytes_len: 0,
39    };
40    let mut strings = Vec::new();
41
42    match event {
43        AgentEvent::ProcessCreated {
44            pid,
45            process,
46            target_bytes,
47            target_path,
48        } => {
49            ffi.kind = EVENT_PROCESS_CREATED;
50            ffi.pid = *pid;
51            ffi.process = *process as isize;
52            ffi.target_bytes = target_bytes.as_ptr();
53            ffi.target_bytes_len = target_bytes.len();
54            ffi.target_path = push_c_string(&mut strings, target_path) as *const u8;
55        }
56        AgentEvent::DllLoaded { name, base } => {
57            ffi.kind = EVENT_DLL_LOADED;
58            ffi.dll_base = *base;
59            ffi.dll_name = push_c_string(&mut strings, name);
60        }
61        AgentEvent::InitialBreakpoint => {
62            ffi.kind = EVENT_INITIAL_BREAKPOINT;
63        }
64        AgentEvent::ThreadCreated { tid, handle } => {
65            ffi.kind = EVENT_THREAD_CREATED;
66            ffi.tid = *tid;
67            ffi.thread_handle = *handle as isize;
68        }
69        AgentEvent::Finished => {
70            ffi.kind = EVENT_FINISHED;
71        }
72        AgentEvent::ThreadFinished { tid } => {
73            ffi.kind = EVENT_THREAD_FINISHED;
74            ffi.tid = *tid;
75        }
76        AgentEvent::DebugString { string } => {
77            ffi.kind = EVENT_DEBUG_STRING;
78            ffi.dbg_string = push_c_string(&mut strings, string) as *const u8;
79        }
80        AgentEvent::AgentInitialized { target_path } => {
81            ffi.kind = EVENT_AGENT_INITIALIZED;
82            ffi.target_path = push_c_string(&mut strings, target_path) as *const u8;
83        }
84        AgentEvent::AgentFinished => {
85            ffi.kind = EVENT_AGENT_FINISHED;
86            ffi.target_path = std::ptr::null();
87        }
88        _ => {}
89    }
90    FfiEventData {
91        event: ffi,
92        _strings: strings,
93    }
94}
95
96fn push_c_string(strings: &mut Vec<CString>, value: &str) -> *const c_char {
97    let c_string = into_c_string(value.to_string());
98    let ptr = c_string.as_ptr();
99    strings.push(c_string);
100    ptr
101}
102
103fn into_c_string(value: String) -> CString {
104    CString::new(value).unwrap_or_else(|err| {
105        let sanitized = err
106            .into_vec()
107            .into_iter()
108            .map(|byte| if byte == 0 { b' ' } else { byte })
109            .collect::<Vec<_>>();
110        CString::new(sanitized).expect("NUL bytes were replaced")
111    })
112}
113
114/// Converts a Rust string into an owned C string pointer
115///
116/// The returned pointer must be released with [`free_ffi_string`]
117pub fn string_to_ffi_ptr(value: impl Into<String>) -> *mut c_char {
118    into_c_string(value.into()).into_raw()
119}
120
121/// Frees a string allocated by [`string_to_ffi_ptr`]
122///
123/// # Safety
124///
125/// `ptr` must either be null or a pointer returned by [`string_to_ffi_ptr`] that has not already
126/// been freed
127pub unsafe fn free_ffi_string(ptr: *mut c_char) {
128    if !ptr.is_null() {
129        unsafe {
130            let _ = CString::from_raw(ptr);
131        }
132    }
133}
134
135/// Converts an [`FfiEvent`] to an [`AgentEvent`] enum value
136///
137/// # Safety
138///
139/// Any non-null pointer fields inside `event` must remain valid and point to properly
140/// null-terminated strings or byte buffers for the returned event's lifetime
141pub unsafe fn ffi_event_to_agent_event(event: &FfiEvent) -> AgentEvent<'_> {
142    match event.kind {
143        EVENT_PROCESS_CREATED => {
144            let path = unsafe { ptr_to_str(event.target_path) };
145            let bytes = if event.target_bytes.is_null() || event.target_bytes_len == 0 {
146                &[]
147            } else {
148                unsafe { std::slice::from_raw_parts(event.target_bytes, event.target_bytes_len) }
149            };
150            AgentEvent::ProcessCreated {
151                pid: event.pid,
152                process: event.process as _,
153                target_path: path,
154                target_bytes: bytes,
155            }
156        }
157        EVENT_DLL_LOADED => {
158            let name = unsafe { c_char_ptr_to_str(event.dll_name) };
159            AgentEvent::DllLoaded {
160                name,
161                base: event.dll_base,
162            }
163        }
164        EVENT_INITIAL_BREAKPOINT => AgentEvent::InitialBreakpoint,
165        EVENT_THREAD_CREATED => AgentEvent::ThreadCreated {
166            tid: event.tid,
167            handle: event.thread_handle as _,
168        },
169        EVENT_THREAD_FINISHED => AgentEvent::ThreadFinished { tid: event.tid },
170        EVENT_DEBUG_STRING => {
171            let str = unsafe { ptr_to_str(event.dbg_string) };
172            AgentEvent::DebugString { string: str }
173        }
174        EVENT_AGENT_INITIALIZED => {
175            let path = unsafe { ptr_to_str(event.target_path) };
176            AgentEvent::AgentInitialized { target_path: path }
177        }
178        EVENT_AGENT_FINISHED => AgentEvent::AgentFinished,
179        EVENT_FINISHED => AgentEvent::Finished,
180        _ => AgentEvent::Unknown,
181    }
182}
183
184unsafe fn ptr_to_str<'a>(ptr: *const u8) -> &'a str {
185    if ptr.is_null() {
186        ""
187    } else {
188        unsafe { CStr::from_ptr(ptr as *const c_char).to_str().unwrap_or("") }
189    }
190}
191
192unsafe fn c_char_ptr_to_str<'a>(ptr: *const c_char) -> &'a str {
193    if ptr.is_null() {
194        ""
195    } else {
196        unsafe { CStr::from_ptr(ptr).to_str().unwrap_or("") }
197    }
198}
199
200/// Converts a vector of [`CheckResult`] into the FFI-safe [`FfiResultArray`]
201pub fn check_results_to_ffi(results: Vec<CheckResult>) -> FfiResultArray {
202    if results.is_empty() {
203        return FfiResultArray {
204            ptr: std::ptr::null_mut(),
205            len: 0,
206        };
207    }
208    let mut ffi: Vec<FfiCheckResult> = results
209        .into_iter()
210        .map(|r| FfiCheckResult {
211            plugin: into_c_string(r.plugin).into_raw(),
212            check: into_c_string(r.check).into_raw(),
213            severity: match r.severity {
214                Severity::Pass => 0,
215                Severity::Info => 1,
216                Severity::Warn => 2,
217                Severity::Fail => 3,
218                Severity::Critical => 4,
219            },
220            message: into_c_string(r.message).into_raw(),
221            details: r
222                .details
223                .map(|d| into_c_string(d).into_raw())
224                .unwrap_or(std::ptr::null_mut()),
225        })
226        .collect();
227    let arr = FfiResultArray {
228        ptr: ffi.as_mut_ptr(),
229        len: ffi.len(),
230    };
231    std::mem::forget(ffi);
232    arr
233}
234
235/// Frees an [`FfiResultArray`] object
236///
237/// # Safety
238/// Uses unsafe functions such as [`Vec::from_raw_parts`], and [`CString::from_raw`], which are unsafe
239pub unsafe fn free_ffi_results(arr: FfiResultArray) {
240    unsafe {
241        if arr.ptr.is_null() {
242            return;
243        }
244        let results = Vec::from_raw_parts(arr.ptr, arr.len, arr.len);
245        for r in results {
246            free_ffi_string(r.plugin);
247            free_ffi_string(r.check);
248            free_ffi_string(r.message);
249            free_ffi_string(r.details);
250        }
251    }
252}
253
254/// Creates a Vector of [`CheckResult`] from the given [`FfiResultArray`]
255///
256/// # Safety
257/// Uses unsafe functions such as [`std::slice::from_raw_parts`] and [`CStr::from_ptr`].
258pub unsafe fn ffi_array_to_results(arr: &FfiResultArray) -> Vec<CheckResult> {
259    unsafe {
260        if arr.ptr.is_null() || arr.len == 0 {
261            return Vec::new();
262        }
263        std::slice::from_raw_parts(arr.ptr, arr.len)
264            .iter()
265            .map(|r| CheckResult {
266                plugin: string_from_ptr(r.plugin),
267                check: string_from_ptr(r.check),
268                severity: match r.severity {
269                    0 => Severity::Pass,
270                    1 => Severity::Info,
271                    2 => Severity::Warn,
272                    3 => Severity::Fail,
273                    _ => Severity::Critical,
274                },
275                message: string_from_ptr(r.message),
276                details: (!r.details.is_null()).then(|| string_from_ptr(r.details)),
277            })
278            .collect()
279    }
280}
281
282unsafe fn string_from_ptr(ptr: *const c_char) -> String {
283    if ptr.is_null() {
284        String::new()
285    } else {
286        unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
287    }
288}