Skip to main content

miniextendr_api/from_r/
cow_and_paths.rs

1//! Cow, PathBuf, OsString, and string collection conversions.
2//!
3//! - `Cow<'static, [T]>` — zero-copy borrow of R native vectors
4//! - `Cow<'static, str>` — zero-copy borrow of R character scalars
5//! - `PathBuf` / `OsString` — from STRSXP via `String` intermediary
6//! - `HashSet<String>` / `BTreeSet<String>` — string set conversions
7//!
8//! # Tradeoff
9//!
10//! These [`TryFromSexp`] impls reject mismatched
11//! [`SEXPTYPE`]s — there is no looser coercion path for `Cow` / `PathBuf` /
12//! `OsString`. The `'static` lifetime on `Cow` borrows is valid only for the
13//! duration of the enclosing `.Call`; if you need an owned value that
14//! outlives R's GC, take `String` or `Vec<T>` instead (see
15//! [`strings`](crate::from_r::strings) and [`references`](crate::from_r::references)).
16
17use std::borrow::Cow;
18use std::collections::{BTreeSet, HashSet};
19use std::ffi::OsString;
20use std::path::PathBuf;
21
22use crate::SEXP;
23use crate::from_r::{
24    SexpError, SexpTypeError, TryFromSexp, charsxp_to_cow, charsxp_to_str, map_strsxp_with,
25};
26
27/// Blanket impl: Convert R vector to `Cow<'static, [T]>` where T: RNativeType.
28///
29/// Returns `Cow::Borrowed` — the slice points directly into R's SEXP data with
30/// no copy. The `'static` lifetime is valid for the duration of the `.Call`
31/// invocation (R protects the SEXP from GC while Rust code is running).
32///
33/// **Important:** Do not send the borrowed `Cow` to another thread or store it
34/// past the `.Call` return — the underlying R memory is only valid while
35/// R's protection stack guards this SEXP.
36impl<T> TryFromSexp for Cow<'static, [T]>
37where
38    T: crate::RNativeType + Copy + Clone,
39{
40    type Error = SexpTypeError;
41
42    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
43        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
44        Ok(Cow::Borrowed(slice))
45    }
46
47    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
48        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
49        Ok(Cow::Borrowed(slice))
50    }
51}
52
53/// Convert R character scalar to `Cow<'static, str>`.
54///
55/// Returns `Cow::Borrowed` — the `&str` points directly into R's CHARSXP data
56/// via `R_CHAR` + `LENGTH` (O(1), no strlen). No allocation or copy occurs.
57/// The `'static` lifetime is valid for the duration of the `.Call` invocation.
58///
59/// This delegates to the `&'static str` impl (which uses `charsxp_to_str`),
60/// giving the same zero-copy behavior. Use `Cow` when your code may need to
61/// mutate the string later — `to_mut()` will copy-on-write at that point.
62impl TryFromSexp for Cow<'static, str> {
63    type Error = SexpError;
64
65    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
66        let s: &'static str = TryFromSexp::try_from_sexp(sexp)?;
67        Ok(Cow::Borrowed(s))
68    }
69
70    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
71        let s: &'static str = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
72        Ok(Cow::Borrowed(s))
73    }
74}
75
76/// Convert R character vector to `Vec<Cow<'static, str>>` — zero-copy per element.
77///
78/// Each element borrows directly from R's CHARSXP data. UTF-8 validity is asserted
79/// at package init via `miniextendr_assert_utf8_locale()`, so no per-string
80/// encoding translation is needed.
81///
82/// # NA Handling
83///
84/// **Warning:** `NA_character_` is converted to `Cow::Borrowed("")`. This is lossy!
85/// Use `Vec<Option<Cow<'static, str>>>` to distinguish NA from empty strings.
86impl TryFromSexp for Vec<Cow<'static, str>> {
87    type Error = SexpError;
88
89    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
90        map_strsxp_with(sexp, |charsxp, _i| {
91            if charsxp == SEXP::na_string() || charsxp == SEXP::blank_string() {
92                Ok(Cow::Borrowed(""))
93            } else {
94                Ok(unsafe { charsxp_to_cow(charsxp) })
95            }
96        })
97    }
98}
99
100/// Convert R character vector to `Vec<Option<Cow<'static, str>>>` — zero-copy, NA-aware.
101///
102/// `NA_character_` → `None`, valid strings → `Some(Cow::Borrowed(&str))`.
103impl TryFromSexp for Vec<Option<Cow<'static, str>>> {
104    type Error = SexpError;
105
106    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
107        map_strsxp_with(sexp, |charsxp, _i| {
108            if charsxp == SEXP::na_string() {
109                Ok(None)
110            } else {
111                // charsxp_to_cow returns Cow::Borrowed("") for R_BlankString-equivalent
112                Ok(Some(unsafe { charsxp_to_cow(charsxp) }))
113            }
114        })
115    }
116}
117
118/// Convert R character vector to `Vec<String>`.
119///
120/// # NA and Encoding Handling
121///
122/// **Warning:** This conversion is lossy for NA values and encoding failures:
123/// - `NA_character_` values are converted to empty string `""`
124/// - Encoding translation failures become empty string `""`
125/// - Invalid UTF-8 (after translation) becomes empty string `""`
126///
127/// If you need to preserve NA semantics, use `Vec<Option<String>>` instead:
128///
129/// ```ignore
130/// let strings: Vec<Option<String>> = sexp.try_into()?;
131/// // NA values will be None, valid strings will be Some(s)
132/// ```
133///
134/// This design choice prioritizes convenience over strict correctness for the
135/// common case where strings are known to be non-NA and properly encoded.
136impl TryFromSexp for Vec<String> {
137    type Error = SexpError;
138
139    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
140        map_strsxp_with(sexp, |charsxp, _i| {
141            let s = if charsxp == SEXP::na_string() {
142                String::new()
143            } else {
144                unsafe { charsxp_to_str(charsxp) }.to_owned()
145            };
146            Ok(s)
147        })
148    }
149}
150
151/// Convert R character vector to `Vec<&str>`.
152///
153/// **Warning:** `NA_character_` values are converted to empty string `""`.
154impl TryFromSexp for Vec<&'static str> {
155    type Error = SexpError;
156
157    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
158        map_strsxp_with(sexp, |charsxp, _i| {
159            if charsxp == SEXP::na_string() || charsxp == SEXP::blank_string() {
160                return Ok("");
161            }
162            Ok(unsafe { charsxp_to_str(charsxp) })
163        })
164    }
165}
166
167/// Convert R character vector to `Vec<Option<&str>>`.
168impl TryFromSexp for Vec<Option<&'static str>> {
169    type Error = SexpError;
170
171    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
172        map_strsxp_with(sexp, |charsxp, _i| {
173            if charsxp == SEXP::na_string() {
174                return Ok(None);
175            }
176            if charsxp == SEXP::blank_string() {
177                return Ok(Some(""));
178            }
179            Ok(Some(unsafe { charsxp_to_str(charsxp) }))
180        })
181    }
182}
183
184macro_rules! impl_set_string_try_from_sexp {
185    ($(#[$meta:meta])* $set_ty:ident) => {
186        $(#[$meta])*
187        impl TryFromSexp for $set_ty<String> {
188            type Error = SexpError;
189
190            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
191                let vec: Vec<String> = TryFromSexp::try_from_sexp(sexp)?;
192                Ok(vec.into_iter().collect())
193            }
194        }
195    };
196}
197
198impl_set_string_try_from_sexp!(
199    /// Convert R character vector to `HashSet<String>`.
200    HashSet
201);
202impl_set_string_try_from_sexp!(
203    /// Convert R character vector to `BTreeSet<String>`.
204    BTreeSet
205);
206// endregion
207
208// region: String-wrapper type conversions (PathBuf, OsString)
209
210/// Generate TryFromSexp impls for types that are `From<String>` (scalar, Option,
211/// Vec, `Vec<Option>`). Used for PathBuf and OsString which delegate to String conversion.
212macro_rules! impl_string_wrapper_try_from_sexp {
213    (
214        $(#[$scalar_meta:meta])*
215        scalar: $ty:ty;
216        $(#[$option_meta:meta])*
217        option: $ty2:ty;
218        $(#[$vec_meta:meta])*
219        vec: $ty3:ty;
220        $(#[$vec_option_meta:meta])*
221        vec_option: $ty4:ty;
222    ) => {
223        $(#[$scalar_meta])*
224        impl TryFromSexp for $ty {
225            type Error = SexpError;
226
227            #[inline]
228            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
229                let s: String = TryFromSexp::try_from_sexp(sexp)?;
230                Ok(<$ty>::from(s))
231            }
232
233            #[inline]
234            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
235                let s: String = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
236                Ok(<$ty>::from(s))
237            }
238        }
239
240        $(#[$option_meta])*
241        impl TryFromSexp for Option<$ty> {
242            type Error = SexpError;
243
244            #[inline]
245            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
246                let opt: Option<String> = TryFromSexp::try_from_sexp(sexp)?;
247                Ok(opt.map(<$ty>::from))
248            }
249
250            #[inline]
251            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
252                let opt: Option<String> = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
253                Ok(opt.map(<$ty>::from))
254            }
255        }
256
257        $(#[$vec_meta])*
258        impl TryFromSexp for Vec<$ty> {
259            type Error = SexpError;
260
261            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
262                let vec: Vec<String> = TryFromSexp::try_from_sexp(sexp)?;
263                Ok(vec.into_iter().map(<$ty>::from).collect())
264            }
265
266            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
267                let vec: Vec<String> = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
268                Ok(vec.into_iter().map(<$ty>::from).collect())
269            }
270        }
271
272        $(#[$vec_option_meta])*
273        impl TryFromSexp for Vec<Option<$ty>> {
274            type Error = SexpError;
275
276            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
277                let vec: Vec<Option<String>> = TryFromSexp::try_from_sexp(sexp)?;
278                Ok(vec.into_iter().map(|opt| opt.map(<$ty>::from)).collect())
279            }
280
281            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
282                let vec: Vec<Option<String>> = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
283                Ok(vec.into_iter().map(|opt| opt.map(<$ty>::from)).collect())
284            }
285        }
286    };
287}
288
289impl_string_wrapper_try_from_sexp!(
290    /// Convert R character scalar (STRSXP of length 1) to `PathBuf`.
291    ///
292    /// # NA Handling
293    ///
294    /// **Warning:** `NA_character_` is converted to empty path `""`. This is lossy!
295    /// If you need to distinguish between NA and empty strings, use `Option<PathBuf>` instead.
296    scalar: PathBuf;
297    /// NA-aware PathBuf conversion: returns `None` for `NA_character_` or `NULL`.
298    option: PathBuf;
299    /// Convert R character vector (STRSXP) to `Vec<PathBuf>`.
300    ///
301    /// # NA Handling
302    ///
303    /// **Warning:** `NA_character_` elements are converted to empty paths.
304    /// Use `Vec<Option<PathBuf>>` if you need to preserve NA values.
305    vec: PathBuf;
306    /// Convert R character vector (STRSXP) to `Vec<Option<PathBuf>>` with NA support.
307    ///
308    /// `NA_character_` elements are converted to `None`.
309    vec_option: PathBuf;
310);
311
312impl_string_wrapper_try_from_sexp!(
313    /// Convert R character scalar (STRSXP of length 1) to `OsString`.
314    ///
315    /// Since R strings are converted to UTF-8, the resulting `OsString` contains
316    /// valid UTF-8 data.
317    ///
318    /// # NA Handling
319    ///
320    /// **Warning:** `NA_character_` is converted to empty string. This is lossy!
321    /// If you need to distinguish between NA and empty strings, use `Option<OsString>` instead.
322    scalar: OsString;
323    /// NA-aware OsString conversion: returns `None` for `NA_character_` or `NULL`.
324    option: OsString;
325    /// Convert R character vector (STRSXP) to `Vec<OsString>`.
326    ///
327    /// # NA Handling
328    ///
329    /// **Warning:** `NA_character_` elements are converted to empty strings.
330    /// Use `Vec<Option<OsString>>` if you need to preserve NA values.
331    vec: OsString;
332    /// Convert R character vector (STRSXP) to `Vec<Option<OsString>>` with NA support.
333    ///
334    /// `NA_character_` elements are converted to `None`.
335    vec_option: OsString;
336);
337// endregion