mermaid_cli/utils/
bounded.rs1use std::io::Read;
10use tokio::io::{AsyncBufRead, AsyncBufReadExt};
11
12pub enum CappedLine {
14 Line(Vec<u8>),
17 TooLong,
21 Eof,
23}
24
25pub async fn read_line_capped<R>(reader: &mut R, max_bytes: usize) -> std::io::Result<CappedLine>
33where
34 R: AsyncBufRead + Unpin,
35{
36 let mut buf: Vec<u8> = Vec::new();
37 let mut overflow = false;
38 loop {
39 let available = reader.fill_buf().await?;
40 if available.is_empty() {
41 if overflow {
43 return Ok(CappedLine::TooLong);
44 }
45 if buf.is_empty() {
46 return Ok(CappedLine::Eof);
47 }
48 return Ok(CappedLine::Line(strip_trailing_cr(buf)));
50 }
51 if let Some(nl) = available.iter().position(|&b| b == b'\n') {
52 if !overflow {
53 if buf.len() + nl > max_bytes {
54 overflow = true;
55 } else {
56 buf.extend_from_slice(&available[..nl]);
57 }
58 }
59 reader.consume(nl + 1);
60 if overflow {
61 return Ok(CappedLine::TooLong);
62 }
63 return Ok(CappedLine::Line(strip_trailing_cr(buf)));
64 }
65 let n = available.len();
67 if !overflow {
68 if buf.len() + n > max_bytes {
69 overflow = true;
72 buf = Vec::new();
73 } else {
74 buf.extend_from_slice(available);
75 }
76 }
77 reader.consume(n);
78 }
79}
80
81fn strip_trailing_cr(mut v: Vec<u8>) -> Vec<u8> {
82 if v.last() == Some(&b'\r') {
83 v.pop();
84 }
85 v
86}
87
88pub fn read_file_capped(
95 path: &std::path::Path,
96 max_bytes: usize,
97) -> std::io::Result<(Vec<u8>, bool)> {
98 read_capped(std::fs::File::open(path)?, max_bytes)
99}
100
101pub fn read_capped(file: std::fs::File, max_bytes: usize) -> std::io::Result<(Vec<u8>, bool)> {
108 let mut buf = Vec::new();
109 let cap_plus = (max_bytes as u64).saturating_add(1);
112 file.take(cap_plus).read_to_end(&mut buf)?;
113 let truncated = buf.len() > max_bytes;
114 if truncated {
115 buf.truncate(max_bytes);
116 }
117 Ok((buf, truncated))
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 async fn read_all(input: &[u8], cap: usize) -> Vec<CappedLine> {
125 let mut reader = tokio::io::BufReader::new(input);
126 let mut out = Vec::new();
127 loop {
128 match read_line_capped(&mut reader, cap).await.unwrap() {
129 CappedLine::Eof => break,
130 other => out.push(other),
131 }
132 }
133 out
134 }
135
136 fn lines(outcomes: &[CappedLine]) -> Vec<String> {
137 outcomes
138 .iter()
139 .map(|o| match o {
140 CappedLine::Line(b) => String::from_utf8_lossy(b).into_owned(),
141 CappedLine::TooLong => "<TOOLONG>".to_string(),
142 CappedLine::Eof => "<EOF>".to_string(),
143 })
144 .collect()
145 }
146
147 #[tokio::test]
148 async fn splits_lines_and_strips_crlf() {
149 let out = read_all(b"alpha\r\nbeta\ngamma", 1024).await;
150 assert_eq!(lines(&out), vec!["alpha", "beta", "gamma"]);
151 }
152
153 #[tokio::test]
154 async fn empty_input_is_eof() {
155 let out = read_all(b"", 1024).await;
156 assert!(out.is_empty());
157 }
158
159 #[tokio::test]
160 async fn oversize_line_is_rejected_then_stream_resyncs() {
161 let big = "x".repeat(5000);
164 let input = format!("{big}\nok\n");
165 let out = read_all(input.as_bytes(), 16).await;
166 assert_eq!(lines(&out), vec!["<TOOLONG>", "ok"]);
167 }
168
169 #[tokio::test]
170 async fn line_exactly_at_cap_is_kept() {
171 let input = b"0123456789\n"; let out = read_all(input, 10).await;
174 assert_eq!(lines(&out), vec!["0123456789"]);
175 }
176
177 #[test]
178 fn read_file_capped_truncates_large_files() {
179 let dir = std::env::temp_dir().join(format!("bounded_test_{}", std::process::id()));
180 std::fs::create_dir_all(&dir).unwrap();
181 let path = dir.join("big.txt");
182 std::fs::write(&path, vec![b'a'; 1000]).unwrap();
183
184 let (bytes, truncated) = read_file_capped(&path, 100).unwrap();
185 assert_eq!(bytes.len(), 100);
186 assert!(truncated);
187
188 let (bytes, truncated) = read_file_capped(&path, 5000).unwrap();
189 assert_eq!(bytes.len(), 1000);
190 assert!(!truncated);
191
192 let _ = std::fs::remove_dir_all(&dir);
193 }
194}