use std::io::{self, Read};
use std::path::Path;
use crate::file_data::FileData;
use crate::map::map_file;
const DEFAULT_STDIN_MAX_BYTES: usize = 1_073_741_824;
#[derive(Debug, PartialEq, Eq)]
enum LoadSource<'a> {
Stdin,
File(&'a Path),
}
fn resolve_source(path: &Path) -> LoadSource<'_> {
if path == Path::new("-") {
LoadSource::Stdin
} else {
LoadSource::File(path)
}
}
const CHUNK_SIZE: usize = 8 * 1024;
#[allow(clippy::indexing_slicing)] #[allow(unreachable_pub)] #[allow(clippy::missing_errors_doc)] pub fn read_bounded<R: Read>(reader: &mut R, max_bytes: Option<usize>) -> io::Result<Vec<u8>> {
let initial_capacity = max_bytes.map_or(CHUNK_SIZE, |cap| cap.min(CHUNK_SIZE));
let mut buf = Vec::with_capacity(initial_capacity);
let mut chunk = [0_u8; CHUNK_SIZE];
loop {
let read_size = match max_bytes {
Some(cap) => {
let remaining = cap.saturating_sub(buf.len());
if remaining == 0 {
let mut probe = [0_u8; 1];
return if reader.read(&mut probe)? == 0 {
Ok(buf)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("input exceeded {cap} bytes"),
))
};
}
remaining.min(CHUNK_SIZE)
}
None => CHUNK_SIZE,
};
let n = reader.read(&mut chunk[..read_size])?;
if n == 0 {
break;
}
buf.extend_from_slice(&chunk[..n]);
}
Ok(buf)
}
#[must_use = "the loaded FileData should be consumed; dropping it discards the data"]
pub fn load(path: impl AsRef<Path>) -> io::Result<FileData> {
let path = path.as_ref();
match resolve_source(path) {
LoadSource::Stdin => load_stdin(Some(DEFAULT_STDIN_MAX_BYTES)),
LoadSource::File(p) => map_file(p),
}
}
#[must_use = "the loaded FileData should be consumed; dropping it discards the data"]
pub fn load_stdin(max_bytes: Option<usize>) -> io::Result<FileData> {
let mut stdin = io::stdin();
let buf = read_bounded(&mut stdin, max_bytes)?;
Ok(FileData::Loaded(buf))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests {
use std::io::{self, Cursor, Write};
use std::path::Path;
use tempfile::NamedTempFile;
use super::*;
#[test]
fn load_maps_regular_file() {
let mut tmp = NamedTempFile::new().expect("failed to create temp file");
tmp.write_all(b"load test").expect("failed to write");
tmp.flush().expect("failed to flush");
let data = load(tmp.path()).expect("load failed");
assert_eq!(&*data, b"load test");
assert!(
matches!(data, FileData::Mapped(..)),
"expected Mapped variant for regular file"
);
}
#[test]
fn load_rejects_empty_file() {
let tmp = NamedTempFile::new().expect("failed to create temp file");
let err = load(tmp.path()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn load_rejects_nonexistent_file() {
let err = load("/tmp/mmap_guard_load_missing_12345").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn read_bounded_accepts_within_limit() {
let input = b"hello, world";
let mut cursor = Cursor::new(input.as_slice());
let result = read_bounded(&mut cursor, Some(1024)).unwrap();
assert_eq!(result, input);
}
#[test]
fn read_bounded_returns_error_on_overflow() {
let input = vec![0xAB_u8; 256];
let mut cursor = Cursor::new(input.as_slice());
let err = read_bounded(&mut cursor, Some(100)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("100 bytes"),
"error message should mention the cap: {err}",
);
}
#[test]
fn read_bounded_unlimited_reads_all() {
let input = vec![0xCD_u8; 32_000];
let mut cursor = Cursor::new(input.as_slice());
let result = read_bounded(&mut cursor, None).unwrap();
assert_eq!(result.len(), 32_000);
}
#[test]
fn read_bounded_exact_cap_succeeds() {
let input = b"exactly";
let mut cursor = Cursor::new(input.as_slice());
let result = read_bounded(&mut cursor, Some(input.len())).unwrap();
assert_eq!(result, input);
}
#[test]
fn read_bounded_one_byte_over_cap_fails() {
let input = b"overflow";
let mut cursor = Cursor::new(input.as_slice());
let err = read_bounded(&mut cursor, Some(input.len() - 1)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn resolve_source_dash_routes_to_stdin() {
assert_eq!(resolve_source(Path::new("-")), LoadSource::Stdin);
}
#[test]
fn resolve_source_regular_path_routes_to_file() {
let path = Path::new("/some/file.txt");
assert_eq!(resolve_source(path), LoadSource::File(path));
}
#[test]
fn resolve_source_dash_prefix_is_not_stdin() {
let path = Path::new("-extra");
assert_eq!(resolve_source(path), LoadSource::File(path));
}
#[test]
fn resolve_source_empty_path_is_not_stdin() {
let path = Path::new("");
assert_eq!(resolve_source(path), LoadSource::File(path));
}
#[test]
#[allow(clippy::exit)] fn load_dash_routes_to_stdin() {
use std::fs;
use std::process::{Command, Stdio};
if let Ok(out_path) = std::env::var("__MMAP_GUARD_STDIN_OUT") {
let result = load("-");
match result {
Ok(data) if matches!(data, FileData::Loaded(..)) => {
fs::write(&out_path, &*data).unwrap();
std::process::exit(0);
}
Ok(data) => {
eprintln!("expected Loaded variant, got: {data:?}");
std::process::exit(1);
}
Err(e) => {
eprintln!("load(\"-\") failed: {e}");
std::process::exit(1);
}
}
}
let current_exe = std::env::current_exe().expect("failed to get current exe");
let payload = b"hello from stdin";
let out_file = NamedTempFile::new().expect("failed to create output temp file");
let out_path = out_file.path().to_owned();
let mut child = Command::new(¤t_exe)
.env("__MMAP_GUARD_STDIN_OUT", &out_path)
.arg("--exact")
.arg("load::tests::load_dash_routes_to_stdin")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn child");
{
let child_stdin = child.stdin.as_mut().expect("missing child stdin");
child_stdin
.write_all(payload)
.expect("failed to write to child stdin");
}
drop(child.stdin.take());
let output = child.wait_with_output().expect("failed to wait on child");
assert!(
output.status.success(),
"child exited with failure: {}\nstderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let written = fs::read(&out_path).expect("failed to read child output file");
assert_eq!(
written, payload,
"child output did not match expected payload"
);
}
#[test]
fn read_bounded_zero_cap_empty_input_succeeds() {
let mut cursor = Cursor::new(b"".as_slice());
let result = read_bounded(&mut cursor, Some(0)).unwrap();
assert!(result.is_empty());
}
#[test]
fn read_bounded_zero_cap_nonempty_input_fails() {
let mut cursor = Cursor::new(b"data".as_slice());
let err = read_bounded(&mut cursor, Some(0)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn read_bounded_multi_chunk_with_cap() {
let input = vec![0xAA_u8; 3 * CHUNK_SIZE + 100];
let mut cursor = Cursor::new(input.as_slice());
let result = read_bounded(&mut cursor, Some(input.len() + 1024)).unwrap();
assert_eq!(result.len(), input.len());
}
#[test]
fn read_bounded_produces_loaded_variant() {
let input = b"simulated stdin";
let mut cursor = Cursor::new(input.as_slice());
let buf = read_bounded(&mut cursor, Some(DEFAULT_STDIN_MAX_BYTES)).unwrap();
let data = FileData::Loaded(buf);
assert_eq!(&*data, input);
}
mod prop {
use std::io::Cursor;
use proptest::prelude::*;
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn prop_read_bounded(
data in proptest::collection::vec(any::<u8>(), 0..65_536),
cap in proptest::option::of(0_u16..=u16::MAX),
) {
let cap_usize = cap.map(usize::from);
let mut cursor = Cursor::new(&data);
match read_bounded(&mut cursor, cap_usize) {
Ok(buf) => {
if let Some(c) = cap_usize {
prop_assert!(buf.len() <= c, "buf.len() {} > cap {c}", buf.len());
} else {
prop_assert_eq!(
buf.len(),
data.len(),
"unlimited read returned fewer bytes than available",
);
}
prop_assert_eq!(&buf[..], &data[..buf.len()]);
}
Err(e) => {
prop_assert_eq!(e.kind(), io::ErrorKind::InvalidData);
prop_assert!(
cap_usize.is_some(),
"got InvalidData with no cap — should be impossible",
);
if let Some(c) = cap_usize {
prop_assert!(
data.len() > c,
"got InvalidData but data.len() {} <= cap {c}",
data.len(),
);
}
}
}
}
}
}
}