vtcode_bash_runner/
stream.rs1use std::io;
2use tokio::io::{AsyncBufRead, AsyncBufReadExt};
3
4#[derive(Debug)]
6pub enum ReadLineResult {
7 Line(Vec<u8>),
8 Truncated(Vec<u8>),
9 Eof,
10}
11
12pub(crate) async fn read_line_with_limit<R: AsyncBufRead + Unpin>(
14 reader: &mut R,
15 buf: &mut Vec<u8>,
16 max_len: usize,
17) -> io::Result<ReadLineResult> {
18 buf.clear();
19
20 loop {
21 let available = reader.fill_buf().await?;
22 if available.is_empty() {
23 return Ok(if buf.is_empty() {
27 ReadLineResult::Eof
28 } else if buf.len() <= max_len {
29 ReadLineResult::Line(std::mem::take(buf))
30 } else {
31 ReadLineResult::Truncated(std::mem::take(buf))
32 });
33 }
34
35 if let Some(pos) = memchr::memchr(b'\n', available) {
36 let to_read = pos + 1;
38 let would_be_total = buf.len() + to_read;
39
40 if would_be_total <= max_len {
41 buf.extend_from_slice(&available[..to_read]);
43 reader.consume(to_read);
44 return Ok(ReadLineResult::Line(std::mem::take(buf)));
45 } else {
46 let remaining_space = max_len.saturating_sub(buf.len());
50 if remaining_space > 0 {
51 buf.extend_from_slice(&available[..remaining_space]);
52 }
53 reader.consume(to_read);
54 return Ok(ReadLineResult::Truncated(std::mem::take(buf)));
55 }
56 }
57
58 let len = available.len();
60 let would_be_total = buf.len() + len;
61
62 if would_be_total <= max_len {
63 buf.extend_from_slice(available);
65 reader.consume(len);
66 } else {
67 let remaining_space = max_len.saturating_sub(buf.len());
71 if remaining_space > 0 {
72 buf.extend_from_slice(&available[..remaining_space]);
73 }
74 reader.consume(len);
75 return Ok(ReadLineResult::Truncated(std::mem::take(buf)));
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::{ReadLineResult, read_line_with_limit};
83 use tokio::io::BufReader;
84
85 #[tokio::test]
86 async fn read_line_with_limit_truncates() -> std::io::Result<()> {
87 let data = "hello world\n";
88 let mut reader = BufReader::new(data.as_bytes());
89 let mut buf = Vec::new();
90
91 let result = read_line_with_limit(&mut reader, &mut buf, 5).await?;
92 match result {
93 ReadLineResult::Truncated(bytes) => {
94 assert!(!bytes.is_empty());
95 }
96 other => panic!("expected truncation, got {other:?}"),
97 }
98 Ok(())
99 }
100
101 #[tokio::test]
102 async fn read_line_with_limit_no_newline_stream_returns_truncated() -> std::io::Result<()> {
103 let data = vec![b'a'; 10_000];
106 let mut reader = BufReader::new(data.as_slice());
107 let mut buf = Vec::new();
108
109 let result = read_line_with_limit(&mut reader, &mut buf, 100).await?;
110 match result {
111 ReadLineResult::Truncated(bytes) => {
112 assert_eq!(bytes.len(), 100, "buffer must not exceed the limit");
113 }
114 other => panic!("expected truncation, got {other:?}"),
115 }
116 Ok(())
117 }
118
119 #[tokio::test]
120 async fn read_line_with_limit_reports_partial_line_at_eof() -> std::io::Result<()> {
121 let data = "no newline at end";
124 let mut reader = BufReader::new(data.as_bytes());
125 let mut buf = Vec::new();
126
127 let result = read_line_with_limit(&mut reader, &mut buf, 100).await?;
128 match result {
129 ReadLineResult::Line(bytes) => {
130 assert_eq!(bytes, b"no newline at end");
131 }
132 other => panic!("expected line, got {other:?}"),
133 }
134 Ok(())
135 }
136}