nautilus_core/ffi/
string.rs

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