use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
const READ_CHUNK_SIZE: usize = 8192;
pub fn input_size_exceeded_message(max_bytes: usize) -> String {
format!("Input exceeds maximum size of {} bytes", max_bytes)
}
pub fn read_line_bounded<R: Read>(reader: &mut R, max_bytes: usize) -> io::Result<String> {
let mut line = Vec::with_capacity(max_bytes.min(READ_CHUNK_SIZE));
let mut buf = [0u8; READ_CHUNK_SIZE];
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break;
}
for &byte in &buf[..n] {
if byte == b'\n' {
line.push(byte);
return String::from_utf8(line)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
}
if line.len() >= max_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
input_size_exceeded_message(max_bytes),
));
}
line.push(byte);
}
}
String::from_utf8(line).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn read_file_bounded(path: impl AsRef<Path>, max_bytes: usize) -> io::Result<String> {
let path = path.as_ref();
let path_display = path.display().to_string();
let metadata = std::fs::metadata(path).map_err(|e| {
io::Error::new(
io::ErrorKind::NotFound,
format!("Failed to read file '{}': {}", path_display, e),
)
})?;
if !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Input path '{}' is not a regular file", path_display),
));
}
if metadata.len() > max_bytes as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Input file '{}' exceeds maximum size of {} bytes",
path_display, max_bytes
),
));
}
let file = File::open(path).map_err(|e| {
io::Error::new(
io::ErrorKind::NotFound,
format!("Failed to read file '{}': {}", path_display, e),
)
})?;
let mut limited = file.take(max_bytes as u64 + 1);
let mut bytes = Vec::new();
limited.read_to_end(&mut bytes)?;
if bytes.len() > max_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Input file '{}' exceeds maximum size of {} bytes",
path_display, max_bytes
),
));
}
String::from_utf8(bytes).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Input file '{}' is not valid UTF-8: {}", path_display, e),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn read_line_bounded_accepts_line_within_limit() {
let mut reader = Cursor::new(b"hello\n");
let result = read_line_bounded(&mut reader, 10);
assert_eq!(result.unwrap(), "hello\n");
}
#[test]
fn read_line_bounded_rejects_oversized_line() {
let mut reader = Cursor::new(b"abcdef\n");
let result = read_line_bounded(&mut reader, 3);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("exceeds maximum size"));
}
#[test]
fn read_line_bounded_eof_without_newline() {
let mut reader = Cursor::new(b"hi");
let result = read_line_bounded(&mut reader, 10);
assert_eq!(result.unwrap(), "hi");
}
#[test]
fn read_line_bounded_accepts_exactly_max_bytes_at_eof() {
let payload = vec![b'x'; 5];
let mut reader = Cursor::new(payload);
let result = read_line_bounded(&mut reader, 5);
assert_eq!(result.unwrap(), "xxxxx");
}
#[test]
fn read_line_bounded_allows_content_plus_newline_at_limit() {
let mut payload = vec![b'a'; 3];
payload.push(b'\n');
let mut reader = Cursor::new(payload);
let result = read_line_bounded(&mut reader, 3);
assert_eq!(result.unwrap(), "aaa\n");
}
#[test]
fn read_line_bounded_rejects_one_byte_over_content_limit() {
let payload = vec![b'b'; 4];
let mut reader = Cursor::new(payload);
let result = read_line_bounded(&mut reader, 3);
assert!(result.is_err());
}
}