Skip to main content

ante_exec/
buffer.rs

1use std::collections::VecDeque;
2
3/// Byte buffer that preserves the beginning and end, dropping the middle.
4#[derive(Debug, Clone)]
5pub struct HeadTailBuffer {
6    max_bytes: usize,
7    head: VecDeque<Vec<u8>>,
8    tail: VecDeque<Vec<u8>>,
9    head_bytes: usize,
10    tail_bytes: usize,
11    omitted_bytes: usize,
12}
13
14impl HeadTailBuffer {
15    pub fn new(max_bytes: usize) -> Self {
16        Self {
17            max_bytes,
18            head: VecDeque::new(),
19            tail: VecDeque::new(),
20            head_bytes: 0,
21            tail_bytes: 0,
22            omitted_bytes: 0,
23        }
24    }
25
26    pub fn push_chunk(&mut self, chunk: Vec<u8>) {
27        if chunk.is_empty() {
28            return;
29        }
30
31        if self.max_bytes == 0 {
32            self.omitted_bytes += chunk.len();
33            return;
34        }
35
36        let mut chunk = chunk;
37        let head_budget = self.max_bytes / 2;
38        let tail_budget = self.max_bytes - head_budget;
39
40        if self.head_bytes < head_budget {
41            let to_head = (head_budget - self.head_bytes).min(chunk.len());
42            let head_part = chunk.drain(..to_head).collect::<Vec<_>>();
43            self.head_bytes += head_part.len();
44            self.head.push_back(head_part);
45        }
46
47        if chunk.is_empty() {
48            return;
49        }
50
51        if tail_budget == 0 {
52            self.omitted_bytes += chunk.len();
53            return;
54        }
55
56        if chunk.len() > tail_budget {
57            self.omitted_bytes += chunk.len() - tail_budget;
58            let split_at = chunk.len() - tail_budget;
59            chunk = chunk.split_off(split_at);
60
61            self.omitted_bytes += self.tail_bytes;
62            self.tail.clear();
63            self.tail_bytes = 0;
64        } else {
65            self.trim_tail_for(chunk.len(), tail_budget);
66        }
67
68        self.tail_bytes += chunk.len();
69        self.tail.push_back(chunk);
70    }
71
72    pub fn snapshot(&self) -> Vec<Vec<u8>> {
73        let mut out = Vec::with_capacity(self.head.len() + self.tail.len());
74        out.extend(self.head.iter().cloned());
75        out.extend(self.tail.iter().cloned());
76        out
77    }
78
79    pub fn to_bytes(&self) -> Vec<u8> {
80        let mut out = Vec::with_capacity(self.retained_bytes());
81        self.append_retained_to(&mut out, None);
82        out
83    }
84
85    pub fn to_bytes_with_omission_marker(&self, marker: &[u8]) -> Vec<u8> {
86        let marker_bytes = if self.omitted_bytes > 0 { marker.len() } else { 0 };
87        let mut out = Vec::with_capacity(self.retained_bytes() + marker_bytes);
88        self.append_retained_to(&mut out, Some(marker));
89
90        out
91    }
92
93    fn append_retained_to(&self, out: &mut Vec<u8>, marker: Option<&[u8]>) {
94        for chunk in &self.head {
95            out.extend_from_slice(chunk);
96        }
97        if self.omitted_bytes > 0
98            && let Some(marker) = marker
99        {
100            out.extend_from_slice(marker);
101        }
102        for chunk in &self.tail {
103            out.extend_from_slice(chunk);
104        }
105    }
106
107    /// Return up to `max_bytes` from the end of the retained content
108    /// (head followed by tail), preserving order. Avoids allocating the full
109    /// buffer when only a suffix is needed.
110    pub fn suffix_bytes(&self, max_bytes: usize) -> Vec<u8> {
111        let take = max_bytes.min(self.retained_bytes());
112        if take == 0 {
113            return Vec::new();
114        }
115
116        let mut out = vec![0u8; take];
117        let mut written = 0;
118        for chunk in self.tail.iter().rev().chain(self.head.iter().rev()) {
119            if written == take {
120                break;
121            }
122            let remaining = take - written;
123            let n = chunk.len().min(remaining);
124            let chunk_start = chunk.len() - n;
125            let dst_end = take - written;
126            let dst_start = dst_end - n;
127            out[dst_start..dst_end].copy_from_slice(&chunk[chunk_start..]);
128            written += n;
129        }
130
131        out
132    }
133
134    pub fn drain(&mut self) -> Vec<Vec<u8>> {
135        let mut out = Vec::with_capacity(self.head.len() + self.tail.len());
136        while let Some(chunk) = self.head.pop_front() {
137            out.push(chunk);
138        }
139        while let Some(chunk) = self.tail.pop_front() {
140            out.push(chunk);
141        }
142
143        self.clear();
144        out
145    }
146
147    pub fn drain_into(&mut self, target: &mut HeadTailBuffer) {
148        while let Some(chunk) = self.head.pop_front() {
149            target.push_chunk(chunk);
150        }
151        while let Some(chunk) = self.tail.pop_front() {
152            target.push_chunk(chunk);
153        }
154
155        self.clear();
156    }
157
158    fn clear(&mut self) {
159        self.head.clear();
160        self.tail.clear();
161        self.head_bytes = 0;
162        self.tail_bytes = 0;
163        self.omitted_bytes = 0;
164    }
165
166    pub fn retained_bytes(&self) -> usize {
167        self.head_bytes + self.tail_bytes
168    }
169
170    pub fn omitted_bytes(&self) -> usize {
171        self.omitted_bytes
172    }
173
174    /// Length in bytes of the retained head (the split point: `to_bytes()` is
175    /// the head followed by the tail, with `omitted_bytes` dropped between).
176    pub fn head_bytes(&self) -> usize {
177        self.head_bytes
178    }
179
180    fn trim_tail_for(&mut self, incoming_bytes: usize, tail_budget: usize) {
181        while self.tail_bytes + incoming_bytes > tail_budget {
182            let overflow = self.tail_bytes + incoming_bytes - tail_budget;
183            let Some(front) = self.tail.front_mut() else {
184                break;
185            };
186
187            if front.len() <= overflow {
188                let removed = front.len();
189                self.tail.pop_front();
190                self.tail_bytes -= removed;
191                self.omitted_bytes += removed;
192                continue;
193            }
194
195            front.drain(..overflow);
196            self.tail_bytes -= overflow;
197            self.omitted_bytes += overflow;
198            break;
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::HeadTailBuffer;
206    use pretty_assertions::assert_eq;
207
208    #[test]
209    fn retains_all_output_when_under_budget() {
210        let mut buffer = HeadTailBuffer::new(16);
211        buffer.push_chunk(b"hello".to_vec());
212        buffer.push_chunk(b" world".to_vec());
213
214        assert_eq!(buffer.to_bytes(), b"hello world".to_vec());
215        assert_eq!(buffer.retained_bytes(), 11);
216        assert_eq!(buffer.omitted_bytes(), 0);
217    }
218
219    #[test]
220    fn preserves_head_and_tail_when_over_budget() {
221        let mut buffer = HeadTailBuffer::new(12);
222        buffer.push_chunk(b"abcdef".to_vec());
223        buffer.push_chunk(b"ghij".to_vec());
224        buffer.push_chunk(b"klmnop".to_vec());
225
226        assert_eq!(buffer.to_bytes(), b"abcdefklmnop".to_vec());
227        assert_eq!(buffer.retained_bytes(), 12);
228        assert_eq!(buffer.omitted_bytes(), 4);
229    }
230
231    #[test]
232    fn large_chunk_keeps_only_tail_suffix() {
233        let mut buffer = HeadTailBuffer::new(10);
234        buffer.push_chunk(b"abcde".to_vec());
235        buffer.push_chunk(b"0123456789".to_vec());
236
237        assert_eq!(buffer.to_bytes(), b"abcde56789".to_vec());
238        assert_eq!(buffer.retained_bytes(), 10);
239        assert_eq!(buffer.omitted_bytes(), 5);
240    }
241
242    #[test]
243    fn omission_marker_is_inserted_between_head_and_tail() {
244        let mut buffer = HeadTailBuffer::new(12);
245        buffer.push_chunk(b"abcdef".to_vec());
246        buffer.push_chunk(b"ghij".to_vec());
247        buffer.push_chunk(b"klmnop".to_vec());
248
249        assert_eq!(
250            buffer.to_bytes_with_omission_marker(b"\n...[truncated]...\n"),
251            b"abcdef\n...[truncated]...\nklmnop".to_vec()
252        );
253    }
254
255    #[test]
256    fn omission_marker_is_not_inserted_without_omitted_bytes() {
257        let mut buffer = HeadTailBuffer::new(12);
258        buffer.push_chunk(b"abcdef".to_vec());
259
260        assert_eq!(
261            buffer.to_bytes_with_omission_marker(b"\n...[truncated]...\n"),
262            b"abcdef".to_vec()
263        );
264    }
265
266    #[test]
267    fn suffix_bytes_returns_last_n_across_chunks() {
268        let mut buffer = HeadTailBuffer::new(64);
269        buffer.push_chunk(b"abc".to_vec());
270        buffer.push_chunk(b"defgh".to_vec());
271        buffer.push_chunk(b"ij".to_vec());
272
273        assert_eq!(buffer.suffix_bytes(0), Vec::<u8>::new());
274        assert_eq!(buffer.suffix_bytes(1), b"j".to_vec());
275        assert_eq!(buffer.suffix_bytes(4), b"fghij".to_vec()[1..].to_vec());
276        assert_eq!(buffer.suffix_bytes(100), b"abcdefghij".to_vec());
277    }
278
279    #[test]
280    fn suffix_bytes_spans_head_when_tail_is_short() {
281        let mut buffer = HeadTailBuffer::new(12);
282        buffer.push_chunk(b"abcdef".to_vec());
283        buffer.push_chunk(b"ghij".to_vec());
284        buffer.push_chunk(b"klmnop".to_vec());
285
286        // Retained content is "abcdef" ++ "klmnop"; suffix should walk into the head.
287        assert_eq!(buffer.suffix_bytes(8), b"efklmnop".to_vec());
288    }
289
290    #[test]
291    fn zero_budget_omits_everything() {
292        let mut buffer = HeadTailBuffer::new(0);
293        buffer.push_chunk(b"abcdef".to_vec());
294
295        assert_eq!(buffer.to_bytes(), Vec::<u8>::new());
296        assert_eq!(buffer.retained_bytes(), 0);
297        assert_eq!(buffer.omitted_bytes(), 6);
298    }
299
300    #[test]
301    fn drain_returns_chunks_and_resets_state() {
302        let mut buffer = HeadTailBuffer::new(8);
303        buffer.push_chunk(b"abcd".to_vec());
304        buffer.push_chunk(b"efgh".to_vec());
305
306        let drained = buffer.drain();
307        let drained_bytes = drained.concat();
308        assert_eq!(drained_bytes, b"abcdefgh".to_vec());
309
310        assert_eq!(buffer.retained_bytes(), 0);
311        assert_eq!(buffer.omitted_bytes(), 0);
312        assert_eq!(buffer.snapshot(), Vec::<Vec<u8>>::new());
313    }
314
315    #[test]
316    fn drain_into_moves_chunks_without_intermediate_snapshot() {
317        let mut source = HeadTailBuffer::new(8);
318        let mut target = HeadTailBuffer::new(16);
319        source.push_chunk(b"abcd".to_vec());
320        source.push_chunk(b"efgh".to_vec());
321
322        source.drain_into(&mut target);
323
324        assert_eq!(target.to_bytes(), b"abcdefgh".to_vec());
325        assert_eq!(source.retained_bytes(), 0);
326        assert_eq!(source.omitted_bytes(), 0);
327        assert_eq!(source.snapshot(), Vec::<Vec<u8>>::new());
328    }
329}