#![allow(clippy::missing_safety_doc)]
use crate::block::BlockDevice;
use crate::error::Error;
use std::cell::RefCell;
use std::ffi::{c_char, CString};
use std::panic::AssertUnwindSafe;
use std::ptr;
use std::slice;
use std::sync::Arc;
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsCoreErrorCode {
Ok = 0,
Io = 1,
ShortRead = 2,
ReadOnly = 3,
OutOfBounds = 4,
Custom = 5,
NullArg = 6,
Panic = 7,
BadString = 8,
}
impl FsCoreErrorCode {
fn from_error(e: &Error) -> Self {
match e {
Error::Io(_) => FsCoreErrorCode::Io,
Error::ShortRead { .. } => FsCoreErrorCode::ShortRead,
Error::ReadOnly => FsCoreErrorCode::ReadOnly,
Error::OutOfBounds { .. } => FsCoreErrorCode::OutOfBounds,
Error::Custom(_) => FsCoreErrorCode::Custom,
}
}
}
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
pub fn set_last_error(message: impl Into<String>) {
let s = message.into();
let cs = CString::new(s.replace('\0', "?")).expect("contains no NUL after replace");
LAST_ERROR.with(|slot| {
*slot.borrow_mut() = Some(cs);
});
}
fn clear_last_error() {
LAST_ERROR.with(|slot| {
*slot.borrow_mut() = None;
});
}
#[unsafe(no_mangle)]
pub extern "C" fn fs_core_last_error_message() -> *const c_char {
LAST_ERROR.with(|slot| {
slot.borrow()
.as_ref()
.map(|cs| cs.as_ptr())
.unwrap_or(ptr::null())
})
}
pub fn ffi_guard<F>(body: F) -> FsCoreErrorCode
where
F: FnOnce() -> Result<(), Error>,
{
clear_last_error();
match std::panic::catch_unwind(AssertUnwindSafe(body)) {
Ok(Ok(())) => FsCoreErrorCode::Ok,
Ok(Err(e)) => {
let code = FsCoreErrorCode::from_error(&e);
set_last_error(e.to_string());
code
}
Err(panic) => {
set_last_error(panic_message(&panic));
FsCoreErrorCode::Panic
}
}
}
fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = panic.downcast_ref::<&'static str>() {
return (*s).to_string();
}
if let Some(s) = panic.downcast_ref::<String>() {
return s.clone();
}
"panic in FFI".to_string()
}
pub struct FsCoreDevice {
inner: Arc<dyn BlockDevice>,
}
impl FsCoreDevice {
pub fn into_handle(inner: Arc<dyn BlockDevice>) -> *mut FsCoreDevice {
Box::into_raw(Box::new(FsCoreDevice { inner }))
}
pub fn inner(&self) -> &Arc<dyn BlockDevice> {
&self.inner
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_close(handle: *mut FsCoreDevice) {
if handle.is_null() {
return;
}
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
drop(Box::from_raw(handle));
}));
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_size_bytes(handle: *const FsCoreDevice) -> u64 {
if handle.is_null() {
return 0;
}
std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { (*handle).inner.size_bytes() }))
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_is_writable(handle: *const FsCoreDevice) -> bool {
if handle.is_null() {
return false;
}
std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { (*handle).inner.is_writable() }))
.unwrap_or(false)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_read_at(
handle: *const FsCoreDevice,
offset: u64,
buf: *mut u8,
len: usize,
) -> FsCoreErrorCode {
if handle.is_null() || (buf.is_null() && len > 0) {
return FsCoreErrorCode::NullArg;
}
ffi_guard(|| {
let slice_buf = unsafe { slice::from_raw_parts_mut(buf, len) };
unsafe { (*handle).inner.read_at(offset, slice_buf) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_write_at(
handle: *const FsCoreDevice,
offset: u64,
buf: *const u8,
len: usize,
) -> FsCoreErrorCode {
if handle.is_null() || (buf.is_null() && len > 0) {
return FsCoreErrorCode::NullArg;
}
ffi_guard(|| {
let slice_buf = unsafe { slice::from_raw_parts(buf, len) };
unsafe { (*handle).inner.write_at(offset, slice_buf) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_flush(handle: *const FsCoreDevice) -> FsCoreErrorCode {
if handle.is_null() {
return FsCoreErrorCode::NullArg;
}
ffi_guard(|| unsafe { (*handle).inner.flush() })
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_file_open(
path: *const c_char,
writable: bool,
) -> *mut FsCoreDevice {
if path.is_null() {
set_last_error("path is null");
return ptr::null_mut();
}
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
let cstr = unsafe { std::ffi::CStr::from_ptr(path) };
let s = match cstr.to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("path is not valid UTF-8");
return ptr::null_mut();
}
};
let dev = if writable {
crate::file_device::FileDevice::open_rw(s)
} else {
crate::file_device::FileDevice::open(s)
};
match dev {
Ok(d) => FsCoreDevice::into_handle(Arc::new(d)),
Err(e) => {
set_last_error(e.to_string());
ptr::null_mut()
}
}
}));
match res {
Ok(p) => p,
Err(panic) => {
set_last_error(panic_message(&panic));
ptr::null_mut()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
fn tmp_image(bytes: &[u8]) -> String {
use std::sync::atomic::{AtomicU32, Ordering};
static C: AtomicU32 = AtomicU32::new(0);
let n = C.fetch_add(1, Ordering::Relaxed);
let p = format!("/tmp/fs_core_ffi_{}_{n}.img", std::process::id());
File::create(&p).unwrap().write_all(bytes).unwrap();
p
}
#[test]
fn open_read_close_round_trip() {
let path = tmp_image(b"hello, fs-core ffi");
let cpath = CString::new(path.as_str()).unwrap();
let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
assert!(!h.is_null(), "open failed");
unsafe {
assert_eq!(fs_core_device_size_bytes(h), 18);
assert!(!fs_core_device_is_writable(h));
let mut buf = [0u8; 5];
let rc = fs_core_device_read_at(h, 0, buf.as_mut_ptr(), buf.len());
assert_eq!(rc, FsCoreErrorCode::Ok);
assert_eq!(&buf, b"hello");
let rc = fs_core_device_write_at(h, 0, b"x".as_ptr(), 1);
assert_eq!(rc, FsCoreErrorCode::ReadOnly);
fs_core_device_close(h);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn null_args_return_null_arg() {
let mut buf = [0u8; 4];
let rc = unsafe { fs_core_device_read_at(ptr::null(), 0, buf.as_mut_ptr(), buf.len()) };
assert_eq!(rc, FsCoreErrorCode::NullArg);
let rc = unsafe { fs_core_device_flush(ptr::null()) };
assert_eq!(rc, FsCoreErrorCode::NullArg);
}
#[test]
fn last_error_populated_on_open_failure() {
let cpath = CString::new("/path/that/does/not/exist/we/hope").unwrap();
let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
assert!(h.is_null());
let msg = fs_core_last_error_message();
assert!(!msg.is_null());
let s = unsafe { std::ffi::CStr::from_ptr(msg).to_string_lossy().into_owned() };
assert!(!s.is_empty(), "expected an error message");
}
}