ipfrs_interface/
ffi.rs

1//! FFI (Foreign Function Interface) bindings for C interoperability
2//!
3//! This module provides a C-compatible API for IPFRS, allowing the library
4//! to be used from C, C++, and other languages that support C FFI.
5//!
6//! # Safety
7//!
8//! All functions are marked as `unsafe extern "C"` and handle panics to prevent
9//! undefined behavior. Proper null checks are performed on all pointer arguments.
10//!
11//! # Memory Management
12//!
13//! - Opaque pointers are used to hide Rust types from C
14//! - Callers must free resources using the provided `*_free` functions
15//! - Strings passed from C must be valid UTF-8 null-terminated strings
16//! - Strings returned to C must be freed using `ipfrs_string_free`
17
18use std::ffi::{CStr, CString};
19use std::os::raw::{c_char, c_int};
20use std::panic::{catch_unwind, AssertUnwindSafe};
21use std::ptr;
22use std::slice;
23
24/// FFI error codes
25#[repr(C)]
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum IpfrsErrorCode {
28    /// Operation succeeded
29    Success = 0,
30    /// Null pointer was passed
31    NullPointer = -1,
32    /// Invalid UTF-8 string
33    InvalidUtf8 = -2,
34    /// Invalid CID format
35    InvalidCid = -3,
36    /// Block not found
37    NotFound = -4,
38    /// I/O error
39    IoError = -5,
40    /// Out of memory
41    OutOfMemory = -6,
42    /// Internal error (panic caught)
43    InternalError = -7,
44    /// Invalid argument
45    InvalidArgument = -8,
46    /// Operation timed out
47    Timeout = -9,
48    /// Unknown error
49    Unknown = -99,
50}
51
52/// Opaque handle to IPFRS client
53#[repr(C)]
54pub struct IpfrsClient {
55    _private: [u8; 0],
56}
57
58/// Opaque handle to a block
59#[repr(C)]
60pub struct IpfrsBlock {
61    _private: [u8; 0],
62}
63
64/// Internal representation of IPFRS client
65struct ClientInner {
66    // In a real implementation, this would contain:
67    // - Gateway configuration
68    // - Blockstore handle
69    // - Tokio runtime handle
70    // For now, we'll keep it simple
71    _placeholder: u8,
72}
73
74/// Internal representation of a block
75#[allow(dead_code)]
76struct BlockInner {
77    cid: String,
78    data: Vec<u8>,
79}
80
81// Thread-local for storing last error message
82thread_local! {
83    static LAST_ERROR: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
84}
85
86/// Set the last error message
87fn set_last_error(msg: String) {
88    LAST_ERROR.with(|e| {
89        *e.borrow_mut() = Some(msg);
90    });
91}
92
93/// Clear the last error message
94fn clear_last_error() {
95    LAST_ERROR.with(|e| {
96        *e.borrow_mut() = None;
97    });
98}
99
100/// Initialize a new IPFRS client
101///
102/// # Arguments
103///
104/// * `config_path` - Path to configuration file (optional, can be NULL)
105///
106/// # Returns
107///
108/// Pointer to IpfrsClient on success, NULL on failure.
109/// Use `ipfrs_get_last_error()` to retrieve error message.
110///
111/// # Safety
112///
113/// - `config_path` must be NULL or a valid null-terminated UTF-8 string
114/// - Returned pointer must be freed with `ipfrs_client_free()`
115#[no_mangle]
116pub unsafe extern "C" fn ipfrs_client_new(config_path: *const c_char) -> *mut IpfrsClient {
117    clear_last_error();
118
119    let result = catch_unwind(AssertUnwindSafe(|| {
120        // Parse config path if provided
121        let _config = if !config_path.is_null() {
122            let c_str = unsafe { CStr::from_ptr(config_path) };
123            match c_str.to_str() {
124                Ok(s) => Some(s.to_string()),
125                Err(_) => {
126                    set_last_error("Invalid UTF-8 in config_path".to_string());
127                    return ptr::null_mut();
128                }
129            }
130        } else {
131            None
132        };
133
134        // Create client inner
135        let inner = Box::new(ClientInner { _placeholder: 0 });
136
137        Box::into_raw(inner) as *mut IpfrsClient
138    }));
139
140    match result {
141        Ok(ptr) => ptr,
142        Err(_) => {
143            set_last_error("Panic occurred in ipfrs_client_new".to_string());
144            ptr::null_mut()
145        }
146    }
147}
148
149/// Free an IPFRS client
150///
151/// # Safety
152///
153/// - `client` must be a valid pointer returned from `ipfrs_client_new()`
154/// - `client` must not be used after this call
155/// - `client` must not be NULL
156#[no_mangle]
157pub unsafe extern "C" fn ipfrs_client_free(client: *mut IpfrsClient) {
158    if client.is_null() {
159        return;
160    }
161
162    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
163        let _ = Box::from_raw(client as *mut ClientInner);
164    }));
165}
166
167/// Add data to IPFRS and return its CID
168///
169/// # Arguments
170///
171/// * `client` - Pointer to IpfrsClient
172/// * `data` - Pointer to data buffer
173/// * `data_len` - Length of data in bytes
174/// * `out_cid` - Output pointer to receive CID string (must be freed with ipfrs_string_free)
175///
176/// # Returns
177///
178/// Error code (0 for success)
179///
180/// # Safety
181///
182/// - `client` must be a valid pointer from `ipfrs_client_new()`
183/// - `data` must point to at least `data_len` bytes
184/// - `out_cid` must be a valid pointer to a char pointer
185#[no_mangle]
186pub unsafe extern "C" fn ipfrs_add(
187    client: *mut IpfrsClient,
188    data: *const u8,
189    data_len: usize,
190    out_cid: *mut *mut c_char,
191) -> c_int {
192    clear_last_error();
193
194    // Null pointer checks
195    if client.is_null() {
196        set_last_error("client is NULL".to_string());
197        return IpfrsErrorCode::NullPointer as c_int;
198    }
199    if data.is_null() {
200        set_last_error("data is NULL".to_string());
201        return IpfrsErrorCode::NullPointer as c_int;
202    }
203    if out_cid.is_null() {
204        set_last_error("out_cid is NULL".to_string());
205        return IpfrsErrorCode::NullPointer as c_int;
206    }
207
208    let result = catch_unwind(AssertUnwindSafe(|| {
209        let _inner = &*(client as *mut ClientInner);
210        let data_slice = unsafe { slice::from_raw_parts(data, data_len) };
211
212        // In a real implementation, this would:
213        // 1. Chunk the data
214        // 2. Create blocks
215        // 3. Store them in the blockstore
216        // 4. Return the root CID
217
218        // For now, create a mock CID based on data length
219        let mock_cid = format!("bafkreidummy{:016x}", data_slice.len());
220
221        // Convert to C string
222        match CString::new(mock_cid) {
223            Ok(c_string) => {
224                unsafe {
225                    *out_cid = c_string.into_raw();
226                }
227                IpfrsErrorCode::Success as c_int
228            }
229            Err(_) => {
230                set_last_error("Failed to create CID string".to_string());
231                IpfrsErrorCode::InternalError as c_int
232            }
233        }
234    }));
235
236    match result {
237        Ok(code) => code,
238        Err(_) => {
239            set_last_error("Panic occurred in ipfrs_add".to_string());
240            IpfrsErrorCode::InternalError as c_int
241        }
242    }
243}
244
245/// Get data from IPFRS by CID
246///
247/// # Arguments
248///
249/// * `client` - Pointer to IpfrsClient
250/// * `cid` - Null-terminated CID string
251/// * `out_data` - Output pointer to receive data buffer (must be freed with ipfrs_data_free)
252/// * `out_len` - Output pointer to receive data length
253///
254/// # Returns
255///
256/// Error code (0 for success)
257///
258/// # Safety
259///
260/// - `client` must be a valid pointer from `ipfrs_client_new()`
261/// - `cid` must be a valid null-terminated UTF-8 string
262/// - `out_data` must be a valid pointer
263/// - `out_len` must be a valid pointer
264#[no_mangle]
265pub unsafe extern "C" fn ipfrs_get(
266    client: *mut IpfrsClient,
267    cid: *const c_char,
268    out_data: *mut *mut u8,
269    out_len: *mut usize,
270) -> c_int {
271    clear_last_error();
272
273    // Null pointer checks
274    if client.is_null() {
275        set_last_error("client is NULL".to_string());
276        return IpfrsErrorCode::NullPointer as c_int;
277    }
278    if cid.is_null() {
279        set_last_error("cid is NULL".to_string());
280        return IpfrsErrorCode::NullPointer as c_int;
281    }
282    if out_data.is_null() {
283        set_last_error("out_data is NULL".to_string());
284        return IpfrsErrorCode::NullPointer as c_int;
285    }
286    if out_len.is_null() {
287        set_last_error("out_len is NULL".to_string());
288        return IpfrsErrorCode::NullPointer as c_int;
289    }
290
291    let result = catch_unwind(AssertUnwindSafe(|| {
292        let _inner = &*(client as *mut ClientInner);
293
294        // Parse CID
295        let c_str = unsafe { CStr::from_ptr(cid) };
296        let cid_str = match c_str.to_str() {
297            Ok(s) => s,
298            Err(_) => {
299                set_last_error("Invalid UTF-8 in CID".to_string());
300                return IpfrsErrorCode::InvalidUtf8 as c_int;
301            }
302        };
303
304        // In a real implementation, this would:
305        // 1. Look up the CID in the blockstore
306        // 2. Retrieve and reconstruct the data
307        // 3. Return it to the caller
308
309        // For now, return mock data
310        let mock_data = format!("Data for CID: {}", cid_str).into_bytes();
311        let len = mock_data.len();
312
313        // Allocate buffer and copy data
314        let mut boxed_data = mock_data.into_boxed_slice();
315        let data_ptr = boxed_data.as_mut_ptr();
316        std::mem::forget(boxed_data); // Prevent deallocation
317
318        unsafe {
319            *out_data = data_ptr;
320            *out_len = len;
321        }
322
323        IpfrsErrorCode::Success as c_int
324    }));
325
326    match result {
327        Ok(code) => code,
328        Err(_) => {
329            set_last_error("Panic occurred in ipfrs_get".to_string());
330            IpfrsErrorCode::InternalError as c_int
331        }
332    }
333}
334
335/// Check if a block exists by CID
336///
337/// # Arguments
338///
339/// * `client` - Pointer to IpfrsClient
340/// * `cid` - Null-terminated CID string
341/// * `out_exists` - Output pointer to receive existence flag (1 = exists, 0 = not found)
342///
343/// # Returns
344///
345/// Error code (0 for success)
346///
347/// # Safety
348///
349/// - `client` must be a valid pointer from `ipfrs_client_new()`
350/// - `cid` must be a valid null-terminated UTF-8 string
351/// - `out_exists` must be a valid pointer
352#[no_mangle]
353pub unsafe extern "C" fn ipfrs_has(
354    client: *mut IpfrsClient,
355    cid: *const c_char,
356    out_exists: *mut c_int,
357) -> c_int {
358    clear_last_error();
359
360    // Null pointer checks
361    if client.is_null() {
362        set_last_error("client is NULL".to_string());
363        return IpfrsErrorCode::NullPointer as c_int;
364    }
365    if cid.is_null() {
366        set_last_error("cid is NULL".to_string());
367        return IpfrsErrorCode::NullPointer as c_int;
368    }
369    if out_exists.is_null() {
370        set_last_error("out_exists is NULL".to_string());
371        return IpfrsErrorCode::NullPointer as c_int;
372    }
373
374    let result = catch_unwind(AssertUnwindSafe(|| {
375        let _inner = &*(client as *mut ClientInner);
376
377        // Parse CID
378        let c_str = unsafe { CStr::from_ptr(cid) };
379        let _cid_str = match c_str.to_str() {
380            Ok(s) => s,
381            Err(_) => {
382                set_last_error("Invalid UTF-8 in CID".to_string());
383                return IpfrsErrorCode::InvalidUtf8 as c_int;
384            }
385        };
386
387        // In a real implementation, check blockstore
388        // For now, always return true
389        unsafe {
390            *out_exists = 1;
391        }
392
393        IpfrsErrorCode::Success as c_int
394    }));
395
396    match result {
397        Ok(code) => code,
398        Err(_) => {
399            set_last_error("Panic occurred in ipfrs_has".to_string());
400            IpfrsErrorCode::InternalError as c_int
401        }
402    }
403}
404
405/// Get the last error message
406///
407/// # Returns
408///
409/// Pointer to null-terminated error string, or NULL if no error.
410/// The string is valid until the next FFI call on this thread.
411/// DO NOT free this pointer.
412#[no_mangle]
413pub extern "C" fn ipfrs_get_last_error() -> *const c_char {
414    LAST_ERROR.with(|e| {
415        e.borrow()
416            .as_ref()
417            .map_or(ptr::null(), |s| s.as_ptr() as *const c_char)
418    })
419}
420
421/// Free a string returned by IPFRS functions
422///
423/// # Safety
424///
425/// - `s` must be a pointer returned by an IPFRS function (e.g., from ipfrs_add)
426/// - `s` must not be used after this call
427/// - `s` can be NULL (no-op)
428#[no_mangle]
429pub unsafe extern "C" fn ipfrs_string_free(s: *mut c_char) {
430    if s.is_null() {
431        return;
432    }
433
434    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
435        let _ = CString::from_raw(s);
436    }));
437}
438
439/// Free data returned by ipfrs_get
440///
441/// # Safety
442///
443/// - `data` must be a pointer returned by `ipfrs_get()`
444/// - `len` must be the length returned by `ipfrs_get()`
445/// - `data` must not be used after this call
446/// - `data` can be NULL (no-op)
447#[no_mangle]
448pub unsafe extern "C" fn ipfrs_data_free(data: *mut u8, len: usize) {
449    if data.is_null() {
450        return;
451    }
452
453    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
454        let _ = Vec::from_raw_parts(data, len, len);
455    }));
456}
457
458/// Get library version string
459///
460/// # Returns
461///
462/// Pointer to static version string. DO NOT free this pointer.
463#[no_mangle]
464pub extern "C" fn ipfrs_version() -> *const c_char {
465    // Use a static string to avoid allocation
466    static VERSION: &[u8] = b"ipfrs-interface 0.1.0\0";
467    VERSION.as_ptr() as *const c_char
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_client_lifecycle() {
476        unsafe {
477            let client = ipfrs_client_new(ptr::null());
478            assert!(!client.is_null());
479            ipfrs_client_free(client);
480        }
481    }
482
483    #[test]
484    fn test_add_and_get() {
485        unsafe {
486            let client = ipfrs_client_new(ptr::null());
487            assert!(!client.is_null());
488
489            // Add data
490            let data = b"Hello, IPFRS!";
491            let mut cid_ptr: *mut c_char = ptr::null_mut();
492            let result = ipfrs_add(client, data.as_ptr(), data.len(), &mut cid_ptr);
493            assert_eq!(result, IpfrsErrorCode::Success as c_int);
494            assert!(!cid_ptr.is_null());
495
496            // Get data back
497            let mut out_data: *mut u8 = ptr::null_mut();
498            let mut out_len: usize = 0;
499            let result = ipfrs_get(client, cid_ptr, &mut out_data, &mut out_len);
500            assert_eq!(result, IpfrsErrorCode::Success as c_int);
501            assert!(!out_data.is_null());
502            assert!(out_len > 0);
503
504            // Clean up
505            ipfrs_string_free(cid_ptr);
506            ipfrs_data_free(out_data, out_len);
507            ipfrs_client_free(client);
508        }
509    }
510
511    #[test]
512    fn test_has_block() {
513        unsafe {
514            let client = ipfrs_client_new(ptr::null());
515            assert!(!client.is_null());
516
517            let cid = CString::new("bafytest123").unwrap();
518            let mut exists: c_int = 0;
519            let result = ipfrs_has(client, cid.as_ptr(), &mut exists);
520            assert_eq!(result, IpfrsErrorCode::Success as c_int);
521
522            ipfrs_client_free(client);
523        }
524    }
525
526    #[test]
527    fn test_null_pointer_handling() {
528        unsafe {
529            // Test with null client
530            let mut cid_ptr: *mut c_char = ptr::null_mut();
531            let data = b"test";
532            let result = ipfrs_add(ptr::null_mut(), data.as_ptr(), data.len(), &mut cid_ptr);
533            assert_eq!(result, IpfrsErrorCode::NullPointer as c_int);
534        }
535    }
536
537    #[test]
538    fn test_version() {
539        let version = ipfrs_version();
540        assert!(!version.is_null());
541        unsafe {
542            let c_str = CStr::from_ptr(version);
543            let version_str = c_str.to_str().unwrap();
544            assert!(version_str.contains("ipfrs-interface"));
545        }
546    }
547}