1use std::collections::VecDeque;
2
3#[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 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 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 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}