use crate::interop::*;
use ferroui_microcom::{ComPtr, HResult};
use std::ffi::{c_void, CString};
pub struct FrnString {
string: String,
native: Vec<u8>,
}
impl FrnString {
pub fn new(s: &str) -> Self {
let mut native = Vec::with_capacity(s.len() + 1);
native.extend_from_slice(s.as_bytes());
native.push(0);
Self { string: s.to_owned(), native }
}
pub fn string(&self) -> &str {
&self.string
}
pub fn bytes(&self) -> &[u8] {
self.string.as_bytes()
}
}
impl IFrnStringImpl for FrnString {
fn pointer(&self) -> Result<*mut c_void, HResult> {
if self.string.is_empty() {
return Ok(std::ptr::null_mut());
}
Ok(self.native.as_ptr() as *mut c_void)
}
fn length(&self) -> Result<i32, HResult> {
Ok(self.string.len() as i32)
}
}
pub struct FrnStringArray {
items: Vec<(String, ComPtr<IFrnString>)>,
}
impl FrnStringArray {
pub fn new<S: AsRef<str>>(items: impl IntoIterator<Item = S>) -> Self {
Self {
items: items
.into_iter()
.map(|s| (s.as_ref().to_owned(), IFrnString::from_impl(FrnString::new(s.as_ref()))))
.collect(),
}
}
pub fn to_string_array(&self) -> Vec<String> {
self.items.iter().map(|(s, _)| s.clone()).collect()
}
}
impl IFrnStringArrayImpl for FrnStringArray {
fn get_count(&self) -> u32 {
self.items.len() as u32
}
fn get(&self, index: u32) -> Result<Option<ComPtr<IFrnString>>, HResult> {
match self.items.get(index as usize) {
Some((_, item)) => Ok(Some(item.clone())),
None => Err(HResult::INVALIDARG),
}
}
}
pub fn frn_string_bytes(s: &IFrnString) -> Vec<u8> {
let (Ok(ptr), Ok(len)) = (s.pointer(), s.length()) else {
return Vec::new();
};
if ptr.is_null() || len <= 0 {
return Vec::new();
}
unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) }.to_vec()
}
pub fn frn_string_to_string(s: &IFrnString) -> Option<String> {
match s.pointer() {
Ok(ptr) if !ptr.is_null() => Some(String::from_utf8_lossy(&frn_string_bytes(s)).into_owned()),
_ => None,
}
}
pub fn frn_string_array_to_vec(array: &IFrnStringArray) -> Vec<String> {
(0..array.get_count())
.map(|index| match array.get(index) {
Ok(Some(s)) => frn_string_to_string(&s).unwrap_or_default(),
_ => String::new(),
})
.collect()
}
pub(crate) fn to_c_string(s: &str) -> CString {
let end = s.bytes().position(|b| b == 0).unwrap_or(s.len());
CString::new(&s.as_bytes()[..end]).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_round_trips_through_its_com_interface() {
let s = IFrnString::from_impl(FrnString::new("héllo ⌘"));
assert_eq!(s.length(), Ok("héllo ⌘".len() as i32));
assert_eq!(frn_string_to_string(&s).as_deref(), Some("héllo ⌘"));
assert_eq!(frn_string_bytes(&s), "héllo ⌘".as_bytes());
let ptr = s.pointer().unwrap() as *const u8;
assert_eq!(unsafe { *ptr.add("héllo ⌘".len()) }, 0);
}
#[test]
fn empty_string_has_no_buffer() {
let s = IFrnString::from_impl(FrnString::new(""));
assert!(s.pointer().unwrap().is_null());
assert_eq!(s.length(), Ok(0));
assert_eq!(frn_string_to_string(&s), None);
assert!(frn_string_bytes(&s).is_empty());
}
#[test]
fn string_array_round_trips() {
let source = FrnStringArray::new(["a", "", "ccc"]);
assert_eq!(source.to_string_array(), ["a", "", "ccc"]);
let array = IFrnStringArray::from_impl(source);
assert_eq!(array.get_count(), 3);
assert_eq!(frn_string_array_to_vec(&array), ["a", "", "ccc"]);
assert_eq!(array.get(3).unwrap_err(), HResult::INVALIDARG);
}
#[test]
fn c_strings_stop_at_the_first_nul() {
assert_eq!(to_c_string("title").as_bytes(), b"title");
assert_eq!(to_c_string("a\0b").as_bytes(), b"a");
assert_eq!(to_c_string("").as_bytes(), b"");
}
}