use std::fs::{self, File};
use std::io::{Read, Take};
use std::path::Path;
pub const SOURCE_FILE_READ_MAX_BYTES: u64 = 8 * 1024 * 1024;
#[derive(Debug)]
pub enum CappedReadError {
Io(std::io::Error),
Oversized { len: Option<u64>, limit: u64 },
NotUtf8(std::string::FromUtf8Error),
}
impl std::fmt::Display for CappedReadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(err) => write!(f, "{err}"),
Self::Oversized {
len: Some(len),
limit,
} => {
write!(
f,
"file is {len} bytes, which exceeds the {limit}-byte source-read limit"
)
}
Self::Oversized { len: None, limit } => {
write!(f, "file exceeds the {limit}-byte source-read limit")
}
Self::NotUtf8(err) => write!(f, "file is not valid UTF-8: {err}"),
}
}
}
impl std::error::Error for CappedReadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::NotUtf8(err) => Some(err),
Self::Oversized { .. } => None,
}
}
}
impl CappedReadError {
pub fn is_oversized(&self) -> bool {
matches!(self, Self::Oversized { .. })
}
}
pub fn read_text_file_capped(path: &Path) -> Result<String, CappedReadError> {
read_text_file_capped_with_limit(path, SOURCE_FILE_READ_MAX_BYTES)
}
pub fn read_text_file_capped_with_limit(
path: &Path,
limit: u64,
) -> Result<String, CappedReadError> {
match fs::symlink_metadata(path) {
Ok(meta) => {
if meta.file_type().is_file() && meta.len() > limit {
return Err(CappedReadError::Oversized {
len: Some(meta.len()),
limit,
});
}
}
Err(err) => return Err(CappedReadError::Io(err)),
}
let file = File::open(path).map_err(CappedReadError::Io)?;
let mut limited: Take<File> = file.take(limit.saturating_add(1));
let mut bytes = Vec::new();
limited
.read_to_end(&mut bytes)
.map_err(CappedReadError::Io)?;
if (bytes.len() as u64) > limit {
return Err(CappedReadError::Oversized { len: None, limit });
}
String::from_utf8(bytes).map_err(CappedReadError::NotUtf8)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_path(label: &str) -> std::path::PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!(
"cargo-allow-capped-read-{label}-{}-{stamp}",
std::process::id()
))
}
#[test]
fn reads_small_utf8_files() {
let path = temp_path("small");
fs::write(&path, "hello\n")
.unwrap_or_else(|err| std::panic::panic_any(format!("write small fixture: {err}")));
let text = read_text_file_capped_with_limit(&path, 64).unwrap_or_else(|err| {
std::panic::panic_any(format!("small read should succeed: {err}"))
});
assert_eq!(text, "hello\n");
let _ = fs::remove_file(&path);
}
#[test]
fn rejects_oversized_files_before_full_allocation() {
let path = temp_path("oversized");
let limit = 64u64;
let mut file = File::create(&path).unwrap_or_else(|err| {
std::panic::panic_any(format!("create oversized fixture: {err}"))
});
file.write_all(&vec![b'a'; (limit as usize) + 1])
.unwrap_or_else(|err| std::panic::panic_any(format!("write oversized: {err}")));
drop(file);
let err = read_text_file_capped_with_limit(&path, limit).unwrap_err();
assert!(err.is_oversized(), "expected oversized, got {err}");
let _ = fs::remove_file(&path);
}
#[test]
fn accepts_file_exactly_at_limit() {
let path = temp_path("exact");
let limit = 32u64;
fs::write(&path, vec![b'b'; limit as usize])
.unwrap_or_else(|err| std::panic::panic_any(format!("write exact fixture: {err}")));
let text = read_text_file_capped_with_limit(&path, limit).unwrap_or_else(|err| {
std::panic::panic_any(format!("exact-limit read should succeed: {err}"))
});
assert_eq!(text.len() as u64, limit);
let _ = fs::remove_file(&path);
}
}