diff --git a/omrf/src/cstr_ffi.rs b/omrf/src/cstr_ffi.rs index 3bcfadc..1b6e0e8 100644 --- a/omrf/src/cstr_ffi.rs +++ b/omrf/src/cstr_ffi.rs @@ -1,19 +1,59 @@ -//! When calling this dynamic library with outside programs, -//! outer programs may usually need to fetch string resource produced by Rust code. -//! However it is impossible pass Rust string directly to outer program. +//! When calling Rust exported dynamic library with outside programs, +//! outer programs may usually need to fetch string resource produced by Rust code, +//! or push unchecked string resource to Rust code. +//! However it is impossible to use Rust string directly to outer program. //! -//! This module provide **thread independent** string cache for resolving this issue. -//! When we need pass string to outer programs, we push that string into this string as C-like format, -//! then return its pointer to outer program. -//! So that outside program can utilize it like calling C/C++ library. -//! The only thing that outer programs should note is that this string is volatile, -//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library. +//! This module provides **thread independent** facilities for two directions: +//! +//! - Output (Rust -> C/C++): push strings / string vectors / string views into a thread-local +//! cache (or re-expose caller-owned memory for views) and hand stable pointers out. +//! - Input (C/C++ -> Rust): parse foreign-provided strings into borrowed `&str` without copying. +//! +//! # Output slots +//! +//! There are three independent kinds of output slots. A single FFI call may produce any number of +//! each, in any combination: +//! +//! - **Single NUL-terminated string** via [`push_ffi_string`]. The string is *copied* into the +//! cache; a stable [`CStrPtr`] is returned. +//! - **String vector** (a list whose entries are NUL-terminated strings) via +//! [`push_ffi_string_vec`] (nullptr-terminated list) or [`push_ffi_string_vec_with_len`] +//! (count-terminated list). Entries are *copied* into the cache. +//! - **String view** via [`push_ffi_string_view`] (single) or +//! [`push_ffi_string_view_vec_with_len`] (vector of views). These are length-delimited and do +//! **not** copy the string data; see "String view ownership" below. +//! +//! All cached slots stay valid until the next [`clear_ffi_strings`] call. Outer programs must +//! duplicate any retrieved pointer immediately before issuing the next call into the library. +//! +//! # String view ownership +//! +//! Please read this chapter CAREFULLY! +//! +//! Functions in the `*_string_view*` family are designed exclusively for transmitting very long +//! strings. To avoid an expensive copy they do **not** take ownership of and do **not** copy the +//! string data. They only re-expose a pointer plus length into memory that the *caller* owns. +//! +//! This deliberately steps outside ordinary Rust lifetime guarantees and is extremely easy to +//! misuse. The caller MUST guarantee that the underlying storage outlives every read performed by +//! the foreign (C/C++) side. If the source string is dropped or moved before the foreign side +//! finishes reading, the foreign side receives a dangling pointer and the behavior is undefined. +//! +//! It is highly suggested that use `SAFETY` annotation for this use case. use std::cell::RefCell; -use std::ffi::{c_char, CStr, CString}; +use std::ffi::{CStr, CString, c_char}; +use std::slice; use thiserror::Error as TeError; -/// The type representing the raw pointer to immutable C-style NUL-terminated string. -pub type CStyleString = *const c_char; +/// The type representing a raw pointer to an immutable C/C++ character string. +/// +/// This is a plain string-pointer alias used uniformly across the module. Whether the pointee is +/// NUL-terminated or length-delimited depends on the specific function it is passed to or returned +/// from; consult each function's documentation for the expected convention. +pub type CStrPtr = *const c_char; + +/// The type representing a pointer to a vector (array) of [`CStrPtr`] entries. +pub type CStrVecPtr = *const CStrPtr; // region: Error @@ -34,64 +74,299 @@ type Result = std::result::Result; // endregion +// region: String View Type + +/// A `#[repr(C)]` mirror of C++ `std::string_view`, exposed so that a *vector of string views* +/// can be returned to the foreign side as one contiguous array. +/// +/// A single string view is passed across the FFI boundary as two independent parameters +/// (`ptr` and `len`); this struct exists solely so that many views can be packed into one array +/// for [`push_ffi_string_view_vec_with_len`] / [`parse_ffi_string_view_vec_with_len`]. +/// +/// # Ownership +/// +/// `ptr` does **not** point into memory owned by this crate. When produced by +/// [`push_ffi_string_view_vec_with_len`] it points into caller-owned memory; see the module-level +/// "String view ownership" section. +#[repr(C)] +pub struct CStringView { + /// Pointer to the first byte of the viewed string. Not NUL-terminated in general and may + /// contain interior NUL bytes. + pub ptr: CStrPtr, + /// Length in bytes of the viewed string. + pub len: usize, +} + +/// The type representing a pointer to a vector (array) of [`CStringView`] entries. +pub type CStrViewVecPtr = *const CStringView; + +// endregion + // region: String Cache for Exposing +/// One independent string-vector slot. +/// +/// `items` owns the actual `CString` data, each individually boxed so the pointers stay stable +/// even when `items` grows. `ptrs` is the parallel pointer array consumed by C/C++; for the +/// nullptr-terminated variant a trailing null pointer is appended to `ptrs`. +struct CStringVec { + items: Vec>, + ptrs: Vec, +} + +impl CStringVec { + /// Build a string vector from the given entries. + /// + /// A trailing null entry is always appended to the pointer array. This single layout serves + /// both consumers: the nullptr-terminated form scans until the null, and the count-terminated + /// form simply reads [`CStringVec::item_count`] entries and ignores the trailing null. + /// + /// Capacity arithmetic uses checked addition because reserving `items.len() + 1` slots could + /// in theory overflow on an absurdly large input; such a case is treated as unrecoverable. + fn new(items: &[&str]) -> Result { + let ptrs_cap = items + .len() + .checked_add(1) + .expect("CStringVec capacity overflow"); + let mut obj = Self { + items: Vec::with_capacity(items.len()), + ptrs: Vec::with_capacity(ptrs_cap), + }; + for s in items { + let boxed = Box::new(CString::new(*s)?); + obj.ptrs.push(boxed.as_ptr()); + obj.items.push(boxed); + } + obj.ptrs.push(std::ptr::null()); + Ok(obj) + } + + /// Return the head pointer of the parallel pointer array, for consumption by C/C++. + fn head_ptr(&self) -> CStrVecPtr { + self.ptrs.as_ptr() + } + + /// Return the number of real string entries (excludes any trailing null terminator). + fn item_count(&self) -> usize { + self.items.len() + } +} + +/// One independent string-view-vector slot. +/// +/// Only the *container* (`views`) is owned by the cache; each [`CStringView::ptr`] inside points +/// into caller-owned memory and is not managed here. +struct CStringViewVec { + views: Vec, +} + +impl CStringViewVec { + /// Build a string-view vector that re-exposes the caller-owned input strings without + /// copying their data. + fn new(items: &[&str]) -> Self { + let views: Vec = items + .iter() + .map(|s| CStringView { + ptr: s.as_ptr() as CStrPtr, + len: s.len(), + }) + .collect(); + Self { views } + } + + /// Return the head pointer of the view array, for consumption by C/C++. + fn head_ptr(&self) -> CStrViewVecPtr { + self.views.as_ptr() + } + + /// Return the number of view entries. + fn item_count(&self) -> usize { + self.views.len() + } +} + struct StringCache { - msg: CString, + strings: Vec>, + string_vecs: Vec>, + string_view_vecs: Vec>, } impl StringCache { fn new() -> Self { Self { - msg: CString::new("").expect("empty string must be valid for CString"), + strings: Vec::new(), + string_vecs: Vec::new(), + string_view_vecs: Vec::new(), } } - pub fn set_msg(&mut self, msg: &str) -> Result<()> { - self.msg = CString::new(msg)?; - Ok(()) + /// Drop every slot held by this cache. + /// + /// For string-view-vector slots only the *container* is dropped; the caller-owned string data + /// behind each [`CStringView::ptr`] is not (and cannot be) touched here. + fn clear(&mut self) { + self.strings.clear(); + self.string_vecs.clear(); + self.string_view_vecs.clear(); } - pub fn get_msg(&self) -> CStyleString { - self.msg.as_ptr() + /// Store one boxed NUL-terminated string and return its stable pointer. + fn push_string(&mut self, s: Box) -> CStrPtr { + let ptr = s.as_ptr(); + self.strings.push(s); + ptr } - pub fn clear_msg(&mut self) { - self.msg = CString::new("").expect("empty string must be valid for CString"); + /// Store one boxed string-vector slot. + fn push_string_vec(&mut self, sv: Box) { + self.string_vecs.push(sv); + } + + /// Store one boxed string-view-vector slot. + fn push_string_view_vec(&mut self, svv: Box) { + self.string_view_vecs.push(svv); } } // endregion -// region: Exposed Functions +// region: Output Slots Functions thread_local! { static STRING_CACHE: RefCell = RefCell::new(StringCache::new()); } -/// Set thread local string exposed for C code. -pub fn set_ffi_string(msg: &str) -> Result<()> { - STRING_CACHE.with(|e| { - e.borrow_mut().set_msg(msg) - }) -} - -/// Get const pointer to thread local string exposed for C code. -pub fn get_ffi_string() -> CStyleString { - STRING_CACHE.with(|e| e.borrow().get_msg()) -} - -/// Clear thread local string exposed for C code. +/// Clear every single-string, string-vector and string-view-vector slot stored for the current +/// thread. /// -/// This function usually should be called at the beginning of every exposed C functions. -pub fn clear_ffi_string() { - STRING_CACHE.with(|e| { - e.borrow_mut().clear_msg(); - }); +/// This function usually should be called at the beginning of every exposed C function so that +/// each FFI call starts from an empty output cache. +/// +/// Note: clearing a string-view-vector slot only drops the *container* held by this crate; the +/// caller-owned string data behind each [`CStringView::ptr`] is not (and cannot be) touched here. +pub fn clear_ffi_strings() { + STRING_CACHE.with(|c| c.borrow_mut().clear()); } -/// Parse string given by C code into Rust string. -pub fn parse_ffi_string<'a>(ptr: CStyleString) -> Result<&'a str> { +/// Push one independent NUL-terminated string into the thread-local cache and return a stable +/// pointer to it. +/// +/// The string is **copied** into the cache. The returned pointer stays valid until the next +/// [`clear_ffi_strings`] call; the foreign side must duplicate it before the next FFI call. +/// +/// # Errors +/// +/// Returns an error if `s` contains an interior NUL byte. +pub fn push_ffi_string(s: &str) -> Result { + let boxed = Box::new(CString::new(s)?); + STRING_CACHE.with(|c| Ok(c.borrow_mut().push_string(boxed))) +} + +/// Push one independent string vector (NUL-terminated entries, **nullptr-terminated list**) into +/// the thread-local cache and return a stable pointer to the pointer array. +/// +/// The returned array has `items.len() + 1` entries whose final entry is a null pointer acting as +/// the list terminator. The foreign side scans forward until it meets the null entry. An empty +/// `items` yields a one-entry array containing only the terminating null (a valid empty list). +/// +/// All string contents are copied into the cache and stay valid until the next +/// [`clear_ffi_strings`] call. +/// +/// # Errors +/// +/// Returns an error if any entry contains an interior NUL byte. +pub fn push_ffi_string_vec(items: &[&str]) -> Result { + let sv = Box::new(CStringVec::new(items)?); + let head = sv.head_ptr(); + STRING_CACHE.with(|c| c.borrow_mut().push_string_vec(sv)); + Ok(head) +} + +/// Push one independent string vector (NUL-terminated entries, **count-terminated list**) into the +/// thread-local cache and return a stable pointer to the pointer array together with the entry +/// count. +/// +/// The returned array contains exactly `count` entries for the foreign side to read (plus an +/// internal trailing null terminator that is ignored). The foreign side must rely on the returned +/// count instead of scanning for a null. The returned pointer is always valid: the trailing null +/// guarantees a non-empty allocation, so even when `count` is 0 the pointer is non-dangling. +/// +/// All string contents are copied into the cache and stay valid until the next +/// [`clear_ffi_strings`] call. +/// +/// # Errors +/// +/// Returns an error if any entry contains an interior NUL byte. +pub fn push_ffi_string_vec_with_len(items: &[&str]) -> Result<(CStrVecPtr, usize)> { + let sv = Box::new(CStringVec::new(items)?); + let head = sv.head_ptr(); + let count = sv.item_count(); + STRING_CACHE.with(|c| c.borrow_mut().push_string_vec(sv)); + Ok((head, count)) +} + +/// Re-expose a single string as a `(ptr, len)` pair **without copying and without storing anything +/// in the cache**. +/// +/// This is intended exclusively for very long strings. The returned pointer points directly into +/// the memory of `s`; no data is duplicated and no slot is created in the cache. +/// +/// # Ownership / Lifetime -- READ CAREFULLY +/// +/// The caller owns the underlying storage and **must** keep it alive -- unmoved and undropped -- +/// for as long as the foreign (C/C++) side keeps reading through the returned pointer. If `s` +/// (or the buffer backing it) is released before the foreign side finishes reading, the foreign +/// side observes a dangling pointer and the behavior is undefined. This crate does not and cannot +/// enforce this constraint. +/// +/// Because the result is length-delimited, `s` is permitted to contain interior NUL bytes; no +/// scanning is performed. This function cannot fail and returns no `Result`. +pub fn push_ffi_string_view(s: &str) -> (CStrPtr, usize) { + (s.as_ptr() as CStrPtr, s.len()) +} + +/// Build a vector of [`CStringView`] over the given strings and return a stable pointer to that +/// array together with the entry count. +/// +/// Only the *container* array is stored in the cache (so the returned [`CStrViewVecPtr`] stays +/// valid until the next [`clear_ffi_strings`] call); the string data behind each +/// [`CStringView::ptr`] is **not** copied and **not** owned by this crate. +/// +/// # Ownership / Lifetime -- READ CAREFULLY +/// +/// Each entry's `ptr` points directly into the memory of the corresponding input `&str`. The +/// caller **must** keep every source string alive -- unmoved and undropped -- for as long as the +/// foreign (C/C++) side reads through the array. If any source string is released before the +/// foreign side finishes, the foreign side observes dangling pointers and the behavior is +/// undefined. This crate does not and cannot enforce this constraint. +/// +/// Because each entry is length-delimited, the inputs are permitted to contain interior NUL +/// bytes; no scanning is performed. This function cannot fail and returns no `Result`. +/// +/// When `count` is 0, the returned pointer may be a dangling pointer valid only for zero-sized +/// reads (per `Vec::as_ptr` for an empty allocation). The foreign side must drive consumption off +/// `count` and must not dereference the pointer when `count` is 0. +pub fn push_ffi_string_view_vec_with_len(items: &[&str]) -> (CStrViewVecPtr, usize) { + let svv = Box::new(CStringViewVec::new(items)); + let head = svv.head_ptr(); + let count = svv.item_count(); + STRING_CACHE.with(|c| c.borrow_mut().push_string_view_vec(svv)); + (head, count) +} + +// endregion + +// region: Input Slots Functions + +/// Parse a NUL-terminated C/C++ string into a borrowed Rust `&str`. +/// +/// The returned slice borrows the foreign memory; no allocation is performed. It is only valid +/// while the caller keeps the original foreign buffer alive. +/// +/// # Errors +/// +/// Returns an error if `ptr` is null or if the content is not valid UTF-8. +pub fn parse_ffi_string<'a>(ptr: CStrPtr) -> Result<&'a str> { if ptr.is_null() { Err(Error::NullPtr) } else { @@ -100,4 +375,123 @@ pub fn parse_ffi_string<'a>(ptr: CStyleString) -> Result<&'a str> { } } +/// Parse a **nullptr-terminated** vector of NUL-terminated C/C++ strings into borrowed Rust +/// `&str`s. +/// +/// `ptrs` points at an array of [`CStrPtr`] terminated by a null entry. The function scans +/// forward until it meets the null terminator. The returned slices borrow the foreign memory and +/// are only valid while the caller keeps the original buffers alive. No allocation is performed +/// for the string contents themselves (a `Vec` of references is returned). +/// +/// # Errors +/// +/// Returns an error if `ptrs` is null or if any entry is not valid UTF-8. +pub fn parse_ffi_string_vec<'a>(ptrs: CStrVecPtr) -> Result> { + if ptrs.is_null() { + return Err(Error::NullPtr); + } + let mut out = Vec::new(); + let mut idx = 0usize; + loop { + let entry = unsafe { *ptrs.add(idx) }; + if entry.is_null() { + break; + } + out.push(parse_ffi_string(entry)?); + idx += 1; + } + Ok(out) +} + +/// Parse a **count-terminated** vector of NUL-terminated C/C++ strings into borrowed Rust `&str`s. +/// +/// `ptrs` points at an array of exactly `count` [`CStrPtr`] entries (no terminator). The returned +/// slices borrow the foreign memory and are only valid while the caller keeps the original buffers +/// alive. No allocation is performed for the string contents themselves (a `Vec` of references is +/// returned). +/// +/// # Errors +/// +/// Returns an error if `ptrs` is null while `count > 0`, or if any entry is not valid UTF-8. When +/// `count` is 0, a null `ptrs` is accepted and an empty vector is returned. +pub fn parse_ffi_string_vec_with_len<'a>( + ptrs: CStrVecPtr, + count: usize, +) -> Result> { + if count == 0 { + return Ok(Vec::new()); + } + if ptrs.is_null() { + return Err(Error::NullPtr); + } + let mut out = Vec::with_capacity(count); + for idx in 0..count { + let entry = unsafe { *ptrs.add(idx) }; + out.push(parse_ffi_string(entry)?); + } + Ok(out) +} + +/// Parse a length-delimited string view (C++ `std::string_view` semantics) given as an independent +/// pointer and length into a borrowed Rust `&str`. +/// +/// Unlike [`parse_ffi_string`], the buffer is not required to be NUL-terminated, so interior NUL +/// bytes are permitted. A default-constructed C++ `std::string_view` (null pointer, zero length) +/// is interpreted as an empty string. +/// +/// # Lifetime +/// +/// The returned slice borrows the foreign memory and is only valid while the caller keeps the +/// original foreign buffer alive. No allocation is performed. +/// +/// # Errors +/// +/// Returns an error if `ptr` is null while `len > 0`, or if the content is not valid UTF-8. +pub fn parse_ffi_string_view<'a>(ptr: CStrPtr, len: usize) -> Result<&'a str> { + if ptr.is_null() { + return if len == 0 { + Ok("") + } else { + Err(Error::NullPtr) + }; + } + let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, len) }; + Ok(std::str::from_utf8(bytes)?) +} + +/// Parse a **count-terminated** vector of [`CStringView`] entries into borrowed Rust `&str`s. +/// +/// `views` points at an array of exactly `count` [`CStringView`] entries. Each entry is treated +/// with `std::string_view` semantics (pointer + length, no NUL requirement, interior NUL +/// permitted). An entry with a null pointer and zero length is interpreted as an empty string. +/// +/// # Lifetime +/// +/// The returned slices borrow the foreign memory behind each view and are only valid while the +/// caller keeps the original buffers alive. No allocation is performed for the string contents +/// themselves (a `Vec` of references is returned). +/// +/// # Errors +/// +/// Returns an error if `views` is null while `count > 0`, if any entry has a null pointer with +/// non-zero length, or if any entry is not valid UTF-8. When `count` is 0, a null `views` is +/// accepted and an empty vector is returned. +pub fn parse_ffi_string_view_vec_with_len<'a>( + views: CStrViewVecPtr, + count: usize, +) -> Result> { + if count == 0 { + return Ok(Vec::new()); + } + if views.is_null() { + return Err(Error::NullPtr); + } + let mut out = Vec::with_capacity(count); + for idx in 0..count { + let view = unsafe { &*views.add(idx) }; + out.push(parse_ffi_string_view(view.ptr, view.len)?); + } + Ok(out) +} + // endregion diff --git a/omrf/tests/cstr_ffi.rs b/omrf/tests/cstr_ffi.rs new file mode 100644 index 0000000..0013dab --- /dev/null +++ b/omrf/tests/cstr_ffi.rs @@ -0,0 +1,220 @@ +//! Integration tests for the `cstr_ffi` module. +//! +//! These exercise the public API only. The thread-local cache is reset at the start of every test +//! to keep cases independent. Tests that hand raw pointers back into the parse functions do so +//! immediately, within the scope where the backing storage is guaranteed to live. + +use sarasacw_omrf::cstr_ffi; + +fn cptr_of(bytes: &[u8]) -> cstr_ffi::CStrPtr { + bytes.as_ptr() as cstr_ffi::CStrPtr +} + +#[test] +fn push_single_string_round_trip() { + cstr_ffi::clear_ffi_strings(); + let ptr = cstr_ffi::push_ffi_string("hello").unwrap(); + assert!(!ptr.is_null()); + assert_eq!(cstr_ffi::parse_ffi_string(ptr).unwrap(), "hello"); +} + +#[test] +fn pushed_strings_remain_valid_after_more_pushes() { + cstr_ffi::clear_ffi_strings(); + let p1 = cstr_ffi::push_ffi_string("first").unwrap(); + // Force the internal Vec to reallocate by pushing many entries. + let later: Vec<_> = (0..64).map(|_| cstr_ffi::push_ffi_string("filler").unwrap()).collect(); + let p2 = cstr_ffi::push_ffi_string("second").unwrap(); + let p3 = cstr_ffi::push_ffi_string("third").unwrap(); + assert_eq!(cstr_ffi::parse_ffi_string(p1).unwrap(), "first"); + assert_eq!(cstr_ffi::parse_ffi_string(p2).unwrap(), "second"); + assert_eq!(cstr_ffi::parse_ffi_string(p3).unwrap(), "third"); + assert_eq!(cstr_ffi::parse_ffi_string(later[40]).unwrap(), "filler"); +} + +#[test] +fn push_ffi_string_rejects_interior_nul() { + cstr_ffi::clear_ffi_strings(); + assert!(cstr_ffi::push_ffi_string("a\x00b").is_err()); +} + +#[test] +fn parse_ffi_string_null_ptr_errors() { + assert!(cstr_ffi::parse_ffi_string(std::ptr::null()).is_err()); +} + +#[test] +fn parse_ffi_string_invalid_utf8_errors() { + let mut buf: Vec = vec![b'h', b'i', 0xFF]; + buf.push(0); + let ptr = cptr_of(&buf); + assert!(cstr_ffi::parse_ffi_string(ptr).is_err()); +} + +#[test] +fn push_string_vec_nullptr_terminated_round_trip() { + cstr_ffi::clear_ffi_strings(); + let head = cstr_ffi::push_ffi_string_vec(&["a", "bb", "ccc"]).unwrap(); + let collected = cstr_ffi::parse_ffi_string_vec(head).unwrap(); + assert_eq!(collected, vec!["a", "bb", "ccc"]); +} + +#[test] +fn push_string_vec_empty_is_valid_empty_list() { + cstr_ffi::clear_ffi_strings(); + let head = cstr_ffi::push_ffi_string_vec(&[]).unwrap(); + let collected = cstr_ffi::parse_ffi_string_vec(head).unwrap(); + assert!(collected.is_empty()); +} + +#[test] +fn push_string_vec_rejects_interior_nul() { + cstr_ffi::clear_ffi_strings(); + assert!(cstr_ffi::push_ffi_string_vec(&["ok", "ba\x00d"]).is_err()); +} + +#[test] +fn parse_ffi_string_vec_null_ptr_errors() { + assert!(cstr_ffi::parse_ffi_string_vec(std::ptr::null()).is_err()); +} + +#[test] +fn push_string_vec_with_len_round_trip() { + cstr_ffi::clear_ffi_strings(); + let (head, count) = cstr_ffi::push_ffi_string_vec_with_len(&["x", "yy", "zzz"]).unwrap(); + assert_eq!(count, 3); + let collected = cstr_ffi::parse_ffi_string_vec_with_len(head, count).unwrap(); + assert_eq!(collected, vec!["x", "yy", "zzz"]); +} + +#[test] +fn push_string_vec_with_len_empty() { + cstr_ffi::clear_ffi_strings(); + let (head, count) = cstr_ffi::push_ffi_string_vec_with_len(&[]).unwrap(); + assert_eq!(count, 0); + let collected = cstr_ffi::parse_ffi_string_vec_with_len(head, count).unwrap(); + assert!(collected.is_empty()); +} + +#[test] +fn parse_ffi_string_vec_with_len_null_zero_len_is_empty() { + assert!( + cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 0) + .unwrap() + .is_empty() + ); +} + +#[test] +fn parse_ffi_string_vec_with_len_null_nonzero_errors() { + assert!(cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 1).is_err()); +} + +#[test] +fn multiple_string_vec_slots_coexist() { + cstr_ffi::clear_ffi_strings(); + let single = cstr_ffi::push_ffi_string("solo").unwrap(); + let (h1, c1) = cstr_ffi::push_ffi_string_vec_with_len(&["l1a", "l1b"]).unwrap(); + let h2 = cstr_ffi::push_ffi_string_vec(&["l2a", "l2b", "l2c"]).unwrap(); + assert_eq!(cstr_ffi::parse_ffi_string(single).unwrap(), "solo"); + assert_eq!( + cstr_ffi::parse_ffi_string_vec_with_len(h1, c1).unwrap(), + vec!["l1a", "l1b"] + ); + assert_eq!( + cstr_ffi::parse_ffi_string_vec(h2).unwrap(), + vec!["l2a", "l2b", "l2c"] + ); +} + +#[test] +fn push_ffi_string_view_points_into_source() { + cstr_ffi::clear_ffi_strings(); + let owned = String::from("a long string that we keep alive"); + let (ptr, len) = cstr_ffi::push_ffi_string_view(&owned); + assert_eq!(len, owned.len()); + assert_eq!(ptr as *const u8, owned.as_ptr()); + let parsed = cstr_ffi::parse_ffi_string_view(ptr, len).unwrap(); + assert_eq!(parsed, owned); +} + +#[test] +fn string_view_allows_interior_nul() { + let src: &str = "ab\x00cd"; + let (ptr, len) = cstr_ffi::push_ffi_string_view(src); + assert_eq!(len, 5); + let parsed = cstr_ffi::parse_ffi_string_view(ptr, len).unwrap(); + assert_eq!(parsed.as_bytes(), src.as_bytes()); +} + +#[test] +fn parse_ffi_string_view_null_zero_is_empty() { + assert_eq!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 0).unwrap(), ""); +} + +#[test] +fn parse_ffi_string_view_null_nonzero_errors() { + assert!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 3).is_err()); +} + +#[test] +fn parse_ffi_string_view_invalid_utf8_errors() { + let bad: [u8; 2] = [b'a', 0xFF]; + let ptr = cptr_of(&bad); + assert!(cstr_ffi::parse_ffi_string_view(ptr, 2).is_err()); +} + +#[test] +fn push_string_view_vec_with_len_round_trip() { + cstr_ffi::clear_ffi_strings(); + let a = String::from("alpha"); + let b = String::from("beta"); + let c = String::from("gamma"); + let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[&a, &b, &c]); + assert_eq!(count, 3); + let parsed = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap(); + assert_eq!(parsed, vec!["alpha", "beta", "gamma"]); +} + +#[test] +fn push_string_view_vec_with_len_empty() { + cstr_ffi::clear_ffi_strings(); + let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[]); + assert_eq!(count, 0); + // count is 0, so head is ignored (may be dangling); parsing must yield an empty vector. + let collected = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap(); + assert!(collected.is_empty()); +} + +#[test] +fn string_view_vec_allows_interior_nul_entries() { + cstr_ffi::clear_ffi_strings(); + let s1: &str = "x\x00y"; + let s2: &str = "p\x00\x00q"; + let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[s1, s2]); + assert_eq!(count, 2); + let parsed = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap(); + assert_eq!(parsed[0].as_bytes(), s1.as_bytes()); + assert_eq!(parsed[1].as_bytes(), s2.as_bytes()); +} + +#[test] +fn parse_ffi_string_view_vec_null_zero_len_is_empty() { + assert!(cstr_ffi::parse_ffi_string_view_vec_with_len(std::ptr::null(), 0) + .unwrap() + .is_empty()); +} + +#[test] +fn parse_ffi_string_view_vec_null_nonzero_errors() { + assert!(cstr_ffi::parse_ffi_string_view_vec_with_len(std::ptr::null(), 1).is_err()); +} + +#[test] +fn clear_makes_cache_reusable() { + cstr_ffi::clear_ffi_strings(); + let _first = cstr_ffi::push_ffi_string("before-clear").unwrap(); + cstr_ffi::clear_ffi_strings(); + let after = cstr_ffi::push_ffi_string("after-clear").unwrap(); + assert_eq!(cstr_ffi::parse_ffi_string(after).unwrap(), "after-clear"); +}