Skip to content

Commit 2f4083e

Browse files
Add conversion for &Cstr, Cstring and Cow<Cstr> (#5482)
* Add conversion for `&Cstr`, `Cstring` and `Cow<Cstr>` * Add `Cow<Cstr>` roundtrip test * fix test * Cast to pystring before converting * fix MSRV * Safety comment
1 parent 9fd849b commit 2f4083e

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed

newsfragments/5482.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add conversion for `&Cstr`, `Cstring` and `Cow<Cstr>`

src/conversions/std/cstring.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use crate::types::PyString;
2+
use crate::{Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, Python};
3+
use std::borrow::Cow;
4+
use std::ffi::{CStr, CString};
5+
use std::str::Utf8Error;
6+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
7+
use {
8+
crate::{exceptions::PyValueError, ffi},
9+
std::slice,
10+
};
11+
12+
impl<'py> IntoPyObject<'py> for &CStr {
13+
type Target = PyString;
14+
type Output = Bound<'py, Self::Target>;
15+
type Error = Utf8Error;
16+
17+
#[inline]
18+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
19+
self.to_str()?.into_pyobject(py).map_err(|err| match err {})
20+
}
21+
}
22+
23+
impl<'py> IntoPyObject<'py> for CString {
24+
type Target = PyString;
25+
type Output = Bound<'py, Self::Target>;
26+
type Error = Utf8Error;
27+
28+
#[inline]
29+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
30+
(&*self).into_pyobject(py)
31+
}
32+
}
33+
34+
impl<'py> IntoPyObject<'py> for &CString {
35+
type Target = PyString;
36+
type Output = Bound<'py, Self::Target>;
37+
type Error = Utf8Error;
38+
39+
#[inline]
40+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
41+
(&**self).into_pyobject(py)
42+
}
43+
}
44+
45+
impl<'py> IntoPyObject<'py> for Cow<'_, CStr> {
46+
type Target = PyString;
47+
type Output = Bound<'py, Self::Target>;
48+
type Error = Utf8Error;
49+
50+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
51+
(*self).into_pyobject(py)
52+
}
53+
}
54+
55+
impl<'py> IntoPyObject<'py> for &Cow<'_, CStr> {
56+
type Target = PyString;
57+
type Output = Bound<'py, Self::Target>;
58+
type Error = Utf8Error;
59+
60+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
61+
(&**self).into_pyobject(py)
62+
}
63+
}
64+
65+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
66+
impl<'a> FromPyObject<'a, '_> for &'a CStr {
67+
type Error = PyErr;
68+
69+
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
70+
let obj = obj.cast::<PyString>()?;
71+
let mut size = 0;
72+
// SAFETY: obj is a PyString so we can safely call PyUnicode_AsUTF8AndSize
73+
let ptr = unsafe { ffi::PyUnicode_AsUTF8AndSize(obj.as_ptr(), &mut size) };
74+
75+
if ptr.is_null() {
76+
return Err(PyErr::fetch(obj.py()));
77+
}
78+
79+
// SAFETY: PyUnicode_AsUTF8AndSize always returns a NUL-terminated string but size does not
80+
// include the NUL terminator. So we add 1 to the size to include it.
81+
let slice = unsafe { slice::from_raw_parts(ptr.cast(), size as usize + 1) };
82+
83+
CStr::from_bytes_with_nul(slice).map_err(|err| PyValueError::new_err(err.to_string()))
84+
}
85+
}
86+
87+
impl<'a> FromPyObject<'a, '_> for Cow<'a, CStr> {
88+
type Error = PyErr;
89+
90+
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
91+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
92+
{
93+
Ok(Cow::Borrowed(obj.extract::<&CStr>()?))
94+
}
95+
96+
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
97+
{
98+
Ok(Cow::Owned(obj.extract::<CString>()?))
99+
}
100+
}
101+
}
102+
impl FromPyObject<'_, '_> for CString {
103+
type Error = PyErr;
104+
105+
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
106+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
107+
{
108+
Ok(obj.extract::<&CStr>()?.to_owned())
109+
}
110+
111+
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
112+
{
113+
CString::new(&*obj.cast::<PyString>()?.to_cow()?).map_err(Into::into)
114+
}
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
use crate::types::string::PyStringMethods;
122+
use crate::types::PyAnyMethods;
123+
use crate::Python;
124+
125+
#[test]
126+
fn test_into_pyobject() {
127+
Python::attach(|py| {
128+
let s = "Hello, Python!";
129+
let cstr = CString::new(s).unwrap();
130+
131+
let py_string = cstr.as_c_str().into_pyobject(py).unwrap();
132+
assert_eq!(py_string.to_cow().unwrap(), s);
133+
134+
let py_string = cstr.into_pyobject(py).unwrap();
135+
assert_eq!(py_string.to_cow().unwrap(), s);
136+
})
137+
}
138+
139+
#[test]
140+
fn test_extract_with_nul_error() {
141+
Python::attach(|py| {
142+
let s = "Hello\0Python";
143+
let py_string = s.into_pyobject(py).unwrap();
144+
145+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
146+
{
147+
let err = py_string.extract::<&CStr>();
148+
assert!(err.is_err());
149+
}
150+
151+
let err = py_string.extract::<CString>();
152+
assert!(err.is_err());
153+
})
154+
}
155+
156+
#[test]
157+
fn test_extract_cstr_and_cstring() {
158+
Python::attach(|py| {
159+
let s = "Hello, world!";
160+
let cstr = CString::new(s).unwrap();
161+
let py_string = cstr.as_c_str().into_pyobject(py).unwrap();
162+
163+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
164+
{
165+
let extracted_cstr: &CStr = py_string.extract().unwrap();
166+
assert_eq!(extracted_cstr.to_str().unwrap(), s);
167+
}
168+
169+
let extracted_cstring: CString = py_string.extract().unwrap();
170+
assert_eq!(extracted_cstring.to_str().unwrap(), s);
171+
})
172+
}
173+
174+
#[test]
175+
fn test_cow_roundtrip() {
176+
Python::attach(|py| {
177+
let s = "Hello, world!";
178+
let cstr = CString::new(s).unwrap();
179+
let cow: Cow<'_, CStr> = Cow::Borrowed(cstr.as_c_str());
180+
181+
let py_string = cow.into_pyobject(py).unwrap();
182+
assert_eq!(py_string.to_cow().unwrap(), s);
183+
184+
let roundtripped: Cow<'_, CStr> = py_string.extract().unwrap();
185+
assert_eq!(roundtripped.as_ref(), cstr.as_c_str());
186+
})
187+
}
188+
}

src/conversions/std/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod array;
22
mod cell;
3+
mod cstring;
34
mod ipaddr;
45
mod map;
56
mod num;

0 commit comments

Comments
 (0)