use std::ffi::{CStr, CString, c_char};
use crate::error::{Error, Result};
const STACK_CAP: usize = 256;
pub(crate) unsafe fn cstr(p: *const c_char) -> String {
if p.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
pub(crate) fn read_string(f: impl Fn(*mut c_char, usize) -> i64) -> Option<String> {
let mut stack = [0u8; STACK_CAP];
let r = f(stack.as_mut_ptr().cast(), STACK_CAP);
if r < 0 {
return None;
}
let len = r as usize;
if len < STACK_CAP {
return Some(String::from_utf8_lossy(&stack[..len]).into_owned());
}
let mut heap = vec![0u8; len + 1];
let r2 = f(heap.as_mut_ptr().cast(), heap.len());
if r2 < 0 {
return None;
}
let len2 = (r2 as usize).min(len);
Some(String::from_utf8_lossy(&heap[..len2]).into_owned())
}
pub(crate) fn reason_or(reason: String, fallback: &str) -> String {
let trimmed = reason.trim();
if trimmed.is_empty() {
fallback.to_owned()
} else {
trimmed.to_owned()
}
}
pub(crate) unsafe fn slice<T>(ptr: &*const T, len: usize) -> &[T] {
if len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(*ptr, len) }
}
}
pub(crate) unsafe fn lossy(ptr: *const c_char, len: usize) -> Option<String> {
if ptr.is_null() || len == 0 {
return None;
}
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
Some(String::from_utf8_lossy(bytes).into_owned())
}
pub(crate) fn with_cstr<R>(
s: &str,
arg: &'static str,
f: impl FnOnce(*const c_char) -> R,
) -> Result<R> {
let c = CString::new(s).map_err(|_| Error::InteriorNul { arg })?;
Ok(f(c.as_ptr()))
}
pub(crate) fn nul_checked<'a>(s: &'a str, arg: &'static str) -> Result<&'a str> {
if s.bytes().any(|b| b == 0) {
Err(Error::InteriorNul { arg })
} else {
Ok(s)
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use rstest::rstest;
use super::*;
fn getter(src: &[u8]) -> impl Fn(*mut c_char, usize) -> i64 + '_ {
move |buf, cap| {
if cap > 0 {
let n = src.len().min(cap - 1);
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), buf.cast::<u8>(), n);
*buf.add(n) = 0;
}
}
src.len() as i64
}
}
#[rstest]
#[case::empty(b"")]
#[case::short(b"main")]
#[case::exact255(&[b'a'; 255])]
#[case::exact256(&[b'b'; 256])]
#[case::long(&[b'c'; 4096])]
fn round_trips_through_regrow(#[case] src: &[u8]) {
let got = read_string(getter(src)).expect("present");
assert!(got.as_bytes() == src);
}
#[test]
fn negative_length_is_absent() {
let r = read_string(|buf, cap| {
if cap > 0 {
unsafe { *buf = 0 };
}
-1
});
assert!(r.is_none());
}
#[test]
fn invalid_utf8_decodes_lossy() {
let got = read_string(getter(&[0xff, 0xfe, b'a'])).expect("present");
assert!(got.contains('\u{fffd}'));
}
#[test]
fn with_cstr_rejects_interior_nul() {
let r = with_cstr("ab\0cd", "name", |_| ());
assert!(r == Err(Error::InteriorNul { arg: "name" }));
}
#[test]
fn with_cstr_passes_valid_string() {
let len = with_cstr("hello", "name", |p| {
unsafe { std::ffi::CStr::from_ptr(p) }.to_bytes().len()
})
.expect("valid");
assert!(len == 5);
}
}