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