1use std::collections::VecDeque;
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, Eq, PartialEq)]
6pub struct Limit {
7 pub start: usize,
9 pub end: usize,
11}
12
13impl Limit {
14 #[must_use]
18 pub const fn split_evenly(total_bytes: usize) -> Self {
19 let start = total_bytes / 2;
20 Self {
21 start,
22 end: total_bytes - start,
23 }
24 }
25}
26
27#[derive(Clone)]
29pub struct BoundedBuffer {
30 start: String,
31 end: VecDeque<u8>,
33 truncated: bool,
34 limit: Limit,
35}
36
37#[derive(Debug, Clone, Eq, PartialEq)]
41pub struct BufferContents {
42 pub start: String,
46
47 pub end: Option<String>,
52}
53
54impl BoundedBuffer {
55 #[must_use]
57 pub fn new(limit: Limit) -> Self {
58 Self {
59 start: String::new(),
60 end: VecDeque::new(),
61 truncated: false,
62 limit,
63 }
64 }
65
66 pub fn take(&mut self) -> BufferContents {
68 let start = std::mem::take(&mut self.start);
69
70 if std::mem::take(&mut self.truncated) {
71 let end = Vec::from(std::mem::take(&mut self.end));
72 if cfg!(debug_assertions) {
73 std::str::from_utf8(&end).expect("invalid utf-8: this is UB!");
74 }
75
76 #[allow(
77 unsafe_code,
78 reason = "unchecked conversion is much faster (O(1) vs O(n)) and is guaranteed by \
79 this type's invariants; every place where `self.end` is modified is \
80 accompanied by a justification for why the invariant is necessarily \
81 upheld, and we perform an explicit check in debug mode for extra \
82 assurance"
83 )]
84 let end = unsafe { String::from_utf8_unchecked(end) };
86 BufferContents {
87 start,
88 end: Some(end),
89 }
90 } else {
91 let mut start = start.into_bytes();
92 start.extend(&self.end);
93 self.end.clear();
94
95 if cfg!(debug_assertions) {
96 std::str::from_utf8(&start).expect("invalid utf-8: this is UB!");
97 }
98
99 #[allow(
100 unsafe_code,
101 reason = "unchecked conversion is much faster (O(1) vs O(n)) and is guaranteed by \
102 this type's invariants; every place where `self.end` is modified is \
103 accompanied by a justification for why the invariant is necessarily \
104 upheld, and we perform an explicit check in debug mode for extra \
105 assurance"
106 )]
107 let start = unsafe { String::from_utf8_unchecked(start) };
111 BufferContents { start, end: None }
112 }
113 }
114
115 pub fn clear(&mut self) {
116 self.start.clear();
117 self.end.clear();
118 self.truncated = false;
119 }
120}
121
122impl fmt::Write for BoundedBuffer {
123 fn write_str(&mut self, s: &str) -> fmt::Result {
124 let s = if self.truncated || !self.end.is_empty() {
125 s
126 } else {
127 let available = self.limit.start - self.start.len();
128 if available >= s.len() {
129 self.start.push_str(s);
130 return Ok(());
131 }
132 let (start, end) = s.split_at(s.floor_char_boundary(available));
133 self.start.push_str(start);
134 end
135 };
136
137 if s.len() > self.limit.end {
138 self.truncated = true;
139 self.end.clear();
141 let tail = &s[s.ceil_char_boundary(s.len() - self.limit.end)..];
142 self.end.extend(tail.as_bytes());
144 return Ok(());
145 }
146
147 let keep = self.limit.end - s.len();
148 if self.end.len() > keep {
149 self.truncated = true;
150 let keep_start = self.end.len() - keep;
151
152 let offset = self.end.iter().skip(keep_start).take(char::MAX_LEN_UTF8).position(|&b| {
154 !matches!(std::str::from_utf8(&[b]), Err(e) if e.error_len().is_some())
162 });
163
164 let keep_start = if let Some(offset) = offset {
166 keep_start + offset
167 } else {
168 self.end.len()
169 };
170
171 self.end.drain(..keep_start);
174 }
175
176 self.end.extend(s.as_bytes());
180 Ok(())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use std::fmt::Write as _;
187
188 use proptest::prelude::*;
189 use rstest::{fixture, rstest};
190
191 use super::{BoundedBuffer, BufferContents, Limit};
192
193 const START: usize = 5;
194 const END: usize = 3;
195 const LIMIT: Limit = Limit {
196 start: START,
197 end: END,
198 };
199
200 #[fixture]
203 fn buffer(#[default(START)] start: usize, #[default(END)] end: usize) -> BoundedBuffer {
204 BoundedBuffer::new(Limit { start, end })
205 }
206
207 fn filled(limit: Limit, writes: &[impl AsRef<str>]) -> BufferContents {
209 let mut buffer = BoundedBuffer::new(limit);
210 for write in writes {
211 buffer.write_str(write.as_ref()).expect("writing never fails");
212 }
213 buffer.take()
214 }
215
216 fn parts(contents: &BufferContents) -> (&str, Option<&str>) {
218 (&contents.start, contents.end.as_deref())
219 }
220
221 #[rstest]
224 #[case::empty(&[], "")]
225 #[case::one_write(&["abc"], "abc")]
226 #[case::several_writes(&["ab", "cd"], "abcd")]
227 #[case::exactly_the_limit(&["abcdefgh"], "abcdefgh")]
229 #[case::exactly_the_limit_in_pieces(&["abcd", "efgh"], "abcdefgh")]
230 #[case::spills_into_the_end_portion(&["abcdefg"], "abcdefg")]
232 fn keeps_everything_that_fits(#[case] writes: &[&str], #[case] expected: &str) {
233 let contents = filled(LIMIT, writes);
234 assert_eq!(
235 parts(&contents),
236 (expected, None),
237 "nothing was dropped, so it all belongs in `start`",
238 );
239 }
240
241 #[rstest]
244 #[case::one_byte_over(&["abcdefghi"], "abcde", "ghi")]
246 #[case::far_over(&["abcdefghijklmnop"], "abcde", "nop")]
247 #[case::overflows_part_way(&["abcdef", "ghi"], "abcde", "ghi")]
249 #[case::byte_at_a_time(&["a", "b", "c", "d", "e", "f", "g", "h", "i"], "abcde", "ghi")]
250 #[case::full_then_more(&["abcdefgh", "i"], "abcde", "ghi")]
251 fn keeps_the_start_and_the_end(
252 #[case] writes: &[&str],
253 #[case] start: &str,
254 #[case] end: &str,
255 ) {
256 let contents = filled(LIMIT, writes);
257 assert_eq!(parts(&contents), (start, Some(end)));
258 }
259
260 #[rstest]
264 fn one_huge_write_is_capped_like_many_small_ones() {
265 let all = "x".repeat(10_000);
266
267 let at_once = filled(LIMIT, std::slice::from_ref(&all));
268 let in_pieces = filled(
269 LIMIT,
270 &all.as_bytes()
271 .chunks(7)
272 .map(|c| std::str::from_utf8(c).expect("ascii").to_string())
273 .collect::<Vec<_>>(),
274 );
275
276 assert_eq!(at_once, in_pieces);
277 assert_eq!(at_once.start.len(), START);
278 assert_eq!(at_once.end.as_deref().map(str::len), Some(END));
279 }
280
281 #[rstest]
282 fn the_end_portion_is_a_sliding_window(mut buffer: BoundedBuffer) {
283 buffer.write_str("abcde").expect("writing never fails");
287 let expected = [
288 ("fgh", ("abcdefgh", None)),
289 ("ij", ("abcde", Some("hij"))),
290 ("klmn", ("abcde", Some("lmn"))),
291 ];
292 for (write, expected) in expected {
293 buffer.write_str(write).expect("writing never fails");
294 let contents = buffer.clone().take();
295 assert_eq!(parts(&contents), expected, "after writing {write:?}");
296 }
297 }
298
299 #[rstest]
302 #[case::no_end_kept(Limit { start: 4, end: 0 }, "abcd", Some(""))]
303 #[case::no_start_kept(Limit { start: 0, end: 4 }, "", Some("wxyz"))]
304 #[case::nothing_kept(Limit { start: 0, end: 0 }, "", Some(""))]
305 fn zero_sided_limits(#[case] limit: Limit, #[case] start: &str, #[case] end: Option<&str>) {
306 let contents = filled(limit, &["abcdefghijklmnopqrstuvwxyz"]);
307 assert_eq!(parts(&contents), (start, end));
308 }
309
310 #[rstest]
311 fn a_zero_limit_keeps_nothing_but_still_reports_the_cut() {
312 let limit = Limit { start: 0, end: 0 };
313 assert_eq!(filled(limit, &[""]), BufferContents {
314 start: String::new(),
315 end: None
316 });
317 assert_eq!(filled(limit, &["a"]), BufferContents {
318 start: String::new(),
319 end: Some(String::new()),
320 });
321 }
322
323 #[rstest]
326 #[case::does_not_fit_in_the_start(Limit { start: 6, end: 8 }, &["abcd", "🦀"], "abcd🦀", None)]
329 #[case::does_not_fit_in_the_end(Limit { start: 2, end: 3 }, &["ab", "🦀z"], "ab", Some("z"))]
331 #[case::fits_the_end_exactly(Limit { start: 2, end: 4 }, &["ab", "cd🦀"], "ab", Some("🦀"))]
332 fn never_splits_a_character(
333 #[case] limit: Limit,
334 #[case] writes: &[&str],
335 #[case] start: &str,
336 #[case] end: Option<&str>,
337 ) {
338 let contents = filled(limit, writes);
341 assert_eq!(parts(&contents), (start, end));
342 }
343
344 #[rstest]
347 fn take_hands_over_the_contents_and_resets(mut buffer: BoundedBuffer) {
348 buffer.write_str("abcdefghi").expect("writing never fails");
349
350 let taken = buffer.take();
351 assert_eq!(parts(&taken), ("abcde", Some("ghi")));
352
353 assert_eq!(buffer.take(), BufferContents {
355 start: String::new(),
356 end: None
357 });
358 buffer.write_str("xy").expect("writing never fails");
359 assert_eq!(parts(&buffer.take()), ("xy", None));
360 }
361
362 #[rstest]
363 fn clear_forgets_the_dropped_middle(mut buffer: BoundedBuffer) {
364 buffer.write_str("abcdefghi").expect("writing never fails");
365 buffer.clear();
366
367 buffer.write_str("xy").expect("writing never fails");
368 assert_eq!(
369 parts(&buffer.take()),
370 ("xy", None),
371 "a cleared buffer must not still claim its middle was dropped",
372 );
373 }
374
375 #[rstest]
376 fn writes_always_succeed(mut buffer: BoundedBuffer) {
377 for write in ["abcdefghij", "more", "and more"] {
380 assert!(buffer.write_str(write).is_ok(), "{write:?} was refused");
381 }
382 assert_eq!(parts(&buffer.take()), ("abcde", Some("ore"))); }
384
385 prop_compose! {
388 fn limit_and_writes()(
390 start in 0usize..12,
391 end in 0usize..12,
392 writes in prop::collection::vec("(?s).{0,8}", 0..8),
393 ) -> (Limit, Vec<String>) {
394 (Limit { start, end }, writes)
395 }
396 }
397
398 proptest! {
399 #[rstest]
403 fn never_exceeds_the_limit((limit, writes) in limit_and_writes()) {
404 let contents = filled(limit, &writes);
405 if let Some(end) = &contents.end {
406 prop_assert!(contents.start.len() <= limit.start);
407 prop_assert!(end.len() <= limit.end);
408 } else {
409 prop_assert!(contents.start.len() <= limit.start + limit.end);
410 }
411 }
412
413 #[rstest]
416 fn reports_a_dropped_middle_exactly_when_data_was_dropped(
417 (limit, writes) in limit_and_writes(),
418 ) {
419 let all = writes.concat();
420 let contents = filled(limit, &writes);
421 let kept = contents.start.len() + contents.end.as_ref().map_or(0, String::len);
422 prop_assert_eq!(contents.end.is_some(), kept < all.len());
423 }
424
425 #[rstest]
428 fn the_limits_decide_whether_anything_is_dropped((limit, writes) in limit_and_writes()) {
429 let all = writes.concat();
430 let contents = filled(limit, &writes);
431 if all.len() <= limit.start {
432 prop_assert_eq!(contents.end, None);
433 } else if all.len() > limit.start + limit.end {
434 prop_assert!(contents.end.is_some());
435 }
436 }
437
438 #[rstest]
441 fn keeps_a_prefix_and_a_suffix((limit, writes) in limit_and_writes()) {
442 let all = writes.concat();
443 let contents = filled(limit, &writes);
444 prop_assert!(all.starts_with(&contents.start));
445 if let Some(end) = &contents.end {
446 prop_assert!(all.ends_with(end.as_str()));
447 prop_assert!(contents.start.len() + end.len() <= all.len());
449 } else {
450 prop_assert_eq!(&contents.start, &all);
451 }
452 }
453
454 #[rstest]
457 fn keeps_as_much_as_it_can((limit, writes) in limit_and_writes()) {
458 let all = writes.concat();
459 let contents = filled(limit, &writes);
460 if let Some(end) = &contents.end {
461 prop_assert!(contents.start.len() + char::MAX_LEN_UTF8 > limit.start.min(all.len()));
462 prop_assert!(end.len() + char::MAX_LEN_UTF8 > limit.end.min(all.len()));
463 }
464 }
465
466 #[rstest]
468 fn chunking_does_not_matter((limit, writes) in limit_and_writes()) {
469 prop_assert_eq!(filled(limit, &writes), filled(limit, &[writes.concat()]));
470 }
471
472 #[rstest]
474 fn writes_never_fail((limit, writes) in limit_and_writes()) {
475 let mut buffer = BoundedBuffer::new(limit);
476 for write in &writes {
477 prop_assert!(buffer.write_str(write).is_ok());
478 }
479 }
480
481 #[rstest]
483 fn take_resets_the_buffer((limit, writes) in limit_and_writes()) {
484 let mut buffer = BoundedBuffer::new(limit);
485 for write in &writes {
486 let _ = buffer.write_str(write);
487 }
488 let _ = buffer.take();
489
490 for write in &writes {
491 let _ = buffer.write_str(write);
492 }
493 prop_assert_eq!(buffer.take(), filled(limit, &writes));
494 }
495
496 #[rstest]
498 fn clear_resets_the_buffer((limit, writes) in limit_and_writes()) {
499 let mut buffer = BoundedBuffer::new(limit);
500 for write in &writes {
501 let _ = buffer.write_str(write);
502 }
503 buffer.clear();
504
505 for write in &writes {
506 let _ = buffer.write_str(write);
507 }
508 prop_assert_eq!(buffer.take(), filled(limit, &writes));
509 }
510 }
511}