Skip to main content

rust_expect/util/
zerocopy.rs

1//! Zero-copy I/O utilities.
2//!
3//! This module provides utilities for minimizing buffer copies during
4//! I/O operations, improving performance for high-throughput terminal
5//! automation.
6//!
7//! # Features
8//!
9//! - `BytesBuffer`: Reference-counted bytes for zero-copy slicing
10//! - `VecWriter`: Efficient vectored write batching
11//! - `BorrowedView`: Borrowed slice views with lifetime tracking
12//!
13//! # Example
14//!
15//! ```rust
16//! use rust_expect::util::zerocopy::{BytesBuffer, VecWriter};
17//!
18//! // Create a buffer with zero-copy slicing
19//! let mut buffer = BytesBuffer::new();
20//! buffer.extend(b"hello world");
21//!
22//! // Slice without copying
23//! let slice = buffer.slice(0..5);
24//! assert_eq!(&slice[..], b"hello");
25//!
26//! // Batch multiple writes
27//! let mut writer = VecWriter::new();
28//! writer.push(&b"hello"[..]);
29//! writer.push(&b" "[..]);
30//! writer.push(&b"world"[..]);
31//! let bytes = writer.freeze();
32//! assert_eq!(&bytes[..], b"hello world");
33//! ```
34
35use std::io::{self, IoSlice, Write};
36use std::ops::{Deref, Range, RangeBounds};
37
38use bytes::{Buf, Bytes, BytesMut};
39
40/// A reference-counted byte buffer that supports zero-copy slicing.
41///
42/// This is a wrapper around `bytes::Bytes` that provides convenient
43/// methods for terminal automation use cases.
44#[derive(Debug, Clone, Default)]
45pub struct BytesBuffer {
46    inner: BytesMut,
47}
48
49impl BytesBuffer {
50    /// Create an empty buffer.
51    #[must_use]
52    pub fn new() -> Self {
53        Self {
54            inner: BytesMut::new(),
55        }
56    }
57
58    /// Create a buffer with the specified capacity.
59    #[must_use]
60    pub fn with_capacity(capacity: usize) -> Self {
61        Self {
62            inner: BytesMut::with_capacity(capacity),
63        }
64    }
65
66    /// Create a buffer from existing bytes.
67    #[must_use]
68    pub fn from_bytes(data: impl Into<Bytes>) -> Self {
69        let bytes: Bytes = data.into();
70        let mut inner = BytesMut::with_capacity(bytes.len());
71        inner.extend_from_slice(&bytes);
72        Self { inner }
73    }
74
75    /// Get the length of the buffer.
76    #[must_use]
77    pub fn len(&self) -> usize {
78        self.inner.len()
79    }
80
81    /// Check if the buffer is empty.
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.inner.is_empty()
85    }
86
87    /// Get the capacity of the buffer.
88    #[must_use]
89    pub fn capacity(&self) -> usize {
90        self.inner.capacity()
91    }
92
93    /// Extend the buffer with data.
94    pub fn extend(&mut self, data: &[u8]) {
95        self.inner.extend_from_slice(data);
96    }
97
98    /// Reserve additional capacity.
99    pub fn reserve(&mut self, additional: usize) {
100        self.inner.reserve(additional);
101    }
102
103    /// Clear the buffer, retaining capacity.
104    pub fn clear(&mut self) {
105        self.inner.clear();
106    }
107
108    /// Get a zero-copy slice of the buffer.
109    ///
110    /// The returned `Bytes` shares ownership with the original buffer.
111    #[must_use]
112    pub fn slice(&self, range: Range<usize>) -> Bytes {
113        self.inner.clone().freeze().slice(range)
114    }
115
116    /// Get a zero-copy slice using range bounds.
117    #[must_use]
118    pub fn slice_ref<R: RangeBounds<usize>>(&self, range: R) -> Bytes {
119        use std::ops::Bound;
120
121        let start = match range.start_bound() {
122            Bound::Included(&n) => n,
123            Bound::Excluded(&n) => n + 1,
124            Bound::Unbounded => 0,
125        };
126
127        let end = match range.end_bound() {
128            Bound::Included(&n) => n + 1,
129            Bound::Excluded(&n) => n,
130            Bound::Unbounded => self.len(),
131        };
132
133        self.slice(start..end)
134    }
135
136    /// Freeze the buffer into immutable bytes.
137    ///
138    /// This is a zero-copy operation.
139    #[must_use]
140    pub fn freeze(self) -> Bytes {
141        self.inner.freeze()
142    }
143
144    /// Split off the first `at` bytes.
145    ///
146    /// Returns the split-off bytes, leaving the rest in the buffer.
147    pub fn split_to(&mut self, at: usize) -> BytesMut {
148        self.inner.split_to(at)
149    }
150
151    /// Split off bytes at the end.
152    pub fn split_off(&mut self, at: usize) -> BytesMut {
153        self.inner.split_off(at)
154    }
155
156    /// Consume `n` bytes from the front of the buffer.
157    pub fn advance(&mut self, n: usize) {
158        self.inner.advance(n);
159    }
160
161    /// Get an immutable view of the buffer.
162    #[must_use]
163    pub fn as_bytes(&self) -> &[u8] {
164        &self.inner
165    }
166
167    /// Get the buffer as a string (lossy UTF-8 conversion).
168    #[must_use]
169    pub fn as_str_lossy(&self) -> std::borrow::Cow<'_, str> {
170        String::from_utf8_lossy(&self.inner)
171    }
172
173    /// Find a byte pattern in the buffer.
174    #[must_use]
175    pub fn find(&self, needle: &[u8]) -> Option<usize> {
176        self.inner
177            .windows(needle.len())
178            .position(|window| window == needle)
179    }
180
181    /// Find a string in the buffer.
182    #[must_use]
183    pub fn find_str(&self, needle: &str) -> Option<usize> {
184        self.find(needle.as_bytes())
185    }
186
187    /// Get a view of the last `n` bytes.
188    #[must_use]
189    pub fn tail(&self, n: usize) -> &[u8] {
190        let start = self.len().saturating_sub(n);
191        &self.inner[start..]
192    }
193
194    /// Get a view of the first `n` bytes.
195    #[must_use]
196    pub fn head(&self, n: usize) -> &[u8] {
197        let end = n.min(self.len());
198        &self.inner[..end]
199    }
200}
201
202impl Deref for BytesBuffer {
203    type Target = [u8];
204
205    fn deref(&self) -> &Self::Target {
206        &self.inner
207    }
208}
209
210impl AsRef<[u8]> for BytesBuffer {
211    fn as_ref(&self) -> &[u8] {
212        &self.inner
213    }
214}
215
216impl From<Vec<u8>> for BytesBuffer {
217    fn from(vec: Vec<u8>) -> Self {
218        Self {
219            inner: BytesMut::from(&vec[..]),
220        }
221    }
222}
223
224impl From<&[u8]> for BytesBuffer {
225    fn from(slice: &[u8]) -> Self {
226        Self {
227            inner: BytesMut::from(slice),
228        }
229    }
230}
231
232impl From<&str> for BytesBuffer {
233    fn from(s: &str) -> Self {
234        Self::from(s.as_bytes())
235    }
236}
237
238impl Write for BytesBuffer {
239    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
240        self.extend(buf);
241        Ok(buf.len())
242    }
243
244    fn flush(&mut self) -> io::Result<()> {
245        Ok(())
246    }
247}
248
249/// A writer that batches multiple small writes for vectored I/O.
250///
251/// This reduces the number of system calls by accumulating writes
252/// and sending them as a single vectored write operation.
253#[derive(Debug, Default)]
254pub struct VecWriter {
255    chunks: Vec<Bytes>,
256    total_len: usize,
257}
258
259impl VecWriter {
260    /// Create a new vectored writer.
261    #[must_use]
262    pub const fn new() -> Self {
263        Self {
264            chunks: Vec::new(),
265            total_len: 0,
266        }
267    }
268
269    /// Create a vectored writer with the specified chunk capacity.
270    #[must_use]
271    pub fn with_capacity(capacity: usize) -> Self {
272        Self {
273            chunks: Vec::with_capacity(capacity),
274            total_len: 0,
275        }
276    }
277
278    /// Add bytes to the writer.
279    pub fn push(&mut self, data: impl Into<Bytes>) {
280        let bytes: Bytes = data.into();
281        self.total_len += bytes.len();
282        self.chunks.push(bytes);
283    }
284
285    /// Add a slice to the writer (copies into a new Bytes).
286    pub fn push_slice(&mut self, data: &[u8]) {
287        self.push(Bytes::copy_from_slice(data));
288    }
289
290    /// Get the number of chunks.
291    #[must_use]
292    pub const fn chunk_count(&self) -> usize {
293        self.chunks.len()
294    }
295
296    /// Get the total length across all chunks.
297    #[must_use]
298    pub const fn len(&self) -> usize {
299        self.total_len
300    }
301
302    /// Check if the writer is empty.
303    #[must_use]
304    pub const fn is_empty(&self) -> bool {
305        self.total_len == 0
306    }
307
308    /// Clear all chunks.
309    pub fn clear(&mut self) {
310        self.chunks.clear();
311        self.total_len = 0;
312    }
313
314    /// Get the chunks as I/O slices for vectored writes.
315    ///
316    /// The returned slices can be passed to `write_vectored`.
317    #[must_use]
318    pub fn as_io_slices(&self) -> Vec<IoSlice<'_>> {
319        self.chunks.iter().map(|b| IoSlice::new(b)).collect()
320    }
321
322    /// Freeze all chunks into a single contiguous buffer.
323    ///
324    /// This is useful when you need a single contiguous view.
325    #[must_use]
326    pub fn freeze(self) -> Bytes {
327        if self.chunks.len() == 1 {
328            // Fast path: single chunk
329            return self
330                .chunks
331                .into_iter()
332                .next()
333                .expect("guarded by chunks.len() == 1 above");
334        }
335
336        let mut buffer = BytesMut::with_capacity(self.total_len);
337        for chunk in self.chunks {
338            buffer.extend_from_slice(&chunk);
339        }
340        buffer.freeze()
341    }
342
343    /// Write all chunks to a writer using vectored I/O.
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if the write fails.
348    pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<usize> {
349        let slices = self.as_io_slices();
350        writer.write_vectored(&slices)
351    }
352}
353
354/// A borrowed view of bytes with lifetime tracking.
355///
356/// This provides a way to pass borrowed slices around without
357/// copying, while still allowing for owned data when needed.
358#[derive(Debug)]
359pub enum BorrowedView<'a> {
360    /// A borrowed slice.
361    Borrowed(&'a [u8]),
362    /// Owned bytes (for cases where borrowing isn't possible).
363    Owned(Bytes),
364}
365
366impl<'a> BorrowedView<'a> {
367    /// Create a borrowed view.
368    #[must_use]
369    pub const fn borrowed(data: &'a [u8]) -> Self {
370        Self::Borrowed(data)
371    }
372
373    /// Create an owned view.
374    #[must_use]
375    pub fn owned(data: impl Into<Bytes>) -> Self {
376        Self::Owned(data.into())
377    }
378
379    /// Get the length of the view.
380    #[must_use]
381    pub const fn len(&self) -> usize {
382        match self {
383            Self::Borrowed(b) => b.len(),
384            Self::Owned(b) => b.len(),
385        }
386    }
387
388    /// Check if the view is empty.
389    #[must_use]
390    pub const fn is_empty(&self) -> bool {
391        self.len() == 0
392    }
393
394    /// Get a slice of the view.
395    #[must_use]
396    pub fn as_slice(&self) -> &[u8] {
397        match self {
398            Self::Borrowed(b) => b,
399            Self::Owned(b) => b,
400        }
401    }
402
403    /// Convert to owned bytes.
404    ///
405    /// This may involve copying if the view is borrowed.
406    #[must_use]
407    pub fn into_owned(self) -> Bytes {
408        match self {
409            Self::Borrowed(b) => Bytes::copy_from_slice(b),
410            Self::Owned(b) => b,
411        }
412    }
413}
414
415impl Deref for BorrowedView<'_> {
416    type Target = [u8];
417
418    fn deref(&self) -> &Self::Target {
419        self.as_slice()
420    }
421}
422
423impl AsRef<[u8]> for BorrowedView<'_> {
424    fn as_ref(&self) -> &[u8] {
425        self.as_slice()
426    }
427}
428
429/// Trait for types that can provide a zero-copy view of their contents.
430pub trait ZeroCopySource {
431    /// Get a borrowed view of the data.
432    fn view(&self) -> BorrowedView<'_>;
433
434    /// Get the length of the data.
435    fn len(&self) -> usize {
436        self.view().len()
437    }
438
439    /// Check if the data is empty.
440    fn is_empty(&self) -> bool {
441        self.len() == 0
442    }
443}
444
445impl ZeroCopySource for [u8] {
446    fn view(&self) -> BorrowedView<'_> {
447        BorrowedView::borrowed(self)
448    }
449}
450
451impl ZeroCopySource for Vec<u8> {
452    fn view(&self) -> BorrowedView<'_> {
453        BorrowedView::borrowed(self)
454    }
455}
456
457impl ZeroCopySource for Bytes {
458    fn view(&self) -> BorrowedView<'_> {
459        BorrowedView::Owned(self.clone())
460    }
461}
462
463impl ZeroCopySource for BytesBuffer {
464    fn view(&self) -> BorrowedView<'_> {
465        BorrowedView::borrowed(&self.inner)
466    }
467}
468
469impl ZeroCopySource for str {
470    fn view(&self) -> BorrowedView<'_> {
471        BorrowedView::borrowed(self.as_bytes())
472    }
473}
474
475impl ZeroCopySource for String {
476    fn view(&self) -> BorrowedView<'_> {
477        BorrowedView::borrowed(self.as_bytes())
478    }
479}
480
481/// A read buffer that minimizes copies during async reads.
482///
483/// This is designed to work with Tokio's `ReadBuf` pattern while
484/// allowing for efficient buffer reuse.
485#[derive(Debug)]
486pub struct ReadPool {
487    buffers: Vec<BytesMut>,
488    buffer_size: usize,
489}
490
491impl ReadPool {
492    /// Create a new read pool.
493    #[must_use]
494    pub const fn new(buffer_size: usize) -> Self {
495        Self {
496            buffers: Vec::new(),
497            buffer_size,
498        }
499    }
500
501    /// Get a buffer from the pool, or create a new one.
502    pub fn acquire(&mut self) -> BytesMut {
503        self.buffers
504            .pop()
505            .unwrap_or_else(|| BytesMut::with_capacity(self.buffer_size))
506    }
507
508    /// Return a buffer to the pool for reuse.
509    pub fn release(&mut self, mut buffer: BytesMut) {
510        buffer.clear();
511        // Only keep buffers that haven't grown too large
512        if buffer.capacity() <= self.buffer_size * 2 {
513            self.buffers.push(buffer);
514        }
515    }
516
517    /// Clear the pool, releasing all buffers.
518    pub fn clear(&mut self) {
519        self.buffers.clear();
520    }
521
522    /// Get the number of buffers in the pool.
523    #[must_use]
524    pub const fn available(&self) -> usize {
525        self.buffers.len()
526    }
527}
528
529impl Default for ReadPool {
530    fn default() -> Self {
531        Self::new(8192) // 8KB default buffer size
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538
539    #[test]
540    fn bytes_buffer_basic() {
541        let mut buffer = BytesBuffer::new();
542        buffer.extend(b"hello");
543        buffer.extend(b" world");
544
545        assert_eq!(buffer.len(), 11);
546        assert_eq!(buffer.as_bytes(), b"hello world");
547    }
548
549    #[test]
550    fn bytes_buffer_slicing() {
551        let mut buffer = BytesBuffer::with_capacity(20);
552        buffer.extend(b"hello world");
553
554        let slice = buffer.slice(0..5);
555        assert_eq!(&slice[..], b"hello");
556
557        let slice = buffer.slice_ref(6..);
558        assert_eq!(&slice[..], b"world");
559    }
560
561    #[test]
562    fn bytes_buffer_find() {
563        let buffer = BytesBuffer::from("the quick brown fox");
564
565        assert_eq!(buffer.find(b"quick"), Some(4));
566        assert_eq!(buffer.find_str("fox"), Some(16));
567        assert_eq!(buffer.find(b"lazy"), None);
568    }
569
570    #[test]
571    fn bytes_buffer_head_tail() {
572        let buffer = BytesBuffer::from("hello world");
573
574        assert_eq!(buffer.head(5), b"hello");
575        assert_eq!(buffer.tail(5), b"world");
576        assert_eq!(buffer.head(100), b"hello world");
577        assert_eq!(buffer.tail(100), b"hello world");
578    }
579
580    #[test]
581    fn vec_writer_basic() {
582        let mut writer = VecWriter::new();
583        writer.push(b"hello".as_slice());
584        writer.push(b" ".as_slice());
585        writer.push(b"world".as_slice());
586
587        assert_eq!(writer.chunk_count(), 3);
588        assert_eq!(writer.len(), 11);
589
590        let bytes = writer.freeze();
591        assert_eq!(&bytes[..], b"hello world");
592    }
593
594    #[test]
595    fn vec_writer_io_slices() {
596        let mut writer = VecWriter::new();
597        writer.push_slice(b"hello");
598        writer.push_slice(b"world");
599
600        let slices = writer.as_io_slices();
601        assert_eq!(slices.len(), 2);
602    }
603
604    #[test]
605    fn borrowed_view() {
606        let data = b"hello world";
607        let borrowed = BorrowedView::borrowed(data);
608
609        assert_eq!(borrowed.len(), 11);
610        assert_eq!(borrowed.as_slice(), data);
611
612        let owned = borrowed.into_owned();
613        assert_eq!(&owned[..], data);
614    }
615
616    #[test]
617    fn zero_copy_source_trait() {
618        let vec: Vec<u8> = b"hello".to_vec();
619        let view = vec.view();
620        assert_eq!(view.len(), 5);
621
622        let string = "world".to_string();
623        let view = string.view();
624        assert_eq!(view.len(), 5);
625    }
626
627    #[test]
628    fn read_pool() {
629        let mut pool = ReadPool::new(4096);
630
631        assert_eq!(pool.available(), 0);
632
633        let buf1 = pool.acquire();
634        assert!(buf1.capacity() >= 4096);
635
636        let buf2 = pool.acquire();
637        pool.release(buf1);
638        assert_eq!(pool.available(), 1);
639
640        let buf3 = pool.acquire();
641        assert_eq!(pool.available(), 0);
642
643        pool.release(buf2);
644        pool.release(buf3);
645        assert_eq!(pool.available(), 2);
646    }
647
648    #[test]
649    fn write_trait() {
650        use std::io::Write;
651
652        let mut buffer = BytesBuffer::new();
653        write!(buffer, "hello {}", 42).unwrap();
654        assert_eq!(buffer.as_str_lossy(), "hello 42");
655    }
656}