nautilus_core/ffi/
string.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    ffi::{CStr, CString, c_char},
18    str,
19};
20
21#[cfg(feature = "python")]
22use pyo3::{Bound, Python, ffi};
23use ustr::Ustr;
24
25#[cfg(feature = "python")]
26/// Returns an owned string from a valid Python object pointer.
27///
28/// # Safety
29///
30/// Assumes `ptr` is borrowed from a valid Python UTF-8 `str`.
31///
32/// # Panics
33///
34/// Panics if `ptr` is null.
35#[must_use]
36pub unsafe fn pystr_to_string(ptr: *mut ffi::PyObject) -> String {
37    assert!(!ptr.is_null(), "`ptr` was NULL");
38    Python::with_gil(|py| unsafe { Bound::from_borrowed_ptr(py, ptr).to_string() })
39}
40
41/// Convert a C string pointer into an owned `String`.
42///
43/// # Safety
44///
45/// Assumes `ptr` is a valid C string pointer.
46///
47/// # Panics
48///
49/// Panics if `ptr` is null.
50#[must_use]
51pub unsafe fn cstr_to_ustr(ptr: *const c_char) -> Ustr {
52    assert!(!ptr.is_null(), "`ptr` was NULL");
53    let cstr = unsafe { CStr::from_ptr(ptr) };
54    Ustr::from(cstr.to_str().expect("CStr::from_ptr failed"))
55}
56
57/// Convert a C string pointer into bytes.
58///
59/// # Safety
60///
61/// - Assumes `ptr` is a valid C string pointer.
62/// - The returned slice is only valid while the original C string remains allocated.
63/// - Caller must ensure the C string outlives any usage of the returned slice.
64///
65/// # Panics
66///
67/// Panics if `ptr` is null.
68///
69/// # Note
70///
71/// This function is designed for immediate consumption within FFI calls.
72/// Do not store the returned slice for use beyond the current function scope.
73#[must_use]
74pub unsafe fn cstr_to_bytes(ptr: *const c_char) -> &'static [u8] {
75    assert!(!ptr.is_null(), "`ptr` was NULL");
76    let cstr = unsafe { CStr::from_ptr(ptr) };
77    cstr.to_bytes()
78}
79
80/// Convert a C string pointer into an owned `Option<Ustr>`.
81///
82/// # Safety
83///
84/// Assumes `ptr` is a valid C string pointer or NULL.
85///
86/// # Panics
87///
88/// Panics if `ptr` is null.
89#[must_use]
90pub unsafe fn optional_cstr_to_ustr(ptr: *const c_char) -> Option<Ustr> {
91    if ptr.is_null() {
92        None
93    } else {
94        Some(unsafe { cstr_to_ustr(ptr) })
95    }
96}
97
98/// Convert a C string pointer into a string slice.
99///
100/// # Safety
101///
102/// - Assumes `ptr` is a valid C string pointer.
103/// - The returned slice is only valid while the original C string remains allocated.
104/// - Caller must ensure the C string outlives any usage of the returned slice.
105///
106/// # Panics
107///
108/// Panics if `ptr` is null or contains invalid UTF-8.
109///
110/// # Note
111///
112/// This function is designed for immediate consumption within FFI calls.
113/// Do not store the returned slice for use beyond the current function scope.
114#[must_use]
115pub unsafe fn cstr_as_str(ptr: *const c_char) -> &'static str {
116    assert!(!ptr.is_null(), "`ptr` was NULL");
117    let cstr = unsafe { CStr::from_ptr(ptr) };
118    cstr.to_str().expect("CStr::from_ptr failed")
119}
120
121/// Convert a C string pointer into an `Option<&str>`.
122///
123/// # Safety
124///
125/// - Assumes `ptr` is a valid C string pointer or NULL.
126/// - The returned slice is only valid while the original C string remains allocated.
127/// - Caller must ensure the C string outlives any usage of the returned slice.
128///
129/// # Panics
130///
131/// Panics if `ptr` is not null but contains invalid UTF-8.
132///
133/// # Note
134///
135/// This function is designed for immediate consumption within FFI calls.
136/// Do not store the returned slice for use beyond the current function scope.
137#[must_use]
138pub unsafe fn optional_cstr_to_str(ptr: *const c_char) -> Option<&'static str> {
139    if ptr.is_null() {
140        None
141    } else {
142        Some(unsafe { cstr_as_str(ptr) })
143    }
144}
145
146/// Create a C string pointer to newly allocated memory from a [`&str`].
147///
148/// # Panics
149///
150/// Panics if the input string contains interior null bytes.
151#[must_use]
152pub fn str_to_cstr(s: &str) -> *const c_char {
153    CString::new(s).expect("CString::new failed").into_raw()
154}
155
156/// Drops the C string memory at the pointer.
157///
158/// # Safety
159///
160/// Assumes `ptr` is a valid C string pointer.
161///
162/// # Panics
163///
164/// Panics if `ptr` is null.
165#[unsafe(no_mangle)]
166pub unsafe extern "C" fn cstr_drop(ptr: *const c_char) {
167    assert!(!ptr.is_null(), "`ptr` was NULL");
168    let cstring = unsafe { CString::from_raw(ptr.cast_mut()) };
169    drop(cstring);
170}
171
172////////////////////////////////////////////////////////////////////////////////
173// Tests
174////////////////////////////////////////////////////////////////////////////////
175#[cfg(test)]
176mod tests {
177    #[cfg(feature = "python")]
178    use pyo3::types::PyString;
179    use rstest::*;
180
181    use super::*;
182
183    #[cfg(feature = "python")]
184    #[cfg_attr(miri, ignore)]
185    #[rstest]
186    fn test_pystr_to_string() {
187        pyo3::prepare_freethreaded_python();
188        // Create a valid Python object pointer
189        let ptr = Python::with_gil(|py| PyString::new(py, "test string1").as_ptr());
190        let result = unsafe { pystr_to_string(ptr) };
191        assert_eq!(result, "test string1");
192    }
193
194    #[cfg(feature = "python")]
195    #[rstest]
196    #[should_panic(expected = "`ptr` was NULL")]
197    fn test_pystr_to_string_with_null_ptr() {
198        // Create a null Python object pointer
199        let ptr: *mut ffi::PyObject = std::ptr::null_mut();
200        unsafe {
201            let _ = pystr_to_string(ptr);
202        };
203    }
204
205    #[rstest]
206    fn test_cstr_to_str() {
207        // Create a valid C string pointer
208        let c_string = CString::new("test string2").expect("CString::new failed");
209        let ptr = c_string.as_ptr();
210        let result = unsafe { cstr_as_str(ptr) };
211        assert_eq!(result, "test string2");
212    }
213
214    #[rstest]
215    fn test_cstr_to_vec() {
216        // Create a valid C string pointer
217        let sample_c_string = CString::new("Hello, world!").expect("CString::new failed");
218        let cstr_ptr = sample_c_string.as_ptr();
219        let result = unsafe { cstr_to_bytes(cstr_ptr) };
220        assert_eq!(result, b"Hello, world!");
221        assert_eq!(result.len(), 13);
222    }
223
224    #[rstest]
225    #[should_panic(expected = "`ptr` was NULL")]
226    fn test_cstr_to_vec_with_null_ptr() {
227        // Create a null C string pointer
228        let ptr: *const c_char = std::ptr::null();
229        unsafe {
230            let _ = cstr_to_bytes(ptr);
231        };
232    }
233
234    #[rstest]
235    fn test_optional_cstr_to_str_with_null_ptr() {
236        // Call optional_cstr_to_str with null pointer
237        let ptr = std::ptr::null();
238        let result = unsafe { optional_cstr_to_str(ptr) };
239        assert!(result.is_none());
240    }
241
242    #[rstest]
243    fn test_optional_cstr_to_str_with_valid_ptr() {
244        // Create a valid C string
245        let input_str = "hello world";
246        let c_str = CString::new(input_str).expect("CString::new failed");
247        let result = unsafe { optional_cstr_to_str(c_str.as_ptr()) };
248        assert!(result.is_some());
249        assert_eq!(result.unwrap(), input_str);
250    }
251
252    #[rstest]
253    fn test_string_to_cstr() {
254        let s = "test string";
255        let c_str_ptr = str_to_cstr(s);
256        let c_str = unsafe { CStr::from_ptr(c_str_ptr) };
257        let result = c_str.to_str().expect("CStr::from_ptr failed");
258        assert_eq!(result, s);
259    }
260
261    #[rstest]
262    fn test_cstr_drop() {
263        let c_string = CString::new("test string3").expect("CString::new failed");
264        let ptr = c_string.into_raw(); // <-- pointer _must_ be obtained this way
265        unsafe { cstr_drop(ptr) };
266    }
267}