allow_core/
capped_read.rs1use std::fs::{self, File};
9use std::io::{Read, Take};
10use std::path::Path;
11
12pub const SOURCE_FILE_READ_MAX_BYTES: u64 = 8 * 1024 * 1024;
17
18#[derive(Debug)]
20pub enum CappedReadError {
21 Io(std::io::Error),
22 Oversized { len: Option<u64>, limit: u64 },
23 NotUtf8(std::string::FromUtf8Error),
24}
25
26impl std::fmt::Display for CappedReadError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::Io(err) => write!(f, "{err}"),
30 Self::Oversized {
31 len: Some(len),
32 limit,
33 } => {
34 write!(
35 f,
36 "file is {len} bytes, which exceeds the {limit}-byte source-read limit"
37 )
38 }
39 Self::Oversized { len: None, limit } => {
40 write!(f, "file exceeds the {limit}-byte source-read limit")
41 }
42 Self::NotUtf8(err) => write!(f, "file is not valid UTF-8: {err}"),
43 }
44 }
45}
46
47impl std::error::Error for CappedReadError {
48 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49 match self {
50 Self::Io(err) => Some(err),
51 Self::NotUtf8(err) => Some(err),
52 Self::Oversized { .. } => None,
53 }
54 }
55}
56
57impl CappedReadError {
58 pub fn is_oversized(&self) -> bool {
59 matches!(self, Self::Oversized { .. })
60 }
61}
62
63pub fn read_text_file_capped(path: &Path) -> Result<String, CappedReadError> {
65 read_text_file_capped_with_limit(path, SOURCE_FILE_READ_MAX_BYTES)
66}
67
68pub fn read_text_file_capped_with_limit(
74 path: &Path,
75 limit: u64,
76) -> Result<String, CappedReadError> {
77 match fs::symlink_metadata(path) {
78 Ok(meta) => {
79 if meta.file_type().is_file() && meta.len() > limit {
80 return Err(CappedReadError::Oversized {
81 len: Some(meta.len()),
82 limit,
83 });
84 }
85 }
86 Err(err) => return Err(CappedReadError::Io(err)),
87 }
88
89 let file = File::open(path).map_err(CappedReadError::Io)?;
90 let mut limited: Take<File> = file.take(limit.saturating_add(1));
91 let mut bytes = Vec::new();
92 limited
93 .read_to_end(&mut bytes)
94 .map_err(CappedReadError::Io)?;
95 if (bytes.len() as u64) > limit {
96 return Err(CappedReadError::Oversized { len: None, limit });
97 }
98 String::from_utf8(bytes).map_err(CappedReadError::NotUtf8)
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use std::io::Write;
105 use std::time::{SystemTime, UNIX_EPOCH};
106
107 fn temp_path(label: &str) -> std::path::PathBuf {
108 let stamp = SystemTime::now()
109 .duration_since(UNIX_EPOCH)
110 .map(|d| d.as_nanos())
111 .unwrap_or(0);
112 std::env::temp_dir().join(format!(
113 "cargo-allow-capped-read-{label}-{}-{stamp}",
114 std::process::id()
115 ))
116 }
117
118 #[test]
119 fn reads_small_utf8_files() {
120 let path = temp_path("small");
121 fs::write(&path, "hello\n")
122 .unwrap_or_else(|err| std::panic::panic_any(format!("write small fixture: {err}")));
123 let text = read_text_file_capped_with_limit(&path, 64).unwrap_or_else(|err| {
124 std::panic::panic_any(format!("small read should succeed: {err}"))
125 });
126 assert_eq!(text, "hello\n");
127 let _ = fs::remove_file(&path);
128 }
129
130 #[test]
131 fn rejects_oversized_files_before_full_allocation() {
132 let path = temp_path("oversized");
133 let limit = 64u64;
134 let mut file = File::create(&path).unwrap_or_else(|err| {
135 std::panic::panic_any(format!("create oversized fixture: {err}"))
136 });
137 file.write_all(&vec![b'a'; (limit as usize) + 1])
138 .unwrap_or_else(|err| std::panic::panic_any(format!("write oversized: {err}")));
139 drop(file);
140
141 let err = read_text_file_capped_with_limit(&path, limit).unwrap_err();
142 assert!(err.is_oversized(), "expected oversized, got {err}");
143 let _ = fs::remove_file(&path);
144 }
145
146 #[test]
147 fn accepts_file_exactly_at_limit() {
148 let path = temp_path("exact");
149 let limit = 32u64;
150 fs::write(&path, vec![b'b'; limit as usize])
151 .unwrap_or_else(|err| std::panic::panic_any(format!("write exact fixture: {err}")));
152 let text = read_text_file_capped_with_limit(&path, limit).unwrap_or_else(|err| {
153 std::panic::panic_any(format!("exact-limit read should succeed: {err}"))
154 });
155 assert_eq!(text.len() as u64, limit);
156 let _ = fs::remove_file(&path);
157 }
158}