nautilus_core/ffi/
string.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//! Utilities for safely moving UTF-8 strings across the FFI boundary.
17//!
18//! Interoperability between Rust and C/C++/Python often requires raw pointers to *null terminated*
19//! strings.  This module provides convenience helpers that:
20//!
21//! * Convert raw `*const c_char` pointers to Rust [`String`], [`&str`], byte slices, or
22//!   `ustr::Ustr` values.
23//! * Perform the inverse conversion when Rust needs to hand ownership of a string to foreign
24//!   code.
25//!
26//! The majority of these functions are marked `unsafe` because they accept raw pointers and rely
27//! on the caller to uphold basic invariants (pointer validity, lifetime, UTF-8 correctness).  Each
28//! function documents the specific safety requirements.
29
30use std::{
31    ffi::{CStr, CString, c_char},
32    str,
33};
34
35#[cfg(feature = "python")]
36use pyo3::{Bound, Python, ffi};
37use ustr::Ustr;
38
39use crate::ffi::abort_on_panic;
40
41#[cfg(feature = "python")]
42/// Returns an owned string from a valid Python object pointer.
43///
44/// # Safety
45///
46/// Assumes `ptr` is borrowed from a valid Python UTF-8 `str`.
47///
48/// # Panics
49///
50/// Panics if `ptr` is null.
51#[must_use]
52pub unsafe fn pystr_to_string(ptr: *mut ffi::PyObject) -> String {
53    assert!(!ptr.is_null(), "`ptr` was NULL");
54    // SAFETY: Caller guarantees ptr is borrowed from a valid Python UTF-8 str
55    Python::attach(|py| unsafe { Bound::from_borrowed_ptr(py, ptr).to_string() })
56}
57
58/// Convert a C string pointer into an owned `String`.
59///
60/// # Safety
61///
62/// Assumes `ptr` is a valid C string pointer.
63///
64/// # Panics
65///
66/// Panics if `ptr` is null.
67#[must_use]
68pub unsafe fn cstr_to_ustr(ptr: *const c_char) -> Ustr {
69    assert!(!ptr.is_null(), "`ptr` was NULL");
70    // SAFETY: Caller guarantees ptr is valid per function contract
71    let cstr = unsafe { CStr::from_ptr(ptr) };
72    Ustr::from(cstr.to_str().expect("CStr::from_ptr failed"))
73}
74
75/// Convert a C string pointer into a borrowed byte slice.
76///
77/// # Safety
78///
79/// - Assumes `ptr` is a valid, null-terminated UTF-8 C string pointer.
80/// - The returned slice borrows the underlying allocation; callers must ensure the
81///   C buffer outlives every use of the slice.
82///
83/// # Panics
84///
85/// Panics if `ptr` is null.
86#[must_use]
87pub unsafe fn cstr_to_bytes<'a>(ptr: *const c_char) -> &'a [u8] {
88    assert!(!ptr.is_null(), "`ptr` was NULL");
89    // SAFETY: Caller guarantees ptr is valid per function contract
90    let cstr = unsafe { CStr::from_ptr(ptr) };
91    cstr.to_bytes()
92}
93
94/// Convert a C string pointer into an owned `Option<Ustr>`.
95///
96/// # Safety
97///
98/// Assumes `ptr` is a valid C string pointer or NULL.
99///
100/// # Panics
101///
102/// Panics if `ptr` is not null but not a valid UTF-8 C string.
103#[must_use]
104pub unsafe fn optional_cstr_to_ustr(ptr: *const c_char) -> Option<Ustr> {
105    if ptr.is_null() {
106        None
107    } else {
108        // SAFETY: Caller guarantees ptr is valid per function contract
109        Some(unsafe { cstr_to_ustr(ptr) })
110    }
111}
112
113/// Convert a C string pointer into a borrowed string slice.
114///
115/// # Safety
116///
117/// - Assumes `ptr` is a valid, null-terminated UTF-8 C string pointer.
118/// - The returned `&str` borrows the underlying allocation; callers must ensure the
119///   C buffer outlives every use of the string slice.
120///
121/// # Panics
122///
123/// Panics if `ptr` is null or contains invalid UTF-8.
124#[must_use]
125pub unsafe fn cstr_as_str<'a>(ptr: *const c_char) -> &'a str {
126    assert!(!ptr.is_null(), "`ptr` was NULL");
127    // SAFETY: Caller guarantees ptr is valid per function contract
128    let cstr = unsafe { CStr::from_ptr(ptr) };
129    cstr.to_str().expect("C string contains invalid UTF-8")
130}
131
132/// Convert an optional C string pointer into `Option<&str>`.
133///
134/// # Safety
135///
136/// - Assumes `ptr` is a valid, null-terminated UTF-8 C string pointer or NULL.
137/// - Any borrowed string must not outlive the underlying allocation.
138///
139/// # Panics
140///
141/// Panics if `ptr` is not null but contains invalid UTF-8.
142#[must_use]
143pub unsafe fn optional_cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
144    if ptr.is_null() {
145        None
146    } else {
147        // SAFETY: Caller guarantees ptr is valid per function contract
148        Some(unsafe { cstr_as_str(ptr) })
149    }
150}
151
152/// Create a C string pointer to newly allocated memory from a [`&str`].
153///
154/// # Panics
155///
156/// Panics if the input string contains interior null bytes.
157#[must_use]
158pub fn str_to_cstr(s: &str) -> *const c_char {
159    CString::new(s).expect("CString::new failed").into_raw()
160}
161
162/// Drops the C string memory at the pointer.
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 cstr_drop(ptr: *const c_char) {
173    abort_on_panic(|| {
174        assert!(!ptr.is_null(), "`ptr` was NULL");
175        // SAFETY: Caller guarantees ptr was allocated by str_to_cstr
176        let cstring = unsafe { CString::from_raw(ptr.cast_mut()) };
177        drop(cstring);
178    });
179}
180
181#[cfg(test)]
182mod tests {
183    #[cfg(feature = "python")]
184    use pyo3::types::PyString;
185    use rstest::*;
186
187    use super::*;
188
189    #[cfg(feature = "python")]
190    #[cfg_attr(miri, ignore)]
191    #[rstest]
192    fn test_pystr_to_string() {
193        Python::initialize();
194        // Create a valid Python object pointer
195        let ptr = Python::attach(|py| PyString::new(py, "test string1").as_ptr());
196        let result = unsafe { pystr_to_string(ptr) };
197        assert_eq!(result, "test string1");
198    }
199
200    #[cfg(feature = "python")]
201    #[rstest]
202    #[should_panic(expected = "`ptr` was NULL")]
203    fn test_pystr_to_string_with_null_ptr() {
204        // Create a null Python object pointer
205        let ptr: *mut ffi::PyObject = std::ptr::null_mut();
206        unsafe {
207            let _ = pystr_to_string(ptr);
208        };
209    }
210
211    #[rstest]
212    fn test_cstr_as_str() {
213        // Create a valid C string pointer
214        let c_string = CString::new("test string2").expect("CString::new failed");
215        let ptr = c_string.as_ptr();
216        let result = unsafe { cstr_as_str(ptr) };
217        assert_eq!(result, "test string2");
218    }
219
220    #[rstest]
221    fn test_cstr_to_bytes() {
222        // Create a valid C string
223        let sample_c_string = CString::new("Hello, world!").expect("CString::new failed");
224        let cstr_ptr = sample_c_string.as_ptr();
225        let result = unsafe { cstr_to_bytes(cstr_ptr) };
226        assert_eq!(result, b"Hello, world!");
227        assert_eq!(result.len(), 13);
228    }
229
230    #[rstest]
231    #[should_panic(expected = "`ptr` was NULL")]
232    fn test_cstr_to_bytes_with_null_ptr() {
233        // Create a null C string pointer
234        let ptr: *const c_char = std::ptr::null();
235        unsafe {
236            let _ = cstr_to_bytes(ptr);
237        };
238    }
239
240    #[rstest]
241    fn test_optional_cstr_to_str_with_null_ptr() {
242        // Call optional_cstr_to_str with null pointer
243        let ptr = std::ptr::null();
244        let result = unsafe { optional_cstr_to_str(ptr) };
245        assert!(result.is_none());
246    }
247
248    #[rstest]
249    fn test_optional_cstr_to_str_with_valid_ptr() {
250        // Create a valid C string
251        let input_str = "hello world";
252        let c_str = CString::new(input_str).expect("CString::new failed");
253        let result = unsafe { optional_cstr_to_str(c_str.as_ptr()) };
254        assert!(result.is_some());
255        assert_eq!(result.unwrap(), input_str);
256    }
257
258    #[rstest]
259    fn test_string_to_cstr() {
260        let s = "test string";
261        let c_str_ptr = str_to_cstr(s);
262        let c_str = unsafe { CStr::from_ptr(c_str_ptr) };
263        let result = c_str.to_str().expect("CStr::from_ptr failed");
264        assert_eq!(result, s);
265    }
266
267    #[rstest]
268    fn test_cstr_drop() {
269        let c_string = CString::new("test string3").expect("CString::new failed");
270        let ptr = c_string.into_raw(); // <-- pointer _must_ be obtained this way
271        unsafe { cstr_drop(ptr) };
272    }
273}