hapi_rs/
stringhandle.rs

1//! String handling utilities and iterators
2use std::ffi::{CStr, CString};
3use std::fmt::Formatter;
4
5use crate::errors::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, PartialEq, Eq)]
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, PartialEq)]
20pub struct StringArray(pub 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 = crate::ffi::get_string_bytes(session, handle)?;
148    String::from_utf8(bytes).map_err(crate::HapiError::from)
149}
150
151pub(crate) fn get_cstring(handle: StringHandle, session: &Session) -> Result<CString> {
152    let bytes = crate::ffi::get_string_bytes(session, handle)?;
153    CString::new(bytes).map_err(crate::HapiError::from)
154}
155
156pub fn get_string_array(handles: &[StringHandle], session: &Session) -> Result<StringArray> {
157    let _lock = session.lock();
158    let length = crate::ffi::get_string_batch_size(handles, session)?;
159    let bytes = if length > 0 {
160        crate::ffi::get_string_batch_bytes(length, session)?
161    } else {
162        vec![]
163    };
164    Ok(StringArray(bytes))
165}
166
167#[cfg(test)]
168mod tests {
169    use super::StringArray;
170    use crate::ffi;
171    use crate::server::ServerOptions;
172    use crate::session::{Session, SessionOptions, new_thrift_session};
173    use once_cell::sync::Lazy;
174    use std::ffi::CString;
175
176    static SESSION: Lazy<Session> = Lazy::new(|| {
177        let _ = env_logger::try_init().ok();
178        new_thrift_session(
179            SessionOptions::default(),
180            ServerOptions::shared_memory_with_defaults(),
181        )
182        .expect("Could not create test session")
183    });
184
185    #[test]
186    fn get_string_api() {
187        let h = ffi::get_server_env_str(&SESSION, &CString::new("HFS").unwrap()).unwrap();
188        assert!(super::get_string(h, &SESSION).is_ok());
189        assert!(super::get_cstring(h, &SESSION).is_ok());
190    }
191
192    #[test]
193    fn string_array_api() {
194        SESSION
195            .set_server_var::<str>("TEST", "177")
196            .expect("could not set var");
197        let var_count = ffi::get_server_env_var_count(&SESSION).unwrap();
198        let handles = ffi::get_server_env_var_list(&SESSION, var_count).unwrap();
199        let array = super::get_string_array(&handles, &SESSION).unwrap();
200        assert_eq!(array.iter_str().count(), var_count as usize);
201        assert_eq!(array.iter_cstr().count(), var_count as usize);
202        assert!(array.iter_str().any(|s| s == "TEST=177"));
203        assert!(
204            array
205                .iter_cstr()
206                .any(|s| s.to_bytes_with_nul() == b"TEST=177\0")
207        );
208        let mut owned: super::OwnedStringIter = array.into_iter();
209        assert!(owned.any(|s| s == "TEST=177"));
210
211        let arr = StringArray(b"One\0Two\0Three\0".to_vec());
212        let v: Vec<_> = arr.iter_cstr().collect();
213        assert_eq!(v[0].to_bytes_with_nul(), b"One\0");
214        assert_eq!(v[2].to_bytes_with_nul(), b"Three\0");
215    }
216
217    #[test]
218    fn test_set_custom_string() {
219        let handle = SESSION.set_custom_string("HAPI_RS_CUSTOM_STRING").unwrap();
220        assert_eq!(SESSION.get_string(handle).unwrap(), "HAPI_RS_CUSTOM_STRING");
221        SESSION.remove_custom_string(handle).unwrap();
222        assert!(SESSION.get_string(handle).is_err());
223    }
224}