rust_expect/expect/
buffer.rs

1//! Ring buffer implementation for expect operations.
2//!
3//! This module provides a ring buffer optimized for terminal output processing,
4//! supporting efficient append, search, and extraction operations.
5
6use std::collections::VecDeque;
7use std::fmt;
8
9/// Default buffer capacity (1 MB).
10pub const DEFAULT_CAPACITY: usize = 1024 * 1024;
11
12/// A ring buffer for accumulating terminal output.
13///
14/// The buffer supports efficient append operations and automatically
15/// discards oldest data when the maximum size is reached.
16#[derive(Clone)]
17pub struct RingBuffer {
18    /// The underlying storage.
19    data: VecDeque<u8>,
20    /// Maximum capacity.
21    max_size: usize,
22    /// Total bytes written (may exceed `max_size` due to wrapping).
23    total_written: usize,
24    /// Bytes discarded due to overflow.
25    bytes_discarded: usize,
26}
27
28impl RingBuffer {
29    /// Create a new ring buffer with the specified maximum size.
30    #[must_use]
31    pub fn new(max_size: usize) -> Self {
32        Self {
33            data: VecDeque::with_capacity(max_size.min(DEFAULT_CAPACITY)),
34            max_size,
35            total_written: 0,
36            bytes_discarded: 0,
37        }
38    }
39
40    /// Create a new ring buffer with default capacity.
41    #[must_use]
42    pub fn with_default_capacity() -> Self {
43        Self::new(DEFAULT_CAPACITY)
44    }
45
46    /// Append data to the buffer.
47    ///
48    /// If the buffer would exceed its maximum size, oldest data is discarded.
49    pub fn append(&mut self, data: &[u8]) {
50        self.total_written += data.len();
51
52        // If new data alone exceeds max_size, only keep the tail
53        if data.len() >= self.max_size {
54            self.data.clear();
55            let start = data.len() - self.max_size;
56            self.data.extend(&data[start..]);
57            self.bytes_discarded += data.len() - self.max_size;
58            return;
59        }
60
61        // Calculate how much space we need to free
62        let needed_space = (self.data.len() + data.len()).saturating_sub(self.max_size);
63        if needed_space > 0 {
64            self.bytes_discarded += needed_space;
65            for _ in 0..needed_space {
66                self.data.pop_front();
67            }
68        }
69
70        self.data.extend(data);
71    }
72
73    /// Get the current buffer contents as a contiguous slice.
74    ///
75    /// Note: This may need to reallocate if the buffer wraps around.
76    #[must_use]
77    pub fn as_slice(&mut self) -> &[u8] {
78        self.data.make_contiguous()
79    }
80
81    /// Get the current buffer contents as a string (lossy UTF-8 conversion).
82    #[must_use]
83    pub fn as_str_lossy(&mut self) -> String {
84        String::from_utf8_lossy(self.as_slice()).into_owned()
85    }
86
87    /// Get the current length of the buffer.
88    #[must_use]
89    pub fn len(&self) -> usize {
90        self.data.len()
91    }
92
93    /// Check if the buffer is empty.
94    #[must_use]
95    pub fn is_empty(&self) -> bool {
96        self.data.is_empty()
97    }
98
99    /// Get the maximum size of the buffer.
100    #[must_use]
101    pub const fn max_size(&self) -> usize {
102        self.max_size
103    }
104
105    /// Get the total bytes written to the buffer.
106    #[must_use]
107    pub const fn total_written(&self) -> usize {
108        self.total_written
109    }
110
111    /// Get the number of bytes that have been discarded due to overflow.
112    #[must_use]
113    pub const fn bytes_discarded(&self) -> usize {
114        self.bytes_discarded
115    }
116
117    /// Clear the buffer.
118    pub fn clear(&mut self) {
119        self.data.clear();
120    }
121
122    /// Find a byte sequence in the buffer.
123    ///
124    /// Returns the position of the first match.
125    #[must_use]
126    pub fn find(&mut self, needle: &[u8]) -> Option<usize> {
127        if needle.is_empty() {
128            return Some(0);
129        }
130        if needle.len() > self.data.len() {
131            return None;
132        }
133
134        let data = self.as_slice();
135        data.windows(needle.len())
136            .position(|window| window == needle)
137    }
138
139    /// Find a string in the buffer.
140    #[must_use]
141    pub fn find_str(&mut self, needle: &str) -> Option<usize> {
142        self.find(needle.as_bytes())
143    }
144
145    /// Consume data up to and including the specified position.
146    ///
147    /// Returns the consumed data.
148    pub fn consume(&mut self, end: usize) -> Vec<u8> {
149        let end = end.min(self.data.len());
150        self.data.drain(..end).collect()
151    }
152
153    /// Consume data up to (but not including) the specified position.
154    ///
155    /// Returns the consumed data as a string (lossy conversion).
156    pub fn consume_before(&mut self, pos: usize) -> String {
157        let data = self.consume(pos);
158        String::from_utf8_lossy(&data).into_owned()
159    }
160
161    /// Consume data up to and including a pattern match.
162    ///
163    /// Returns (`before_match`, `matched_text`) if found.
164    pub fn consume_until(&mut self, needle: &str) -> Option<(String, String)> {
165        let pos = self.find_str(needle)?;
166        let before = self.consume_before(pos);
167        let matched = self.consume(needle.len());
168        Some((before, String::from_utf8_lossy(&matched).into_owned()))
169    }
170
171    /// Get a slice of the last N bytes in the buffer.
172    #[must_use]
173    pub fn tail(&mut self, n: usize) -> Vec<u8> {
174        let data = self.as_slice();
175        let start = data.len().saturating_sub(n);
176        data[start..].to_vec()
177    }
178
179    /// Get a slice of the first N bytes in the buffer.
180    #[must_use]
181    pub fn head(&mut self, n: usize) -> Vec<u8> {
182        let data = self.as_slice();
183        let end = n.min(data.len());
184        data[..end].to_vec()
185    }
186
187    /// Search within a window at the end of the buffer.
188    ///
189    /// This is more efficient for large buffers when patterns
190    /// are expected near the end.
191    #[must_use]
192    pub fn find_in_tail(&mut self, needle: &[u8], window_size: usize) -> Option<usize> {
193        let data = self.as_slice();
194        let search_start = data.len().saturating_sub(window_size);
195        let search_data = &data[search_start..];
196
197        search_data
198            .windows(needle.len())
199            .position(|w| w == needle)
200            .map(|pos| search_start + pos)
201    }
202
203    /// Apply a function to search the buffer contents.
204    ///
205    /// This is useful for complex pattern matching like regex.
206    pub fn search<F, R>(&mut self, f: F) -> R
207    where
208        F: FnOnce(&str) -> R,
209    {
210        let s = self.as_str_lossy();
211        f(&s)
212    }
213}
214
215impl Default for RingBuffer {
216    fn default() -> Self {
217        Self::with_default_capacity()
218    }
219}
220
221impl fmt::Debug for RingBuffer {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        f.debug_struct("RingBuffer")
224            .field("len", &self.len())
225            .field("max_size", &self.max_size)
226            .field("total_written", &self.total_written)
227            .field("bytes_discarded", &self.bytes_discarded)
228            .finish()
229    }
230}
231
232impl std::io::Write for RingBuffer {
233    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
234        self.append(buf);
235        Ok(buf.len())
236    }
237
238    fn flush(&mut self) -> std::io::Result<()> {
239        Ok(())
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn basic_append() {
249        let mut buf = RingBuffer::new(100);
250        buf.append(b"hello");
251        assert_eq!(buf.len(), 5);
252        assert_eq!(buf.as_slice(), b"hello");
253    }
254
255    #[test]
256    fn overflow_discards_oldest() {
257        let mut buf = RingBuffer::new(10);
258        buf.append(b"12345");
259        buf.append(b"67890");
260        buf.append(b"abc");
261
262        assert_eq!(buf.len(), 10);
263        // After overflow, we should have the last 10 bytes
264        assert_eq!(buf.as_str_lossy(), "4567890abc");
265    }
266
267    #[test]
268    fn find_pattern() {
269        let mut buf = RingBuffer::new(100);
270        buf.append(b"hello world");
271        assert_eq!(buf.find(b"world"), Some(6));
272        assert_eq!(buf.find(b"foo"), None);
273    }
274
275    #[test]
276    fn consume_until() {
277        let mut buf = RingBuffer::new(100);
278        buf.append(b"login: username");
279        let result = buf.consume_until("login:");
280        assert!(result.is_some());
281        let (before, matched) = result.unwrap();
282        assert_eq!(before, "");
283        assert_eq!(matched, "login:");
284        assert_eq!(buf.as_str_lossy(), " username");
285    }
286
287    #[test]
288    fn tail_and_head() {
289        let mut buf = RingBuffer::new(100);
290        buf.append(b"hello world");
291        assert_eq!(buf.tail(5), b"world".to_vec());
292        assert_eq!(buf.head(5), b"hello".to_vec());
293    }
294
295    #[test]
296    fn find_in_tail() {
297        let mut buf = RingBuffer::new(100);
298        buf.append(b"the quick brown fox jumps over the lazy dog");
299        // Should find "lazy" in the last 20 bytes
300        assert!(buf.find_in_tail(b"lazy", 20).is_some());
301        // Should not find "quick" in the last 20 bytes
302        assert!(buf.find_in_tail(b"quick", 20).is_none());
303    }
304
305    #[test]
306    fn write_trait() {
307        use std::io::Write;
308
309        let mut buf = RingBuffer::new(100);
310        write!(buf, "hello world").unwrap();
311        assert_eq!(buf.as_str_lossy(), "hello world");
312    }
313}