sim_lib_net_core/line.rs
1//! Incremental newline framing (LineDecoder).
2
3/// Incremental newline framer.
4///
5/// Accumulates pushed bytes and yields complete lines split on `\n`, stripping
6/// a single trailing `\r` from each (so both `\n` and `\r\n` framing work). A
7/// partial final line with no terminating `\n` stays buffered across pushes
8/// until either a later push completes it or [`flush`](LineDecoder::flush) is
9/// called.
10///
11/// This is the framing half of `sim-lib-agent-runner-http`'s old per-decoder
12/// `line_buffer` loop, lifted into one shared, tested type.
13#[derive(Debug, Default)]
14pub struct LineDecoder {
15 buf: Vec<u8>,
16}
17
18impl LineDecoder {
19 /// Create an empty decoder.
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 /// Append `bytes` and return every line completed by a `\n`. The trailing
25 /// `\r` of a `\r\n` pair is stripped. Bytes after the last `\n` remain
26 /// buffered.
27 pub fn push(&mut self, bytes: &[u8]) -> Vec<Vec<u8>> {
28 self.buf.extend_from_slice(bytes);
29 let mut lines = Vec::new();
30 while let Some(index) = self.buf.iter().position(|byte| *byte == b'\n') {
31 let mut line = self.buf.drain(..=index).collect::<Vec<u8>>();
32 line.pop(); // drop the '\n'
33 if line.last() == Some(&b'\r') {
34 line.pop();
35 }
36 lines.push(line);
37 }
38 lines
39 }
40
41 /// Take any buffered remainder (an unterminated final line). Returns `None`
42 /// when nothing is buffered. A single trailing `\r` is stripped to match
43 /// [`push`](LineDecoder::push).
44 pub fn flush(&mut self) -> Option<Vec<u8>> {
45 if self.buf.is_empty() {
46 return None;
47 }
48 let mut line = std::mem::take(&mut self.buf);
49 if line.last() == Some(&b'\r') {
50 line.pop();
51 }
52 Some(line)
53 }
54
55 /// Number of bytes currently buffered as a partial line.
56 pub fn buffered_len(&self) -> usize {
57 self.buf.len()
58 }
59
60 /// Borrow the bytes currently buffered as a partial (unterminated) line.
61 pub fn buffered(&self) -> &[u8] {
62 &self.buf
63 }
64
65 /// Whether any partial line is currently buffered.
66 pub fn has_buffered(&self) -> bool {
67 !self.buf.is_empty()
68 }
69}