Skip to main content

miniextendr_api/from_r/
strings.rs

1//! String conversions — STRSXP requires special handling via `STRING_ELT`.
2//!
3//! R stores strings as STRSXP (vector of CHARSXP). Each element requires
4//! `STRING_ELT` + `R_CHAR` to extract, unlike numeric vectors which expose
5//! a contiguous data pointer.
6//!
7//! Covers: `&str`, `String`, `char`, `Option<&str>`, `Option<String>`,
8//! `Vec<String>`, `Vec<&str>`, `Box<[String]>`.
9//!
10//! # Tradeoff
11//!
12//! Prefer borrowed `&str` / `Vec<&str>` over owned `String` / `Vec<String>`
13//! when the data only needs to live for the `.Call` — borrowed strings point
14//! straight into R's CHARSXP pool with no allocation. Use `Option<String>`
15//! / `Vec<Option<String>>` when callers may pass `NA_character_`; the plain
16//! `String` / `Vec<String>` impls map NA to `""`, which is lossy. The optional
17//! forms preserve the distinction as `None`.
18//!
19//! UTF-8 validity is guaranteed by `miniextendr_assert_utf8_locale()` at
20//! package init — these impls skip per-string validation. Outbound
21//! counterparts: `String` / `&str` impls in [`crate::into_r`].
22
23use crate::from_r::{
24    SexpError, TryFromSexp, charsxp_to_str, charsxp_to_str_unchecked, scalar_charsxp,
25    scalar_charsxp_unchecked,
26};
27use crate::{SEXP, SEXPTYPE, SexpExt};
28
29/// Convert R character vector (STRSXP) to Rust &str.
30///
31/// Extracts the first element of the character vector and returns it as a UTF-8 string.
32/// The Rust type uses a static lifetime for API convenience, but the borrow is
33/// only valid while the source CHARSXP remains reachable.
34///
35/// # NA Handling
36///
37/// **Warning:** `NA_character_` is converted to empty string `""`. This is lossy!
38/// If you need to distinguish between NA and empty strings, use `Option<String>` instead:
39///
40/// ```ignore
41/// let maybe_str: Option<String> = sexp.try_into()?;
42/// ```
43///
44/// # Safety
45/// The returned &str is only valid as long as R doesn't garbage collect the CHARSXP.
46/// For ALTSTRING inputs, rooting the source alone may not retain its elements.
47/// Use `TryFromRoot` to retain uncached elements for the call's scope.
48impl TryFromSexp for &'static str {
49    type Error = SexpError;
50    const CHARACTER_ONLY: bool = true;
51
52    #[inline]
53    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
54        let charsxp = scalar_charsxp(sexp)?;
55
56        // Check for NA_STRING or R_BlankString
57        if charsxp == SEXP::na_string() {
58            return Ok("");
59        }
60        if charsxp == SEXP::blank_string() {
61            return Ok("");
62        }
63
64        // Use LENGTH-based conversion (O(1)) instead of CStr::from_ptr (O(n) strlen)
65        Ok(unsafe { charsxp_to_str(charsxp) })
66    }
67
68    unsafe fn __mx_try_from_rooted(root: crate::Root<'_>) -> Result<Self, Self::Error> {
69        let charsxp = scalar_charsxp(root.get())?;
70        if charsxp == SEXP::na_string() || charsxp == SEXP::blank_string() {
71            return Ok("");
72        }
73        let charsxp = unsafe { root.protect_child(charsxp) }.get();
74        Ok(unsafe { charsxp_to_str(charsxp) })
75    }
76
77    #[inline]
78    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
79        let charsxp = unsafe { scalar_charsxp_unchecked(sexp)? };
80
81        // Check for NA_STRING or R_BlankString
82        if charsxp == SEXP::na_string() {
83            return Ok("");
84        }
85        if charsxp == SEXP::blank_string() {
86            return Ok("");
87        }
88
89        // Use LENGTH-based conversion (O(1)) instead of CStr::from_ptr (O(n) strlen)
90        Ok(unsafe { charsxp_to_str_unchecked(charsxp) })
91    }
92}
93
94impl TryFromSexp for Option<&'static str> {
95    type Error = SexpError;
96    const CHARACTER_ONLY: bool = true;
97
98    #[inline]
99    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
100        if sexp.type_of() == SEXPTYPE::NILSXP {
101            return Ok(None);
102        }
103
104        let charsxp = scalar_charsxp(sexp)?;
105        if charsxp == SEXP::na_string() {
106            return Ok(None);
107        }
108        if charsxp == SEXP::blank_string() {
109            return Ok(Some(""));
110        }
111
112        Ok(Some(unsafe { charsxp_to_str(charsxp) }))
113    }
114
115    unsafe fn __mx_try_from_rooted(root: crate::Root<'_>) -> Result<Self, Self::Error> {
116        if root.get().type_of() == SEXPTYPE::NILSXP {
117            return Ok(None);
118        }
119        let charsxp = scalar_charsxp(root.get())?;
120        if charsxp == SEXP::na_string() {
121            return Ok(None);
122        }
123        if charsxp == SEXP::blank_string() {
124            return Ok(Some(""));
125        }
126        let charsxp = unsafe { root.protect_child(charsxp) }.get();
127        Ok(Some(unsafe { charsxp_to_str(charsxp) }))
128    }
129
130    #[inline]
131    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
132        if sexp.type_of() == SEXPTYPE::NILSXP {
133            return Ok(None);
134        }
135
136        let charsxp = unsafe { scalar_charsxp_unchecked(sexp)? };
137        if charsxp == SEXP::na_string() {
138            return Ok(None);
139        }
140        if charsxp == SEXP::blank_string() {
141            return Ok(Some(""));
142        }
143
144        Ok(Some(unsafe { charsxp_to_str_unchecked(charsxp) }))
145    }
146}
147
148/// Convert R character vector (STRSXP) to Rust char.
149///
150/// Extracts the first character of the first element of the character vector.
151/// Returns an error if the string is empty, NA, or has more than one character.
152impl TryFromSexp for char {
153    type Error = SexpError;
154    const CHARACTER_ONLY: bool = true;
155
156    #[inline]
157    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
158        let s: &str = TryFromSexp::try_from_sexp(sexp)?;
159        let mut chars = s.chars();
160        match (chars.next(), chars.next()) {
161            (Some(c), None) => Ok(c),
162            (None, _) => Err(SexpError::InvalidValue(
163                "empty string cannot be converted to char".to_string(),
164            )),
165            (Some(_), Some(_)) => Err(SexpError::InvalidValue(
166                "string has more than one character".to_string(),
167            )),
168        }
169    }
170
171    #[inline]
172    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
173        let s: &str = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
174        let mut chars = s.chars();
175        match (chars.next(), chars.next()) {
176            (Some(c), None) => Ok(c),
177            (None, _) => Err(SexpError::InvalidValue(
178                "empty string cannot be converted to char".to_string(),
179            )),
180            (Some(_), Some(_)) => Err(SexpError::InvalidValue(
181                "string has more than one character".to_string(),
182            )),
183        }
184    }
185}
186
187/// Convert R character vector (STRSXP) to owned Rust String.
188///
189/// Extracts the first element and creates an owned copy.
190///
191/// # NA Handling
192///
193/// **Warning:** `NA_character_` is converted to empty string `""`. This is lossy!
194/// If you need to distinguish between NA and empty strings, use `Option<String>` instead:
195///
196/// ```ignore
197/// let maybe_str: Option<String> = sexp.try_into()?;
198/// ```
199impl TryFromSexp for String {
200    type Error = SexpError;
201    const CHARACTER_ONLY: bool = true;
202
203    #[inline]
204    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
205        let charsxp = scalar_charsxp(sexp)?;
206
207        if charsxp == SEXP::na_string() {
208            return Ok(String::new());
209        }
210
211        Ok(unsafe { charsxp_to_str(charsxp) }.to_owned())
212    }
213
214    #[inline]
215    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
216        let charsxp = unsafe { scalar_charsxp_unchecked(sexp)? };
217
218        if charsxp == SEXP::na_string() {
219            return Ok(String::new());
220        }
221
222        Ok(unsafe { charsxp_to_str_unchecked(charsxp) }.to_owned())
223    }
224}
225
226/// NA-aware string conversion: returns `None` for `NA_character_`.
227///
228/// Use this when you need to distinguish between NA and empty strings:
229/// ```ignore
230/// let maybe_str: Option<String> = sexp.try_into()?;
231/// match maybe_str {
232///     Some(s) => println!("Got string: {}", s),
233///     None => println!("Got NA"),
234/// }
235/// ```
236impl TryFromSexp for Option<String> {
237    type Error = SexpError;
238    const CHARACTER_ONLY: bool = true;
239
240    #[inline]
241    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
242        // NULL -> None
243        if sexp.type_of() == SEXPTYPE::NILSXP {
244            return Ok(None);
245        }
246
247        let charsxp = scalar_charsxp(sexp)?;
248
249        // Return None for NA_STRING
250        if charsxp == SEXP::na_string() {
251            return Ok(None);
252        }
253
254        Ok(Some(unsafe { charsxp_to_str(charsxp) }.to_owned()))
255    }
256
257    #[inline]
258    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
259        // For Option<String>, unchecked is same as checked (NA check is semantic, not safety)
260        Self::try_from_sexp(sexp)
261    }
262}
263// endregion
264
265// region: Owned unsized strings
266//
267// Preserve String's scalar/NA semantics and vector diagnostics. Each string is
268// owned before returning, so no CHARSXP borrow escapes in an Rc/Arc/Box.
269macro_rules! impl_owned_str_from_r {
270    ($owner:ident) => {
271        impl TryFromSexp for $owner<str> {
272            type Error = SexpError;
273            const CHARACTER_ONLY: bool = true;
274            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
275                String::try_from_sexp(sexp).map(Self::from)
276            }
277            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
278                unsafe { String::try_from_sexp_unchecked(sexp) }.map(Self::from)
279            }
280        }
281        impl TryFromSexp for Option<$owner<str>> {
282            type Error = SexpError;
283            const CHARACTER_ONLY: bool = true;
284            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
285                Option::<String>::try_from_sexp(sexp).map(|v| v.map($owner::from))
286            }
287            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
288                unsafe { Option::<String>::try_from_sexp_unchecked(sexp) }
289                    .map(|v| v.map($owner::from))
290            }
291        }
292        impl TryFromSexp for Vec<$owner<str>> {
293            type Error = SexpError;
294            const CHARACTER_ONLY: bool = true;
295            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
296                Vec::<String>::try_from_sexp(sexp)
297                    .map(|v| v.into_iter().map($owner::from).collect())
298            }
299            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
300                unsafe { Vec::<String>::try_from_sexp_unchecked(sexp) }
301                    .map(|v| v.into_iter().map($owner::from).collect())
302            }
303        }
304        impl TryFromSexp for Vec<Option<$owner<str>>> {
305            type Error = SexpError;
306            const CHARACTER_ONLY: bool = true;
307            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
308                Vec::<Option<String>>::try_from_sexp(sexp)
309                    .map(|v| v.into_iter().map(|s| s.map($owner::from)).collect())
310            }
311            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
312                unsafe { Vec::<Option<String>>::try_from_sexp_unchecked(sexp) }
313                    .map(|v| v.into_iter().map(|s| s.map($owner::from)).collect())
314            }
315        }
316    };
317}
318use std::rc::Rc;
319use std::sync::Arc;
320impl TryFromSexp for Box<str> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        String::try_from_sexp(sexp).map(Self::from)
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe { String::try_from_sexp_unchecked(sexp) }.map(Self::from)
    }
}
impl TryFromSexp for Option<Box<str>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Option::<String>::try_from_sexp(sexp).map(|v| v.map(Box::from))
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Option::<String>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.map(Box::from))
    }
}
impl TryFromSexp for Vec<Box<str>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Vec::<String>::try_from_sexp(sexp).map(|v|
                v.into_iter().map(Box::from).collect())
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Vec::<String>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.into_iter().map(Box::from).collect())
    }
}
impl TryFromSexp for Vec<Option<Box<str>>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Vec::<Option<String>>::try_from_sexp(sexp).map(|v|
                v.into_iter().map(|s| s.map(Box::from)).collect())
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Vec::<Option<String>>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.into_iter().map(|s| s.map(Box::from)).collect())
    }
}impl_owned_str_from_r!(Box);
321impl TryFromSexp for Rc<str> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        String::try_from_sexp(sexp).map(Self::from)
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe { String::try_from_sexp_unchecked(sexp) }.map(Self::from)
    }
}
impl TryFromSexp for Option<Rc<str>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Option::<String>::try_from_sexp(sexp).map(|v| v.map(Rc::from))
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Option::<String>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.map(Rc::from))
    }
}
impl TryFromSexp for Vec<Rc<str>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Vec::<String>::try_from_sexp(sexp).map(|v|
                v.into_iter().map(Rc::from).collect())
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Vec::<String>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.into_iter().map(Rc::from).collect())
    }
}
impl TryFromSexp for Vec<Option<Rc<str>>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Vec::<Option<String>>::try_from_sexp(sexp).map(|v|
                v.into_iter().map(|s| s.map(Rc::from)).collect())
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Vec::<Option<String>>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.into_iter().map(|s| s.map(Rc::from)).collect())
    }
}impl_owned_str_from_r!(Rc);
322impl TryFromSexp for Arc<str> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        String::try_from_sexp(sexp).map(Self::from)
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe { String::try_from_sexp_unchecked(sexp) }.map(Self::from)
    }
}
impl TryFromSexp for Option<Arc<str>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Option::<String>::try_from_sexp(sexp).map(|v| v.map(Arc::from))
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Option::<String>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.map(Arc::from))
    }
}
impl TryFromSexp for Vec<Arc<str>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Vec::<String>::try_from_sexp(sexp).map(|v|
                v.into_iter().map(Arc::from).collect())
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Vec::<String>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.into_iter().map(Arc::from).collect())
    }
}
impl TryFromSexp for Vec<Option<Arc<str>>> {
    type Error = SexpError;
    const CHARACTER_ONLY: bool = true;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        Vec::<Option<String>>::try_from_sexp(sexp).map(|v|
                v.into_iter().map(|s| s.map(Arc::from)).collect())
    }
    unsafe fn try_from_sexp_unchecked(sexp: SEXP)
        -> Result<Self, Self::Error> {
        unsafe {
                Vec::<Option<String>>::try_from_sexp_unchecked(sexp)
            }.map(|v| v.into_iter().map(|s| s.map(Arc::from)).collect())
    }
}impl_owned_str_from_r!(Arc);
323// endregion