nautilus_core/ffi/
parsing.rs1use std::{
28 collections::HashMap,
29 ffi::{CStr, CString, c_char},
30};
31
32use serde::de::DeserializeOwned;
33use serde_json::Value;
34use ustr::Ustr;
35
36use crate::{
37 ffi::{abort_on_panic, string::cstr_as_str},
38 string::parsing::min_increment_precision_from_str,
39};
40
41#[must_use]
52pub unsafe fn bytes_to_string_vec(ptr: *const c_char) -> Vec<String> {
53 assert!(!ptr.is_null(), "`ptr` was NULL");
54
55 let c_str = unsafe { CStr::from_ptr(ptr) };
57 let bytes = c_str.to_bytes();
58
59 let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
60 let value: serde_json::Value =
61 serde_json::from_str(json_string).expect("C string contains invalid JSON");
62
63 let arr = value
64 .as_array()
65 .expect("C string JSON must be an array of strings");
66
67 arr.iter()
68 .map(|value| {
69 value
70 .as_str()
71 .expect("C string JSON array must contain only strings")
72 .to_owned()
73 })
74 .collect()
75}
76
77#[must_use]
83pub fn string_vec_to_bytes(strings: &[String]) -> *const c_char {
84 let json_string = serde_json::to_string(strings).expect("Failed to serialize strings to JSON");
85 let c_string = CString::new(json_string).expect("JSON string contains interior null bytes");
86
87 c_string.into_raw()
88}
89
90#[must_use]
100pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option<HashMap<String, Value>> {
101 unsafe { optional_json_from_cstr(ptr) }
103}
104
105#[must_use]
115pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option<HashMap<Ustr, Ustr>> {
116 unsafe { optional_json_from_cstr(ptr) }
118}
119
120#[must_use]
130pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option<Vec<String>> {
131 unsafe { optional_json_from_cstr(ptr) }
133}
134
135unsafe fn optional_json_from_cstr<T>(ptr: *const c_char) -> Option<T>
140where
141 T: DeserializeOwned,
142{
143 if ptr.is_null() {
144 return None;
145 }
146
147 let json = unsafe { cstr_as_str(ptr) };
149 let result = serde_json::from_str(json).expect("C string contains invalid JSON");
150 Some(result)
151}
152
153#[unsafe(no_mangle)]
163pub unsafe extern "C" fn precision_from_cstr(ptr: *const c_char) -> u8 {
164 abort_on_panic(|| {
165 assert!(!ptr.is_null(), "`ptr` was NULL");
166 let s = unsafe { cstr_as_str(ptr) };
168 precision_from_v1_str(s)
169 })
170}
171
172#[unsafe(no_mangle)]
182pub unsafe extern "C" fn min_increment_precision_from_cstr(ptr: *const c_char) -> u8 {
183 abort_on_panic(|| {
184 assert!(!ptr.is_null(), "`ptr` was NULL");
185 let s = unsafe { cstr_as_str(ptr) };
187 min_increment_precision_from_str(s)
188 })
189}
190
191fn precision_from_v1_str(value: &str) -> u8 {
193 let value = value.trim().to_ascii_lowercase();
194
195 if value.contains("e-") {
196 let exponent = value
197 .split("e-")
198 .nth(1)
199 .expect("Invalid scientific notation format: missing exponent after 'e-'");
200
201 if let Ok(exponent) = exponent.parse::<u64>() {
202 return u8::try_from(exponent).unwrap_or(u8::MAX);
203 }
204
205 assert!(
206 !exponent.is_empty(),
207 "Invalid scientific notation format: missing exponent after 'e-'"
208 );
209
210 if exponent.chars().all(|c| c.is_ascii_digit()) {
211 return u8::MAX;
212 }
213
214 panic!("Invalid scientific notation exponent '{exponent}': must be a valid number");
215 }
216
217 value.split_once('.').map_or(0, |(_, decimal)| {
218 u8::try_from(decimal.len()).unwrap_or(u8::MAX)
219 })
220}
221
222#[must_use]
224pub const fn u8_as_bool(value: u8) -> bool {
225 value != 0
226}
227
228#[cfg(test)]
229mod tests {
230 use std::ffi::CString;
231
232 use rstest::rstest;
233
234 use super::*;
235
236 #[rstest]
237 fn test_optional_bytes_to_json_null() {
238 let ptr = std::ptr::null();
239 let result = unsafe { optional_bytes_to_json(ptr) };
240 assert_eq!(result, None);
241 }
242
243 #[rstest]
244 fn test_optional_bytes_to_json_empty() {
245 let json_str = CString::new("{}").unwrap();
246 let ptr = json_str.as_ptr().cast::<c_char>();
247 let result = unsafe { optional_bytes_to_json(ptr) };
248 assert_eq!(result, Some(HashMap::new()));
249 }
250
251 #[rstest]
252 fn test_string_vec_to_bytes_valid() {
253 let strings = vec!["value1", "value2", "value3"]
254 .into_iter()
255 .map(String::from)
256 .collect::<Vec<String>>();
257
258 let ptr = string_vec_to_bytes(&strings);
259
260 let result = unsafe { bytes_to_string_vec(ptr) };
261 assert_eq!(result, strings);
262 }
263
264 #[rstest]
265 fn test_string_vec_to_bytes_empty() {
266 let strings = Vec::new();
267 let ptr = string_vec_to_bytes(&strings);
268
269 let result = unsafe { bytes_to_string_vec(ptr) };
270 assert_eq!(result, strings);
271 }
272
273 #[rstest]
274 fn test_bytes_to_string_vec_valid() {
275 let json_str = CString::new(r#"["value1", "value2", "value3"]"#).unwrap();
276 let ptr = json_str.as_ptr().cast::<c_char>();
277 let result = unsafe { bytes_to_string_vec(ptr) };
278
279 let expected_vec = vec!["value1", "value2", "value3"]
280 .into_iter()
281 .map(String::from)
282 .collect::<Vec<String>>();
283
284 assert_eq!(result, expected_vec);
285 }
286
287 #[rstest]
288 #[should_panic(expected = "array must contain only strings")]
289 fn test_bytes_to_string_vec_invalid() {
290 let json_str = CString::new(r#"["value1", 42, "value3"]"#).unwrap();
291 let ptr = json_str.as_ptr().cast::<c_char>();
292 let _ = unsafe { bytes_to_string_vec(ptr) };
293 }
294
295 #[rstest]
296 fn test_optional_bytes_to_json_valid() {
297 let json_str = CString::new(r#"{"key1": "value1", "key2": 2}"#).unwrap();
298 let ptr = json_str.as_ptr().cast::<c_char>();
299 let result = unsafe { optional_bytes_to_json(ptr) };
300 let mut expected_map = HashMap::new();
301 expected_map.insert("key1".to_owned(), Value::String("value1".to_owned()));
302 expected_map.insert(
303 "key2".to_owned(),
304 Value::Number(serde_json::Number::from(2)),
305 );
306 assert_eq!(result, Some(expected_map));
307 }
308
309 #[rstest]
310 fn test_optional_bytes_to_str_map_valid() {
311 let json_str = CString::new(r#"{"key1": "value1", "key2": "value2"}"#).unwrap();
312 let ptr = json_str.as_ptr().cast::<c_char>();
313 let result = unsafe { optional_bytes_to_str_map(ptr) };
314 let expected_map = HashMap::from([
315 (Ustr::from("key1"), Ustr::from("value1")),
316 (Ustr::from("key2"), Ustr::from("value2")),
317 ]);
318 assert_eq!(result, Some(expected_map));
319 }
320
321 #[rstest]
322 fn test_optional_bytes_to_str_vec_valid() {
323 let json_str = CString::new(r#"["value1", "value2", "value3"]"#).unwrap();
324 let ptr = json_str.as_ptr().cast::<c_char>();
325 let result = unsafe { optional_bytes_to_str_vec(ptr) };
326 let expected_vec = vec![
327 "value1".to_string(),
328 "value2".to_string(),
329 "value3".to_string(),
330 ];
331 assert_eq!(result, Some(expected_vec));
332 }
333
334 #[rstest]
335 #[should_panic(expected = "C string contains invalid JSON")]
336 fn test_optional_bytes_to_json_invalid() {
337 let json_str = CString::new(r#"{"key1": "value1", "key2": }"#).unwrap();
338 let ptr = json_str.as_ptr().cast::<c_char>();
339 let _result = unsafe { optional_bytes_to_json(ptr) };
340 }
341
342 #[rstest]
343 #[case("1e8", 0)]
344 #[case("123", 0)]
345 #[case("123.45", 2)]
346 #[case("123.456789", 6)]
347 #[case("2.5e4", 3)]
348 #[case("7.89E1", 4)]
349 #[case("1.23456789e-2", 2)]
350 #[case("1.23456789e-12", 12)]
351 fn test_precision_from_cstr(#[case] input: &str, #[case] expected: u8) {
352 let c_str = CString::new(input).unwrap();
353 assert_eq!(unsafe { precision_from_cstr(c_str.as_ptr()) }, expected);
354 }
355}