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