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
16use std::{
17    collections::HashMap,
18    ffi::{CStr, CString, c_char},
19};
20
21use serde_json::Value;
22use ustr::Ustr;
23
24use crate::{
25    ffi::string::cstr_as_str,
26    parsing::{min_increment_precision_from_str, precision_from_str},
27};
28
29/// Convert a C bytes pointer into an owned `Vec<String>`.
30///
31/// # Safety
32///
33/// Assumes `ptr` is a valid C string pointer.
34///
35/// # Panics
36///
37/// Panics if `ptr` is null, contains invalid UTF-8, or is invalid JSON.
38#[must_use]
39pub unsafe fn bytes_to_string_vec(ptr: *const c_char) -> Vec<String> {
40    assert!(!ptr.is_null(), "`ptr` was NULL");
41
42    let c_str = unsafe { CStr::from_ptr(ptr) };
43    let bytes = c_str.to_bytes();
44
45    let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
46    let value: serde_json::Value =
47        serde_json::from_str(json_string).expect("C string contains invalid JSON");
48
49    match value {
50        serde_json::Value::Array(arr) => arr
51            .into_iter()
52            .filter_map(|value| match value {
53                serde_json::Value::String(string_value) => Some(string_value),
54                _ => None,
55            })
56            .collect(),
57        _ => Vec::new(),
58    }
59}
60
61/// Convert a slice of `String` into a C string pointer (JSON encoded).
62///
63/// # Panics
64///
65/// Panics if JSON serialization fails or if the generated string contains interior null bytes.
66#[must_use]
67pub fn string_vec_to_bytes(strings: &[String]) -> *const c_char {
68    let json_string = serde_json::to_string(strings).expect("Failed to serialize strings to JSON");
69    let c_string = CString::new(json_string).expect("JSON string contains interior null bytes");
70
71    c_string.into_raw()
72}
73
74/// Convert a C bytes pointer into an owned `Option<HashMap<String, Value>>`.
75///
76/// # Safety
77///
78/// Assumes `ptr` is a valid C string pointer.
79///
80/// # Panics
81///
82/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
83#[must_use]
84pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option<HashMap<String, Value>> {
85    if ptr.is_null() {
86        None
87    } else {
88        let c_str = unsafe { CStr::from_ptr(ptr) };
89        let bytes = c_str.to_bytes();
90
91        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
92        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
93
94        Some(result)
95    }
96}
97
98/// Convert a C bytes pointer into an owned `Option<HashMap<Ustr, Ustr>>`.
99///
100/// # Safety
101///
102/// Assumes `ptr` is a valid C string pointer.
103///
104/// # Panics
105///
106/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
107#[must_use]
108pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option<HashMap<Ustr, Ustr>> {
109    if ptr.is_null() {
110        None
111    } else {
112        let c_str = unsafe { CStr::from_ptr(ptr) };
113        let bytes = c_str.to_bytes();
114
115        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
116        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
117
118        Some(result)
119    }
120}
121
122/// Convert a C bytes pointer into an owned `Option<Vec<String>>`.
123///
124/// # Safety
125///
126/// Assumes `ptr` is a valid C string pointer.
127///
128/// # Panics
129///
130/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
131#[must_use]
132pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option<Vec<String>> {
133    if ptr.is_null() {
134        None
135    } else {
136        let c_str = unsafe { CStr::from_ptr(ptr) };
137        let bytes = c_str.to_bytes();
138
139        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
140        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
141
142        Some(result)
143    }
144}
145
146/// Return the decimal precision inferred from the given C string.
147///
148/// # Safety
149///
150/// Assumes `ptr` is a valid C string pointer.
151///
152/// # Panics
153///
154/// Panics if `ptr` is null.
155#[unsafe(no_mangle)]
156pub unsafe extern "C" fn precision_from_cstr(ptr: *const c_char) -> u8 {
157    assert!(!ptr.is_null(), "`ptr` was NULL");
158    let s = unsafe { cstr_as_str(ptr) };
159    precision_from_str(s)
160}
161
162/// Return the minimum price increment decimal precision inferred from the given C string.
163///
164/// # Safety
165///
166/// Assumes `ptr` is a valid C string pointer.
167///
168/// # Panics
169///
170/// Panics if `ptr` is null.
171#[unsafe(no_mangle)]
172pub unsafe extern "C" fn min_increment_precision_from_cstr(ptr: *const c_char) -> u8 {
173    assert!(!ptr.is_null(), "`ptr` was NULL");
174    let s = unsafe { cstr_as_str(ptr) };
175    min_increment_precision_from_str(s)
176}
177
178/// Return a `bool` value from the given `u8`.
179#[must_use]
180pub const fn u8_as_bool(value: u8) -> bool {
181    value != 0
182}
183
184////////////////////////////////////////////////////////////////////////////////
185// Tests
186////////////////////////////////////////////////////////////////////////////////
187#[cfg(test)]
188mod tests {
189    use std::ffi::CString;
190
191    use rstest::rstest;
192
193    use super::*;
194
195    #[rstest]
196    fn test_optional_bytes_to_json_null() {
197        let ptr = std::ptr::null();
198        let result = unsafe { optional_bytes_to_json(ptr) };
199        assert_eq!(result, None);
200    }
201
202    #[rstest]
203    fn test_optional_bytes_to_json_empty() {
204        let json_str = CString::new("{}").unwrap();
205        let ptr = json_str.as_ptr().cast::<c_char>();
206        let result = unsafe { optional_bytes_to_json(ptr) };
207        assert_eq!(result, Some(HashMap::new()));
208    }
209
210    #[rstest]
211    fn test_string_vec_to_bytes_valid() {
212        let strings = vec!["value1", "value2", "value3"]
213            .into_iter()
214            .map(String::from)
215            .collect::<Vec<String>>();
216
217        let ptr = string_vec_to_bytes(&strings);
218
219        let result = unsafe { bytes_to_string_vec(ptr) };
220        assert_eq!(result, strings);
221    }
222
223    #[rstest]
224    fn test_string_vec_to_bytes_empty() {
225        let strings = Vec::new();
226        let ptr = string_vec_to_bytes(&strings);
227
228        let result = unsafe { bytes_to_string_vec(ptr) };
229        assert_eq!(result, strings);
230    }
231
232    #[rstest]
233    fn test_bytes_to_string_vec_valid() {
234        let json_str = CString::new(r#"["value1", "value2", "value3"]"#).unwrap();
235        let ptr = json_str.as_ptr().cast::<c_char>();
236        let result = unsafe { bytes_to_string_vec(ptr) };
237
238        let expected_vec = vec!["value1", "value2", "value3"]
239            .into_iter()
240            .map(String::from)
241            .collect::<Vec<String>>();
242
243        assert_eq!(result, expected_vec);
244    }
245
246    #[rstest]
247    fn test_bytes_to_string_vec_invalid() {
248        let json_str = CString::new(r#"["value1", 42, "value3"]"#).unwrap();
249        let ptr = json_str.as_ptr().cast::<c_char>();
250        let result = unsafe { bytes_to_string_vec(ptr) };
251
252        let expected_vec = vec!["value1", "value3"]
253            .into_iter()
254            .map(String::from)
255            .collect::<Vec<String>>();
256
257        assert_eq!(result, expected_vec);
258    }
259
260    #[rstest]
261    fn test_optional_bytes_to_json_valid() {
262        let json_str = CString::new(r#"{"key1": "value1", "key2": 2}"#).unwrap();
263        let ptr = json_str.as_ptr().cast::<c_char>();
264        let result = unsafe { optional_bytes_to_json(ptr) };
265        let mut expected_map = HashMap::new();
266        expected_map.insert("key1".to_owned(), Value::String("value1".to_owned()));
267        expected_map.insert(
268            "key2".to_owned(),
269            Value::Number(serde_json::Number::from(2)),
270        );
271        assert_eq!(result, Some(expected_map));
272    }
273
274    #[rstest]
275    #[should_panic(expected = "C string contains invalid JSON")]
276    fn test_optional_bytes_to_json_invalid() {
277        let json_str = CString::new(r#"{"key1": "value1", "key2": }"#).unwrap();
278        let ptr = json_str.as_ptr().cast::<c_char>();
279        let _result = unsafe { optional_bytes_to_json(ptr) };
280    }
281
282    #[rstest]
283    #[case("1e8", 0)]
284    #[case("123", 0)]
285    #[case("123.45", 2)]
286    #[case("123.456789", 6)]
287    #[case("1.23456789e-2", 2)]
288    #[case("1.23456789e-12", 12)]
289    fn test_precision_from_cstr(#[case] input: &str, #[case] expected: u8) {
290        let c_str = CString::new(input).unwrap();
291        assert_eq!(unsafe { precision_from_cstr(c_str.as_ptr()) }, expected);
292    }
293}