Skip to content

Commit cb00978

Browse files
Add conversion for &Cstr, Cstring and Cow<Cstr>
1 parent 9fd849b commit cb00978

File tree

3 files changed

+180
-0
lines changed

3 files changed

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

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)