hapi_rs/
stringhandle.rs

1//! String handling utilities and iterators
2use std::ffi::{CStr, CString};
3use std::fmt::Formatter;
4
5use crate::errors::{ErrorContext, Result};
6use crate::session::Session;
7
8// StringArray iterators SAFETY: Are Houdini strings expected to be valid utf? Maybe revisit.
9
10/// A handle to a string returned by some api.
11/// Then the String can be retrieved with [`Session::get_string`]
12#[derive(Debug, Copy, Clone)]
13#[repr(transparent)]
14pub struct StringHandle(pub(crate) i32);
15
16/// Holds a contiguous array of bytes where each individual string value is null-separated.
17/// You can choose how to iterate over it by calling a corresponding iter_* function.
18/// The `Debug` impl has an alternative `{:#?}` representation, which prints as a vec of strings.
19#[derive(Clone)]
20pub struct StringArray(Vec<u8>);
21
22impl std::fmt::Debug for StringArray {
23    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24        if f.alternate() {
25            let strings = self.iter_str().collect::<Vec<_>>();
26            strings.fmt(f)
27        } else {
28            let count = self.0.iter().filter(|v| **v == b'\0').count();
29            write!(f, "StringArray[num_strings = {count}]")
30        }
31    }
32}
33
34/// Iterator over &str, returned from StringArray::iter_str()
35pub struct StringIter<'a> {
36    inner: &'a [u8],
37}
38
39/// Consuming iterator over String, returned from StringArray::into_iter()
40pub struct OwnedStringIter {
41    inner: Vec<u8>,
42    cursor: usize,
43}
44
45impl Iterator for OwnedStringIter {
46    type Item = String;
47
48    fn next(&mut self) -> Option<Self::Item> {
49        match self
50            .inner
51            .iter()
52            .skip(self.cursor)
53            .position(|b| *b == b'\0')
54        {
55            None => None,
56            Some(pos) => {
57                let idx = self.cursor + pos;
58                let ret = &self.inner[self.cursor..idx];
59                self.cursor = idx + 1;
60                Some(String::from_utf8_lossy(ret).to_string())
61            }
62        }
63    }
64}
65
66/// Iterator over CStrings returned from StringArray::iter_cstr()
67pub struct CStringIter<'a> {
68    inner: &'a [u8],
69}
70
71impl<'a> StringArray {
72    /// Create an empty StringArray
73    pub fn empty() -> StringArray {
74        StringArray(vec![])
75    }
76    /// Return an iterator over &str
77    pub fn iter_str(&'a self) -> StringIter<'a> {
78        StringIter { inner: &self.0 }
79    }
80
81    /// Return an iterator over &CStr
82    pub fn iter_cstr(&'a self) -> CStringIter<'a> {
83        CStringIter { inner: &self.0 }
84    }
85
86    #[inline]
87    pub fn is_empty(&self) -> bool {
88        self.0.is_empty()
89    }
90
91    /// Reference to underlying bytes
92    #[inline]
93    pub fn bytes(&self) -> &[u8] {
94        self.0.as_slice()
95    }
96}
97
98impl From<StringArray> for Vec<String> {
99    fn from(a: StringArray) -> Self {
100        a.into_iter().collect()
101    }
102}
103
104impl<'a> Iterator for StringIter<'a> {
105    type Item = &'a str;
106
107    fn next(&mut self) -> Option<Self::Item> {
108        match self.inner.iter().position(|c| *c == b'\0') {
109            None => None,
110            Some(idx) => {
111                let ret = &self.inner[..idx];
112                self.inner = &self.inner[idx + 1..];
113                unsafe { Some(std::str::from_utf8_unchecked(ret)) }
114            }
115        }
116    }
117}
118
119impl<'a> Iterator for CStringIter<'a> {
120    type Item = &'a CStr;
121
122    fn next(&mut self) -> Option<Self::Item> {
123        match self.inner.iter().position(|c| *c == b'\0') {
124            None => None,
125            Some(idx) => {
126                let ret = &self.inner[..idx + 1];
127                self.inner = &self.inner[idx + 1..];
128                unsafe { Some(CStr::from_bytes_with_nul_unchecked(ret)) }
129            }
130        }
131    }
132}
133
134impl IntoIterator for StringArray {
135    type Item = String;
136    type IntoIter = OwnedStringIter;
137
138    fn into_iter(self) -> Self::IntoIter {
139        OwnedStringIter {
140            inner: self.0,
141            cursor: 0,
142        }
143    }
144}
145
146pub(crate) fn get_string(handle: StringHandle, session: &Session) -> Result<String> {
147    let bytes = get_string_bytes(handle, session).context("Calling get_string_bytes")?;
148    String::from_utf8(bytes).map_err(crate::errors::HapiError::from)
149}
150
151pub(crate) fn get_cstring(handle: StringHandle, session: &Session) -> Result<CString> {
152    unsafe {
153        let bytes = get_string_bytes(handle, session).context("Calling get_string_bytes")?;
154        // SAFETY: HAPI C API should not return strings with interior zero byte
155        Ok(CString::from_vec_unchecked(bytes))
156    }
157}
158
159pub(crate) fn get_string_bytes(handle: StringHandle, session: &Session) -> Result<Vec<u8>> {
160    if handle.0 < 0 {
161        return Ok(Vec::new());
162    }
163    let length = crate::ffi::get_string_buff_len(session, handle.0)?;
164    if length == 0 {
165        Ok(Vec::new())
166    } else {
167        crate::ffi::get_string(session, handle.0, length)
168    }
169}
170
171pub fn get_string_array(handles: &[StringHandle], session: &Session) -> Result<StringArray> {
172    let _lock = session.lock();
173    let length = crate::ffi::get_string_batch_size(handles, session)?;
174    let bytes = if length > 0 {
175        crate::ffi::get_string_batch(length, session)?
176    } else {
177        vec![]
178    };
179    Ok(StringArray(bytes))
180}
181
182#[cfg(test)]
183mod tests {
184    use super::StringArray;
185    use crate::ffi;
186    use crate::session::Session;
187    use once_cell::sync::Lazy;
188    use std::ffi::CString;
189
190    static SESSION: Lazy<Session> = Lazy::new(|| {
191        let _ = env_logger::try_init().ok();
192        crate::session::quick_session(None).expect("Could not create test session")
193    });
194
195    #[test]
196    fn get_string_api() {
197        let h = ffi::get_server_env_str(&SESSION, &CString::new("HFS").unwrap()).unwrap();
198        assert!(super::get_string(h, &SESSION).is_ok());
199        assert!(super::get_cstring(h, &SESSION).is_ok());
200    }
201
202    #[test]
203    fn string_array_api() {
204        SESSION
205            .set_server_var::<str>("TEST", "177")
206            .expect("could not set var");
207        let var_count = ffi::get_server_env_var_count(&SESSION).unwrap();
208        let handles = ffi::get_server_env_var_list(&SESSION, var_count).unwrap();
209        let array = super::get_string_array(&handles, &SESSION).unwrap();
210        assert_eq!(array.iter_str().count(), var_count as usize);
211        assert_eq!(array.iter_cstr().count(), var_count as usize);
212        assert!(array.iter_str().any(|s| s == "TEST=177"));
213        assert!(array
214            .iter_cstr()
215            .any(|s| s.to_bytes_with_nul() == b"TEST=177\0"));
216        let mut owned: super::OwnedStringIter = array.into_iter();
217        assert!(owned.any(|s| s == "TEST=177"));
218
219        let arr = StringArray(b"One\0Two\0Three\0".to_vec());
220        let v: Vec<_> = arr.iter_cstr().collect();
221        assert_eq!(v[0].to_bytes_with_nul(), b"One\0");
222        assert_eq!(v[2].to_bytes_with_nul(), b"Three\0");
223    }
224}