Skip to main content

cpdb_sys/
util.rs

1//! Small FFI utilities shared by the higher-level modules.
2
3use super::bindings as ffi;
4use crate::error::{CpdbError, Result};
5use libc::c_char;
6use std::ffi::{CStr, CString};
7
8/// Converts a borrowed C string into an owned `String`.
9///
10/// Returns [`CpdbError::NullPointer`] when `ptr` is null. Invalid UTF-8 is
11/// replaced with U+FFFD (lossy) — cpdb-libs strings should always be valid
12/// UTF-8, but we do not trust that strictly.
13///
14/// # Safety
15/// `ptr` must either be null or point at a NUL-terminated C string that
16/// stays valid for the duration of this call.
17pub unsafe fn cstr_to_string(ptr: *const c_char) -> Result<String> {
18    if ptr.is_null() {
19        return Err(CpdbError::NullPointer);
20    }
21    // SAFETY: caller guarantees `ptr` references a live NUL-terminated string.
22    Ok(unsafe { CStr::from_ptr(ptr) }
23        .to_string_lossy()
24        .into_owned())
25}
26
27/// Same as [`cstr_to_string`] but frees the underlying buffer with `g_free`.
28///
29/// Use this for return values from cpdb-libs functions that `g_strdup` their
30/// result (`cpdbGetDefault`, `cpdbGetCurrent`, `cpdbPrintFile`, ...).
31///
32/// # Safety
33/// `ptr` must be null, or a pointer returned by a GLib allocator that the
34/// caller has ownership of (so freeing it is correct).
35pub unsafe fn cstr_to_string_and_g_free(ptr: *mut c_char) -> Result<String> {
36    if ptr.is_null() {
37        return Err(CpdbError::NullPointer);
38    }
39    // SAFETY: caller guarantees ownership of a GLib-allocated string.
40    let owned = unsafe { CStr::from_ptr(ptr) }
41        .to_string_lossy()
42        .into_owned();
43    unsafe { glib_sys::g_free(ptr as glib_sys::gpointer) };
44    Ok(owned)
45}
46
47/// A pinned, owned array of `cpdb_option_t` with backing `CString` storage.
48///
49/// The strings cannot be reallocated after construction, so raw pointers
50/// embedded in the `cpdb_option_t` entries stay valid for the lifetime of
51/// the `COptions`. Allocation in `to_c_options` therefore cannot invalidate
52/// any pointer captured here.
53pub struct COptions {
54    // Boxed slice: the storage is fixed-length and never grows, so pointer
55    // capture is sound for the whole lifetime of the struct.
56    _strings: Box<[CString]>,
57    options: Box<[ffi::cpdb_option_t]>,
58}
59
60impl COptions {
61    /// Returns a raw mutable pointer to the underlying option array,
62    /// suitable for passing to cpdb-libs functions that expect a
63    /// `cpdb_option_t *` plus a length.
64    pub fn as_mut_ptr(&mut self) -> *mut ffi::cpdb_option_t {
65        self.options.as_mut_ptr()
66    }
67
68    /// Number of options held.
69    pub fn len(&self) -> usize {
70        self.options.len()
71    }
72
73    /// `true` when no options are stored.
74    pub fn is_empty(&self) -> bool {
75        self.options.is_empty()
76    }
77}
78
79/// Converts a slice of `(name, value)` pairs into a [`COptions`] suitable
80/// for passing to cpdb-libs.
81///
82/// The returned [`COptions`] owns its backing `CString` storage, so the
83/// raw pointers embedded in each `cpdb_option_t` remain valid for the
84/// `COptions` lifetime.
85pub fn to_c_options(pairs: &[(&str, &str)]) -> Result<COptions> {
86    let mut strings: Vec<CString> = Vec::with_capacity(pairs.len() * 2);
87    for (key, value) in pairs {
88        strings.push(CString::new(*key)?);
89        strings.push(CString::new(*value)?);
90    }
91    let strings: Box<[CString]> = strings.into_boxed_slice();
92
93    let mut options: Vec<ffi::cpdb_option_t> = Vec::with_capacity(pairs.len());
94    for i in 0..pairs.len() {
95        let key_ptr = strings[i * 2].as_ptr() as *mut c_char;
96        let val_ptr = strings[i * 2 + 1].as_ptr() as *mut c_char;
97        options.push(ffi::cpdb_option_t {
98            option_name: key_ptr,
99            default_value: val_ptr,
100            group_name: std::ptr::null_mut(),
101            num_supported: 0,
102            supported_values: std::ptr::null_mut(),
103        });
104    }
105
106    Ok(COptions {
107        _strings: strings,
108        options: options.into_boxed_slice(),
109    })
110}
111
112#[cfg(test)]
113mod tests {
114    //! Pure-Rust unit tests. These do not touch cpdb-libs and are safe
115    //! to run under `cargo miri test`.
116
117    use super::*;
118
119    /// Reads back the strings embedded in a `COptions` via the same raw
120    /// pointer machinery cpdb-libs uses on the C side. Validates that
121    /// `to_c_options` produces a layout consistent with the C ABI.
122    fn read_back(opts: &COptions) -> Vec<(String, String)> {
123        let mut out = Vec::with_capacity(opts.len());
124        for i in 0..opts.len() {
125            // SAFETY: pointer arithmetic inside the boxed slice; pointers
126            // captured at construction remain valid for the slice's
127            // lifetime.
128            let entry = unsafe { &*opts.options.as_ptr().add(i) };
129            let name = unsafe { CStr::from_ptr(entry.option_name) }
130                .to_string_lossy()
131                .into_owned();
132            let value = unsafe { CStr::from_ptr(entry.default_value) }
133                .to_string_lossy()
134                .into_owned();
135            out.push((name, value));
136        }
137        out
138    }
139
140    #[test]
141    fn empty_input_yields_empty_options() {
142        let opts = to_c_options(&[]).unwrap();
143        assert!(opts.is_empty());
144        assert_eq!(opts.len(), 0);
145        assert!(read_back(&opts).is_empty());
146    }
147
148    #[test]
149    fn single_pair_round_trips() {
150        let opts = to_c_options(&[("copies", "2")]).unwrap();
151        assert_eq!(opts.len(), 1);
152        let echoed = read_back(&opts);
153        assert_eq!(echoed, vec![("copies".to_string(), "2".to_string())]);
154    }
155
156    #[test]
157    fn multiple_pairs_round_trip_in_order() {
158        let input = &[
159            ("copies", "3"),
160            ("sides", "two-sided-long-edge"),
161            ("orientation-requested", "portrait"),
162            ("media", "iso_a4_210x297mm"),
163        ];
164        let opts = to_c_options(input).unwrap();
165        assert_eq!(opts.len(), input.len());
166
167        let echoed = read_back(&opts);
168        let expected: Vec<(String, String)> = input
169            .iter()
170            .map(|(k, v)| (k.to_string(), v.to_string()))
171            .collect();
172        assert_eq!(echoed, expected);
173    }
174
175    #[test]
176    fn interior_nul_in_key_is_rejected() {
177        let result = to_c_options(&[("co\0pies", "1")]);
178        assert!(matches!(result, Err(CpdbError::NulError(_))));
179    }
180
181    #[test]
182    fn interior_nul_in_value_is_rejected() {
183        let result = to_c_options(&[("copies", "1\0")]);
184        assert!(matches!(result, Err(CpdbError::NulError(_))));
185    }
186
187    #[test]
188    fn group_and_supported_fields_are_null_initialised() {
189        let opts = to_c_options(&[("a", "b")]).unwrap();
190        // SAFETY: opts.len() == 1, so index 0 is a valid entry.
191        let entry = unsafe { &*opts.options.as_ptr() };
192        assert!(entry.group_name.is_null());
193        assert!(entry.supported_values.is_null());
194        assert_eq!(entry.num_supported, 0);
195    }
196
197    #[test]
198    fn pointers_stay_valid_across_move() {
199        // Constructing then moving the COptions must not invalidate the
200        // raw pointers embedded in its `options` slice. Boxed slices have
201        // stable addresses; this test pins that invariant.
202        let opts = to_c_options(&[("k", "v")]).unwrap();
203        let moved = opts;
204        let echoed = read_back(&moved);
205        assert_eq!(echoed, vec![("k".to_string(), "v".to_string())]);
206    }
207}