use std::ffi::CStr;
pub const DEFAULT_BUFFER_SIZE: usize = 1024;
pub const SMALL_BUFFER_SIZE: usize = 256;
const STACK_BUFFER_BYTES: usize = 256;
#[allow(clippy::cast_possible_wrap)]
pub unsafe fn ffi_string_from_buffer<F>(buffer_size: usize, ffi_call: F) -> Option<String>
where
F: FnOnce(*mut i8, isize) -> bool,
{
if buffer_size <= STACK_BUFFER_BYTES {
let mut buffer = [0i8; STACK_BUFFER_BYTES];
let success = ffi_call(buffer.as_mut_ptr(), buffer_size as isize);
if !success {
return None;
}
return parse_buffer(&buffer[..buffer_size]);
}
let mut buffer = vec![0i8; buffer_size];
let success = ffi_call(buffer.as_mut_ptr(), buffer.len() as isize);
if !success {
return None;
}
parse_buffer(&buffer)
}
fn parse_buffer(buffer: &[i8]) -> Option<String> {
let bytes = unsafe { std::slice::from_raw_parts(buffer.as_ptr().cast::<u8>(), buffer.len()) };
let nul_pos = bytes.iter().position(|&b| b == 0)?;
let s = String::from_utf8_lossy(&bytes[..nul_pos]).into_owned();
if s.is_empty() {
None
} else {
Some(s)
}
}
#[allow(clippy::cast_possible_wrap)]
pub unsafe fn ffi_string_from_buffer_or_empty<F>(buffer_size: usize, ffi_call: F) -> String
where
F: FnOnce(*mut i8, isize) -> bool,
{
ffi_string_from_buffer(buffer_size, ffi_call).unwrap_or_default()
}
pub unsafe fn ffi_string_owned<F, Free>(ffi_call: F, free_fn: Free) -> Option<String>
where
F: FnOnce() -> *mut i8,
Free: FnOnce(*mut i8),
{
struct FreeGuard<F: FnOnce(*mut i8)> {
ptr: *mut i8,
free_fn: Option<F>,
}
impl<F: FnOnce(*mut i8)> Drop for FreeGuard<F> {
fn drop(&mut self) {
if !self.ptr.is_null() {
if let Some(free) = self.free_fn.take() {
free(self.ptr);
}
}
}
}
let ptr = ffi_call();
if ptr.is_null() {
return None;
}
let _guard = FreeGuard {
ptr,
free_fn: Some(free_fn),
};
let bytes = CStr::from_ptr(ptr).to_bytes();
if bytes.is_empty() {
return None;
}
Some(String::from_utf8_lossy(bytes).into_owned())
}
pub unsafe fn ffi_string_owned_or_empty<F, Free>(ffi_call: F, free_fn: Free) -> String
where
F: FnOnce() -> *mut i8,
Free: FnOnce(*mut i8),
{
ffi_string_owned(ffi_call, free_fn).unwrap_or_default()
}
#[inline]
pub unsafe fn take_owned_cstring<Free>(ptr: *mut i8, free_fn: Free) -> Option<String>
where
Free: FnOnce(*mut i8),
{
if ptr.is_null() {
return None;
}
ffi_string_owned(|| ptr, free_fn)
}
#[inline]
pub unsafe fn take_owned_cstring_c<Free>(
ptr: *mut core::ffi::c_char,
free_fn: Free,
) -> Option<String>
where
Free: FnOnce(*mut core::ffi::c_char),
{
if ptr.is_null() {
return None;
}
let bytes = CStr::from_ptr(ptr).to_bytes().to_vec();
free_fn(ptr);
if bytes.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&bytes).into_owned())
}
}
#[cfg(test)]
mod take_owned_tests {
use super::*;
use std::ffi::CString;
#[test]
fn take_owned_cstring_null_returns_none() {
let freed = std::cell::Cell::new(false);
let result = unsafe { take_owned_cstring(core::ptr::null_mut(), |_| freed.set(true)) };
assert_eq!(result, None);
assert!(!freed.get(), "free fn must not be called for null ptr");
}
#[test]
fn take_owned_cstring_returns_value_and_frees() {
let s = CString::new("hello").unwrap();
let raw = s.into_raw();
let freed = std::cell::Cell::new(false);
let result = unsafe {
take_owned_cstring(raw.cast::<i8>(), |p| {
freed.set(true);
let _ = CString::from_raw(p.cast::<core::ffi::c_char>());
})
};
assert_eq!(result.as_deref(), Some("hello"));
assert!(freed.get(), "free fn must be called for non-null ptr");
}
#[test]
fn take_owned_cstring_c_returns_value_and_frees() {
let s = CString::new("world").unwrap();
let raw = s.into_raw();
let freed = std::cell::Cell::new(false);
let result = unsafe {
take_owned_cstring_c(raw, |p| {
freed.set(true);
let _ = CString::from_raw(p);
})
};
assert_eq!(result.as_deref(), Some("world"));
assert!(freed.get());
}
}
#[cfg(test)]
mod buffer_tests {
use super::{
ffi_string_from_buffer, ffi_string_from_buffer_or_empty, DEFAULT_BUFFER_SIZE,
SMALL_BUFFER_SIZE,
};
unsafe fn write_into(buffer: *mut i8, len: isize, bytes: &[u8]) -> bool {
if usize::try_from(len).map_or(true, |len| bytes.len() > len) {
return false;
}
unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer.cast::<u8>(), bytes.len()) };
true
}
#[test]
fn reads_a_string_through_the_stack_buffer() {
let result = unsafe {
ffi_string_from_buffer(SMALL_BUFFER_SIZE, |buffer, len| {
assert_eq!(usize::try_from(len), Ok(SMALL_BUFFER_SIZE));
write_into(buffer, len, b"device-1\0")
})
};
assert_eq!(result.as_deref(), Some("device-1"));
}
#[test]
fn reads_a_string_through_the_heap_buffer() {
let mut long = "x".repeat(600).into_bytes();
long.push(0);
let result = unsafe {
ffi_string_from_buffer(DEFAULT_BUFFER_SIZE, |buffer, len| {
assert_eq!(usize::try_from(len), Ok(DEFAULT_BUFFER_SIZE));
write_into(buffer, len, &long)
})
};
assert_eq!(result, Some("x".repeat(600)));
}
#[test]
fn failed_call_returns_none() {
assert_eq!(unsafe { ffi_string_from_buffer(64, |_, _| false) }, None);
assert_eq!(
unsafe { ffi_string_from_buffer_or_empty(64, |_, _| false) },
""
);
}
#[test]
fn empty_string_returns_none() {
let result =
unsafe { ffi_string_from_buffer(64, |buffer, len| write_into(buffer, len, b"\0")) };
assert_eq!(result, None);
let result = unsafe {
ffi_string_from_buffer_or_empty(64, |buffer, len| write_into(buffer, len, b"\0"))
};
assert_eq!(result, "");
}
#[test]
fn missing_terminator_returns_none() {
for size in [16, 512] {
let unterminated = vec![b'x'; size];
let result = unsafe {
ffi_string_from_buffer(size, |buffer, len| write_into(buffer, len, &unterminated))
};
assert_eq!(result, None, "buffer size {size}");
}
}
#[test]
fn invalid_utf8_is_replaced() {
let result = unsafe {
ffi_string_from_buffer(8, |buffer, len| {
write_into(buffer, len, &[b'f', 0xff, b'o', 0])
})
};
assert_eq!(result.as_deref(), Some("f\u{fffd}o"));
}
}