nautilus_core/ffi/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Helper functions that convert common C types (primarily UTF-8 encoded `char *` pointers) into
17//! the Rust data structures used throughout PoseiTrader.
18//!
19//! The conversions are opinionated:
20//!
21//! * JSON is used as the interchange format for complex structures.
22//! * `ustr::Ustr` is preferred over `String` where possible for its performance benefits.
23//!
24//! All functions are `#[must_use]` and, unless otherwise noted, **assume** that the input pointer
25//! is non-null and points to a valid, *null-terminated* UTF-8 string.
26
27use std::{
28    collections::HashMap,
29    ffi::{CStr, CString, c_char},
30};
31
32use serde_json::Value;
33use ustr::Ustr;
34
35use crate::{
36    ffi::string::cstr_as_str,
37    parsing::{min_increment_precision_from_str, precision_from_str},
38};
39
40/// Convert a C bytes pointer into an owned `Vec<String>`.
41///
42/// # Safety
43///
44/// Assumes `ptr` is a valid C string pointer.
45///
46/// # Panics
47///
48/// Panics if `ptr` is null, contains invalid UTF-8, or is invalid JSON.
49#[must_use]
50pub unsafe fn bytes_to_string_vec(ptr: *const c_char) -> Vec<String> {
51    assert!(!ptr.is_null(), "`ptr` was NULL");
52
53    let c_str = unsafe { CStr::from_ptr(ptr) };
54    let bytes = c_str.to_bytes();
55
56    let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
57    let value: serde_json::Value =
58        serde_json::from_str(json_string).expect("C string contains invalid JSON");
59
60    match value {
61        serde_json::Value::Array(arr) => arr
62            .into_iter()
63            .filter_map(|value| match value {
64                serde_json::Value::String(string_value) => Some(string_value),
65                _ => None,
66            })
67            .collect(),
68        _ => Vec::new(),
69    }
70}
71
72/// Convert a slice of `String` into a C string pointer (JSON encoded).
73///
74/// # Panics
75///
76/// Panics if JSON serialization fails or if the generated string contains interior null bytes.
77#[must_use]
78pub fn string_vec_to_bytes(strings: &[String]) -> *const c_char {
79    let json_string = serde_json::to_string(strings).expect("Failed to serialize strings to JSON");
80    let c_string = CString::new(json_string).expect("JSON string contains interior null bytes");
81
82    c_string.into_raw()
83}
84
85/// Convert a C bytes pointer into an owned `Option<HashMap<String, Value>>`.
86///
87/// # Safety
88///
89/// Assumes `ptr` is a valid C string pointer.
90///
91/// # Panics
92///
93/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
94#[must_use]
95pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option<HashMap<String, Value>> {
96    if ptr.is_null() {
97        None
98    } else {
99        let c_str = unsafe { CStr::from_ptr(ptr) };
100        let bytes = c_str.to_bytes();
101
102        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
103        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
104
105        Some(result)
106    }
107}
108
109/// Convert a C bytes pointer into an owned `Option<HashMap<Ustr, Ustr>>`.
110///
111/// # Safety
112///
113/// Assumes `ptr` is a valid C string pointer.
114///
115/// # Panics
116///
117/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
118#[must_use]
119pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option<HashMap<Ustr, Ustr>> {
120    if ptr.is_null() {
121        None
122    } else {
123        let c_str = unsafe { CStr::from_ptr(ptr) };
124        let bytes = c_str.to_bytes();
125
126        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
127        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
128
129        Some(result)
130    }
131}
132
133/// Convert a C bytes pointer into an owned `Option<Vec<String>>`.
134///
135/// # Safety
136///
137/// Assumes `ptr` is a valid C string pointer.
138///
139/// # Panics
140///
141/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
142#[must_use]
143pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option<Vec<String>> {
144    if ptr.is_null() {
145        None
146    } else {
147        let c_str = unsafe { CStr::from_ptr(ptr) };
148        let bytes = c_str.to_bytes();
149
150        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
151        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
152
153        Some(result)
154    }
155}
156
157/// Return the decimal precision inferred from the given C string.
158///
159/// # Safety
160///
161/// Assumes `ptr` is a valid C string pointer.
162///
163/// # Panics
164///
165/// Panics if `ptr` is null.
166#[unsafe(no_mangle)]
167pub unsafe extern "C" fn precision_from_cstr(ptr: *const c_char) -> u8 {
168    assert!(!ptr.is_null(), "`ptr` was NULL");
169    let s = unsafe { cstr_as_str(ptr) };
170    precision_from_str(s)
171}
172
173/// Return the minimum price increment decimal precision inferred from the given C string.
174///
175/// # Safety
176///
177/// Assumes `ptr` is a valid C string pointer.
178///
179/// # Panics
180///
181/// Panics if `ptr` is null.
182#[unsafe(no_mangle)]
183pub unsafe extern "C" fn min_increment_precision_from_cstr(ptr: *const c_char) -> u8 {
184    assert!(!ptr.is_null(), "`ptr` was NULL");
185    let s = unsafe { cstr_as_str(ptr) };
186    min_increment_precision_from_str(s)
187}
188
189/// Return a `bool` value from the given `u8`.
190#[must_use]
191pub const fn u8_as_bool(value: u8) -> bool {
192    value != 0
193}
194
195////////////////////////////////////////////////////////////////////////////////
196// Tests
197////////////////////////////////////////////////////////////////////////////////
198#[cfg(test)]
199mod tests {
200    use std::ffi::CString;
201
202    use rstest::rstest;
203
204    use super::*;
205
206    #[rstest]
207    fn test_optional_bytes_to_json_null() {
208        let ptr = std::ptr::null();
209        let result = unsafe { optional_bytes_to_json(ptr) };
210        assert_eq!(result, None);
211    }
212
213    #[rstest]
214    fn test_optional_bytes_to_json_empty() {
215        let json_str = CString::new("{}").unwrap();
216        let ptr = json_str.as_ptr().cast::<c_char>();
217        let result = unsafe { optional_bytes_to_json(ptr) };
218        assert_eq!(result, Some(HashMap::new()));
219    }
220
221    #[rstest]
222    fn test_string_vec_to_bytes_valid() {
223        let strings = vec!["value1", "value2", "value3"]
224            .into_iter()
225            .map(String::from)
226            .collect::<Vec<String>>();
227
228        let ptr = string_vec_to_bytes(&strings);
229
230        let result = unsafe { bytes_to_string_vec(ptr) };
231        assert_eq!(result, strings);
232    }
233
234    #[rstest]
235    fn test_string_vec_to_bytes_empty() {
236        let strings = Vec::new();
237        let ptr = string_vec_to_bytes(&strings);
238
239        let result = unsafe { bytes_to_string_vec(ptr) };
240        assert_eq!(result, strings);
241    }
242
243    #[rstest]
244    fn test_bytes_to_string_vec_valid() {
245        let json_str = CString::new(r#"["value1", "value2", "value3"]"#).unwrap();
246        let ptr = json_str.as_ptr().cast::<c_char>();
247        let result = unsafe { bytes_to_string_vec(ptr) };
248
249        let expected_vec = vec!["value1", "value2", "value3"]
250            .into_iter()
251            .map(String::from)
252            .collect::<Vec<String>>();
253
254        assert_eq!(result, expected_vec);
255    }
256
257    #[rstest]
258    fn test_bytes_to_string_vec_invalid() {
259        let json_str = CString::new(r#"["value1", 42, "value3"]"#).unwrap();
260        let ptr = json_str.as_ptr().cast::<c_char>();
261        let result = unsafe { bytes_to_string_vec(ptr) };
262
263        let expected_vec = vec!["value1", "value3"]
264            .into_iter()
265            .map(String::from)
266            .collect::<Vec<String>>();
267
268        assert_eq!(result, expected_vec);
269    }
270
271    #[rstest]
272    fn test_optional_bytes_to_json_valid() {
273        let json_str = CString::new(r#"{"key1": "value1", "key2": 2}"#).unwrap();
274        let ptr = json_str.as_ptr().cast::<c_char>();
275        let result = unsafe { optional_bytes_to_json(ptr) };
276        let mut expected_map = HashMap::new();
277        expected_map.insert("key1".to_owned(), Value::String("value1".to_owned()));
278        expected_map.insert(
279            "key2".to_owned(),
280            Value::Number(serde_json::Number::from(2)),
281        );
282        assert_eq!(result, Some(expected_map));
283    }
284
285    #[rstest]
286    #[should_panic(expected = "C string contains invalid JSON")]
287    fn test_optional_bytes_to_json_invalid() {
288        let json_str = CString::new(r#"{"key1": "value1", "key2": }"#).unwrap();
289        let ptr = json_str.as_ptr().cast::<c_char>();
290        let _result = unsafe { optional_bytes_to_json(ptr) };
291    }
292
293    #[rstest]
294    #[case("1e8", 0)]
295    #[case("123", 0)]
296    #[case("123.45", 2)]
297    #[case("123.456789", 6)]
298    #[case("1.23456789e-2", 2)]
299    #[case("1.23456789e-12", 12)]
300    fn test_precision_from_cstr(#[case] input: &str, #[case] expected: u8) {
301        let c_str = CString::new(input).unwrap();
302        assert_eq!(unsafe { precision_from_cstr(c_str.as_ptr()) }, expected);
303    }
304}