//! 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 } /// Coverage: single NUL-terminated string push -> parse round trip. #[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"); } /// Coverage: pointers returned by earlier `push_ffi_string` calls stay valid after the internal /// storage reallocates from many later pushes (move-stability of the cached `CString` buffers). #[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"); } /// Coverage: `push_ffi_string` rejects a string containing an interior NUL byte. #[test] fn push_ffi_string_rejects_interior_nul() { cstr_ffi::clear_ffi_strings(); assert!(cstr_ffi::push_ffi_string("a\x00b").is_err()); } /// Coverage: `parse_ffi_string` rejects a null pointer. #[test] fn parse_ffi_string_null_ptr_errors() { assert!(cstr_ffi::parse_ffi_string(std::ptr::null()).is_err()); } /// Coverage: `parse_ffi_string` rejects a NUL-terminated buffer whose content is not valid UTF-8. #[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()); } /// Coverage: nullptr-terminated string vector push -> parse round trip. #[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"]); } /// Coverage: an empty input to `push_ffi_string_vec` yields a valid empty list (only the trailing /// null), which parses back to an empty vector. #[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()); } /// Coverage: `push_ffi_string_vec` rejects an entry containing an interior NUL byte. #[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()); } /// Coverage: `parse_ffi_string_vec` (nullptr-terminated) rejects a null pointer. #[test] fn parse_ffi_string_vec_null_ptr_errors() { assert!(cstr_ffi::parse_ffi_string_vec(std::ptr::null()).is_err()); } /// Coverage: count-terminated string vector push -> parse round trip, with the returned count. #[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"]); } /// Coverage: empty input to `push_ffi_string_vec_with_len` yields count 0 and parses to an empty /// vector. #[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()); } /// Coverage: `parse_ffi_string_vec_with_len` with count 0 accepts a null pointer and returns an /// empty vector. #[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()); } /// Coverage: `parse_ffi_string_vec_with_len` with count > 0 rejects a null pointer. #[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()); } /// Coverage: independent single-string, count-terminated, and nullptr-terminated slots produced in /// one call cycle all coexist and read back correctly. #[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"] ); } /// Coverage: `push_ffi_string_view` re-exposes the source string in place (no copy) -- the pointer /// aliases the source buffer and the length matches. #[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); } /// Coverage: a string view may contain interior NUL bytes (length-delimited, not NUL-terminated). #[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()); } /// Coverage: `parse_ffi_string_view` with a null pointer and zero length is interpreted as an /// empty string (C++ default `std::string_view`). #[test] fn parse_ffi_string_view_null_zero_is_empty() { assert_eq!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 0).unwrap(), ""); } /// Coverage: `parse_ffi_string_view` rejects a null pointer when the length is non-zero. #[test] fn parse_ffi_string_view_null_nonzero_errors() { assert!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 3).is_err()); } /// Coverage: `parse_ffi_string_view` rejects a buffer whose content is not valid UTF-8. #[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()); } /// Coverage: string-view vector push -> parse round trip; the container is cached while the string /// data stays caller-owned. #[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"]); } /// Coverage: empty input to `push_ffi_string_view_vec_with_len` yields count 0; parsing (which /// ignores the possibly-dangling head) returns an empty vector. #[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); let collected = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap(); assert!(collected.is_empty()); } /// Coverage: string-view vector entries may individually contain interior NUL bytes. #[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()); } /// Coverage: `parse_ffi_string_view_vec_with_len` with count 0 accepts a null pointer and returns /// an empty vector. #[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()); } /// Coverage: `parse_ffi_string_view_vec_with_len` with count > 0 rejects a null pointer. #[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()); } /// Coverage: after `clear_ffi_strings`, the cache is reusable -- a fresh push/parse works /// independently of any state pushed before the clear. #[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"); }