Skip to main content

pyo3_bytes/
bytes.rs

1//! Support for Python buffer protocol
2
3use std::fmt::Write;
4use std::os::raw::c_int;
5use std::ptr::NonNull;
6
7use bytes::{Bytes, BytesMut};
8use pyo3::buffer::PyBuffer;
9use pyo3::exceptions::{PyIndexError, PyMemoryError, PyOverflowError, PyValueError};
10use pyo3::prelude::*;
11use pyo3::types::{PyDict, PySlice, PyTuple};
12use pyo3::{ffi, IntoPyObjectExt};
13
14/// A wrapper around a [`bytes::Bytes`][].
15///
16/// This implements both import and export via the Python buffer protocol.
17///
18/// ### Buffer protocol import
19///
20/// This can be very useful as a general way to support ingest of a Python buffer protocol object.
21///
22/// The underlying [Bytes] manages the external memory, automatically calling the Python
23/// buffer's release callback when the internal reference count reaches 0.
24///
25/// Note that converting this [`Bytes`] into a [BytesMut][::bytes::BytesMut] will always create a
26/// deep copy of the buffer into newly allocated memory, since this `Bytes` is constructed from an
27/// owner.
28///
29/// ### Buffer protocol export
30///
31/// PyBytes implements the Python buffer protocol to enable Python to access the underlying `Bytes`
32/// data view without copies. In Python, this `PyBytes` object can be passed to Python `bytes` or
33/// `memoryview` constructors, `numpy.frombuffer`, or any other function that supports buffer
34/// protocol input.
35#[pyclass(name = "Bytes", subclass, frozen, sequence, weakref)]
36#[derive(Clone, Default, Hash, PartialEq, PartialOrd, Eq, Ord)]
37pub struct PyBytes(Bytes);
38
39impl AsRef<Bytes> for PyBytes {
40    fn as_ref(&self) -> &Bytes {
41        &self.0
42    }
43}
44
45impl AsRef<[u8]> for PyBytes {
46    fn as_ref(&self) -> &[u8] {
47        self.0.as_ref()
48    }
49}
50
51impl PyBytes {
52    /// Construct a new [PyBytes]
53    pub fn new(buffer: Bytes) -> Self {
54        Self(buffer)
55    }
56
57    /// Consume and return the [Bytes]
58    pub fn into_inner(self) -> Bytes {
59        self.0
60    }
61
62    /// Access the underlying buffer as a byte slice
63    pub fn as_slice(&self) -> &[u8] {
64        self.as_ref()
65    }
66
67    /// Slice the underlying buffer using a Python slice object
68    ///
69    /// This should behave the same as Python's byte slicing:
70    ///     - `ValueError` if step is zero
71    ///     - Negative indices a-ok
72    ///     - If start/stop are out of bounds, they are clipped to the bounds of the buffer
73    ///     - If start > stop, the slice is empty
74    ///
75    /// This is NOT exposed to Python under the `#[pymethods]` impl
76    fn slice(&self, slice: &Bound<'_, PySlice>) -> PyResult<PyBytes> {
77        let bytes_length = self.0.len() as isize;
78        let (start, stop, step) = {
79            let slice_indices = slice.indices(bytes_length)?;
80            (slice_indices.start, slice_indices.stop, slice_indices.step)
81        };
82
83        let new_capacity = if (step > 0 && stop > start) || (step < 0 && stop < start) {
84            (((stop - start).abs() + step.abs() - 1) / step.abs()) as usize
85        } else {
86            0
87        };
88
89        if new_capacity == 0 {
90            return Ok(PyBytes(Bytes::new()));
91        }
92        if step == 1 {
93            // if start < 0  and stop > len and step == 1 just copy?
94            if start < 0 && stop >= bytes_length {
95                let out = self.0.slice(..);
96                let py_bytes = PyBytes(out);
97                return Ok(py_bytes);
98            }
99
100            if start >= 0 && stop <= bytes_length && start < stop {
101                let out = self.0.slice(start as usize..stop as usize);
102                let py_bytes = PyBytes(out);
103                return Ok(py_bytes);
104            }
105            // fall through to the general case here...
106        }
107        if step > 0 {
108            // forward
109            let mut new_buf = BytesMut::with_capacity(new_capacity);
110            new_buf.extend(
111                (start..stop)
112                    .step_by(step as usize)
113                    .map(|i| self.0[i as usize]),
114            );
115            Ok(PyBytes(new_buf.freeze()))
116        } else {
117            // backward
118            let mut new_buf = BytesMut::with_capacity(new_capacity);
119            new_buf.extend(
120                (stop + 1..=start)
121                    .rev()
122                    .step_by((-step) as usize)
123                    .map(|i| self.0[i as usize]),
124            );
125            Ok(PyBytes(new_buf.freeze()))
126        }
127    }
128}
129
130impl From<PyBytes> for Bytes {
131    fn from(value: PyBytes) -> Self {
132        value.0
133    }
134}
135
136impl From<Vec<u8>> for PyBytes {
137    fn from(value: Vec<u8>) -> Self {
138        PyBytes(value.into())
139    }
140}
141
142impl From<Bytes> for PyBytes {
143    fn from(value: Bytes) -> Self {
144        PyBytes(value)
145    }
146}
147
148impl From<BytesMut> for PyBytes {
149    fn from(value: BytesMut) -> Self {
150        PyBytes(value.into())
151    }
152}
153
154#[pymethods]
155impl PyBytes {
156    // By setting the argument to PyBytes, this means that any buffer-protocol object is supported
157    // here, since it will use the FromPyObject impl.
158    #[new]
159    #[pyo3(signature = (buf = PyBytes(Bytes::new())), text_signature = "(buf = b'')")]
160    fn py_new(buf: PyBytes) -> Self {
161        buf
162    }
163
164    fn __getnewargs_ex__<'py>(&'py self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
165        let py_bytes = self.to_bytes(py);
166        let args = PyTuple::new(py, vec![py_bytes])?.into_bound_py_any(py)?;
167        let kwargs = PyDict::new(py).into_bound_py_any(py)?;
168        PyTuple::new(py, [args, kwargs])
169    }
170
171    /// The number of bytes in this Bytes
172    fn __len__(&self) -> usize {
173        self.0.len()
174    }
175
176    fn __repr__(&self) -> String {
177        format!("{self:?}")
178    }
179
180    fn __add__(&self, other: PyBytes) -> PyBytes {
181        let total_length = self.0.len() + other.0.len();
182        let mut new_buffer = BytesMut::with_capacity(total_length);
183        new_buffer.extend_from_slice(&self.0);
184        new_buffer.extend_from_slice(&other.0);
185        new_buffer.into()
186    }
187
188    fn __contains__(&self, item: PyBytes) -> bool {
189        let needle = item.as_slice();
190        needle.is_empty() || self.0.windows(needle.len()).any(|window| window == needle)
191    }
192
193    fn __eq__(&self, other: PyBytes) -> bool {
194        self.0.as_ref() == other.0.as_ref()
195    }
196
197    fn __getitem__<'py>(
198        &self,
199        py: Python<'py>,
200        key: BytesGetItemKey<'py>,
201    ) -> PyResult<Bound<'py, PyAny>> {
202        match key {
203            BytesGetItemKey::Int(mut index) => {
204                if index < 0 {
205                    index += self.0.len() as isize;
206                }
207                if index < 0 {
208                    return Err(PyIndexError::new_err("Index out of range"));
209                }
210                self.0
211                    .get(index as usize)
212                    .ok_or(PyIndexError::new_err("Index out of range"))?
213                    .into_bound_py_any(py)
214            }
215            BytesGetItemKey::Slice(slice) => {
216                let s = self.slice(&slice)?;
217                s.into_bound_py_any(py)
218            }
219        }
220    }
221
222    fn __mul__(&self, value: usize) -> PyResult<PyBytes> {
223        // Raise OverflowError if the total length exceeds isize::MAX, since Python's bytes type
224        // cannot be larger than that.
225        let total_length = self
226            .0
227            .len()
228            .checked_mul(value)
229            .filter(|&len| len <= isize::MAX as usize)
230            .ok_or_else(|| PyOverflowError::new_err("repeated bytes are too long"))?;
231
232        if total_length == 0 {
233            return Ok(PyBytes::new(Bytes::new()));
234        }
235
236        // Raise MemoryError if the allocation fails
237        let mut out_buf = Vec::new();
238        out_buf
239            .try_reserve_exact(total_length)
240            .map_err(|_| PyMemoryError::new_err(()))?;
241
242        (0..value).for_each(|_| out_buf.extend_from_slice(self.0.as_ref()));
243        Ok(out_buf.into())
244    }
245
246    /// This is taken from opendal:
247    /// https://github.com/apache/opendal/blob/d001321b0f9834bc1e2e7d463bcfdc3683e968c9/bindings/python/src/utils.rs#L51-L72
248    #[allow(unsafe_code)]
249    unsafe fn __getbuffer__(
250        slf: PyRef<Self>,
251        view: *mut ffi::Py_buffer,
252        flags: c_int,
253    ) -> PyResult<()> {
254        let bytes = slf.0.as_ref();
255        let ret = ffi::PyBuffer_FillInfo(
256            view,
257            slf.as_ptr() as *mut _,
258            bytes.as_ptr() as *mut _,
259            bytes.len().try_into().unwrap(),
260            1, // read only
261            flags,
262        );
263        if ret == -1 {
264            return Err(PyErr::fetch(slf.py()));
265        }
266        Ok(())
267    }
268
269    // Comment from david hewitt on discord:
270    // > I think normally `__getbuffer__` takes a pointer to the owning Python object, so you
271    // > don't need to treat the allocation as owned separately. It should be good enough to keep
272    // > the allocation owned by the object.
273    // https://discord.com/channels/1209263839632424990/1324816949464666194/1328299411427557397
274    #[allow(unsafe_code)]
275    unsafe fn __releasebuffer__(&self, _view: *mut ffi::Py_buffer) {}
276
277    /// If the binary data starts with the prefix string, return bytes[len(prefix):]. Otherwise,
278    /// return a copy of the original binary data:
279    #[pyo3(signature = (prefix, /))]
280    fn removeprefix(&self, prefix: PyBytes) -> PyBytes {
281        if self.0.starts_with(prefix.as_ref()) {
282            self.0.slice(prefix.0.len()..).into()
283        } else {
284            self.0.clone().into()
285        }
286    }
287
288    /// If the binary data ends with the suffix string and that suffix is not empty, return
289    /// `bytes[:-len(suffix)]`. Otherwise, return the original binary data.
290    #[pyo3(signature = (suffix, /))]
291    fn removesuffix(&self, suffix: PyBytes) -> PyBytes {
292        if self.0.ends_with(suffix.as_ref()) {
293            self.0.slice(0..self.0.len() - suffix.0.len()).into()
294        } else {
295            self.0.clone().into()
296        }
297    }
298
299    /// Return True if all bytes in the sequence are alphabetical ASCII characters or ASCII decimal
300    /// digits and the sequence is not empty, False otherwise. Alphabetic ASCII characters are
301    /// those byte values in the sequence b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.
302    /// ASCII decimal digits are those byte values in the sequence b'0123456789'.
303    fn isalnum(&self) -> bool {
304        if self.0.is_empty() {
305            return false;
306        }
307
308        for c in self.0.as_ref() {
309            if !c.is_ascii_alphanumeric() {
310                return false;
311            }
312        }
313        true
314    }
315
316    /// Return True if all bytes in the sequence are alphabetic ASCII characters and the sequence
317    /// is not empty, False otherwise. Alphabetic ASCII characters are those byte values in the
318    /// sequence b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.
319    fn isalpha(&self) -> bool {
320        if self.0.is_empty() {
321            return false;
322        }
323
324        for c in self.0.as_ref() {
325            if !c.is_ascii_alphabetic() {
326                return false;
327            }
328        }
329        true
330    }
331
332    /// Return True if the sequence is empty or all bytes in the sequence are ASCII, False
333    /// otherwise. ASCII bytes are in the range 0-0x7F.
334    fn isascii(&self) -> bool {
335        for c in self.0.as_ref() {
336            if !c.is_ascii() {
337                return false;
338            }
339        }
340        true
341    }
342
343    /// Return True if all bytes in the sequence are ASCII decimal digits and the sequence is not
344    /// empty, False otherwise. ASCII decimal digits are those byte values in the sequence
345    /// b'0123456789'.
346    fn isdigit(&self) -> bool {
347        if self.0.is_empty() {
348            return false;
349        }
350
351        for c in self.0.as_ref() {
352            if !c.is_ascii_digit() {
353                return false;
354            }
355        }
356        true
357    }
358
359    /// Return True if there is at least one lowercase ASCII character in the sequence and no
360    /// uppercase ASCII characters, False otherwise.
361    fn islower(&self) -> bool {
362        let mut has_lower = false;
363        for c in self.0.as_ref() {
364            if c.is_ascii_uppercase() {
365                return false;
366            }
367            if !has_lower && c.is_ascii_lowercase() {
368                has_lower = true;
369            }
370        }
371
372        has_lower
373    }
374
375    /// Return True if all bytes in the sequence are ASCII whitespace and the sequence is not
376    /// empty, False otherwise. ASCII whitespace characters are those byte values in the sequence
377    /// b' \t\n\r\x0b\f' (space, tab, newline, carriage return, vertical tab, form feed).
378    fn isspace(&self) -> bool {
379        if self.0.is_empty() {
380            return false;
381        }
382
383        for c in self.0.as_ref() {
384            // Also check for vertical tab
385            if !(c.is_ascii_whitespace() || *c == b'\x0b') {
386                return false;
387            }
388        }
389        true
390    }
391
392    /// Return True if there is at least one uppercase alphabetic ASCII character in the sequence
393    /// and no lowercase ASCII characters, False otherwise.
394    fn isupper(&self) -> bool {
395        let mut has_upper = false;
396        for c in self.0.as_ref() {
397            if c.is_ascii_lowercase() {
398                return false;
399            }
400            if !has_upper && c.is_ascii_uppercase() {
401                has_upper = true;
402            }
403        }
404
405        has_upper
406    }
407
408    /// Return a copy of the sequence with all the uppercase ASCII characters converted to their
409    /// corresponding lowercase counterpart.
410    fn lower(&self) -> PyBytes {
411        self.0.to_ascii_lowercase().into()
412    }
413
414    /// Return a copy of the sequence with all the lowercase ASCII characters converted to their
415    /// corresponding uppercase counterpart.
416    fn upper(&self) -> PyBytes {
417        self.0.to_ascii_uppercase().into()
418    }
419
420    /// Copy this buffer's contents to a Python `bytes` object
421    fn to_bytes<'py>(&'py self, py: Python<'py>) -> Bound<'py, pyo3::types::PyBytes> {
422        pyo3::types::PyBytes::new(py, &self.0)
423    }
424}
425
426impl<'py> FromPyObject<'_, 'py> for PyBytes {
427    type Error = PyErr;
428
429    fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
430        let buffer = obj.extract::<PyBytesWrapper>()?;
431        let bytes = Bytes::from_owner(buffer);
432        Ok(Self(bytes))
433    }
434}
435
436/// A wrapper around a PyBuffer that applies a custom destructor that checks if the Python
437/// interpreter is still initialized before freeing the buffer memory.
438///
439/// This also implements AsRef<[u8]> because that is required for Bytes::from_owner
440#[derive(Debug)]
441struct PyBytesWrapper(PyBuffer<u8>);
442
443impl AsRef<[u8]> for PyBytesWrapper {
444    #[allow(unsafe_code)]
445    fn as_ref(&self) -> &[u8] {
446        let len = self.0.item_count();
447
448        let ptr = NonNull::new(self.0.buf_ptr() as _).expect("Expected buffer ptr to be non null");
449
450        // Safety:
451        //
452        // This requires that the data will not be mutated from Python. Sadly, the buffer protocol
453        // does not uphold this invariant always for us, and the Python user must take care not to
454        // mutate the provided buffer.
455        unsafe { std::slice::from_raw_parts(ptr.as_ptr() as *const u8, len) }
456    }
457}
458
459fn validate_buffer(buf: &PyBuffer<u8>) -> PyResult<()> {
460    if !buf.is_c_contiguous() {
461        return Err(PyValueError::new_err("Buffer is not C contiguous"));
462    }
463
464    if buf.strides().iter().any(|s| *s != 1) {
465        return Err(PyValueError::new_err(format!(
466            "strides other than 1 not supported, got: {:?} ",
467            buf.strides()
468        )));
469    }
470
471    Ok(())
472}
473
474impl<'py> FromPyObject<'_, 'py> for PyBytesWrapper {
475    type Error = PyErr;
476
477    fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
478        let buffer = obj.extract::<PyBuffer<u8>>()?;
479        validate_buffer(&buffer)?;
480        Ok(Self(buffer))
481    }
482}
483
484/// This is _mostly_ the same as the upstream [`bytes::Bytes` Debug
485/// impl](https://github.com/tokio-rs/bytes/blob/71824b095c4150b3af0776ac158795c00ff9d53f/src/fmt/debug.rs#L6-L37),
486/// however we don't use it because that impl doesn't look how the python bytes repr looks; this
487/// isn't exactly the same either, as the python repr will switch between `'` and `"` based on the
488/// presence of the other in the string, but it's close enough AND we don't have to do a full scan
489/// of the bytes to check for that.
490impl std::fmt::Debug for PyBytes {
491    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492        f.write_str("Bytes(b\"")?;
493        for &byte in self.0.as_ref() {
494            match byte {
495                // https://doc.rust-lang.org/reference/tokens.html#byte-escapes
496                b'\\' => f.write_str(r"\\")?,
497                b'"' => f.write_str("\\\"")?,
498                b'\n' => f.write_str(r"\n")?,
499                b'\r' => f.write_str(r"\r")?,
500                b'\t' => f.write_str(r"\t")?,
501                // printable ASCII
502                0x20..=0x7E => f.write_char(byte as char)?,
503                _ => write!(f, "\\x{byte:02x}")?,
504            }
505        }
506        f.write_str("\")")?;
507        Ok(())
508    }
509}
510
511/// A key for the `__getitem__` method of `PyBytes` - int/slice
512#[derive(FromPyObject)]
513enum BytesGetItemKey<'py> {
514    /// An integer index
515    Int(isize),
516    /// A python slice
517    Slice(Bound<'py, PySlice>),
518}