Skip to main content

nautilus_core/ffi/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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 NautilusTrader.
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::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/// Convert a C bytes pointer into an owned `Vec<String>`.
42///
43/// # Safety
44///
45/// Assumes `ptr` is a valid C string pointer.
46///
47/// # Panics
48///
49/// Panics if `ptr` is null, contains invalid UTF-8/JSON, or the JSON value
50/// is not an array of strings.
51#[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    // SAFETY: Caller guarantees ptr is valid per function contract
56    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/// Convert a slice of `String` into a C string pointer (JSON encoded).
78///
79/// # Panics
80///
81/// Panics if JSON serialization fails or if the generated string contains interior null bytes.
82#[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/// Convert a C bytes pointer into an owned `Option<HashMap<String, Value>>`.
91///
92/// # Safety
93///
94/// Assumes `ptr` is a valid C string pointer.
95///
96/// # Panics
97///
98/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
99#[must_use]
100pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option<HashMap<String, Value>> {
101    // SAFETY: A non-null pointer is valid under the caller's contract
102    unsafe { optional_json_from_cstr(ptr) }
103}
104
105/// Convert a C bytes pointer into an owned `Option<HashMap<Ustr, Ustr>>`.
106///
107/// # Safety
108///
109/// Assumes `ptr` is a valid C string pointer.
110///
111/// # Panics
112///
113/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
114#[must_use]
115pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option<HashMap<Ustr, Ustr>> {
116    // SAFETY: A non-null pointer is valid under the caller's contract
117    unsafe { optional_json_from_cstr(ptr) }
118}
119
120/// Convert a C bytes pointer into an owned `Option<Vec<String>>`.
121///
122/// # Safety
123///
124/// Assumes `ptr` is a valid C string pointer.
125///
126/// # Panics
127///
128/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
129#[must_use]
130pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option<Vec<String>> {
131    // SAFETY: A non-null pointer is valid under the caller's contract
132    unsafe { optional_json_from_cstr(ptr) }
133}
134
135/// # Safety
136///
137/// If `ptr` is non-null, it must reference a valid, null-terminated UTF-8 C string that remains
138/// unchanged for the duration of this call.
139unsafe 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    // SAFETY: A non-null pointer is valid under the caller's contract
148    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/// Return the decimal precision inferred from the given C string.
154///
155/// # Safety
156///
157/// Assumes `ptr` is a valid C string pointer.
158///
159/// # Panics
160///
161/// Panics if `ptr` is null.
162#[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        // SAFETY: Caller guarantees ptr is valid per function contract
167        let s = unsafe { cstr_as_str(ptr) };
168        precision_from_v1_str(s)
169    })
170}
171
172/// Return the minimum price increment decimal precision inferred from the given C string.
173///
174/// # Safety
175///
176/// Assumes `ptr` is a valid C string pointer.
177///
178/// # Panics
179///
180/// Panics if `ptr` is null.
181#[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        // SAFETY: Caller guarantees ptr is valid per function contract
186        let s = unsafe { cstr_as_str(ptr) };
187        min_increment_precision_from_str(s)
188    })
189}
190
191// TODO: Remove this temporary parser when v1 drops its legacy source-text precision contract
192fn 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/// Return a `bool` value from the given `u8`.
223#[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}