miden_serde_utils/
file.rs1use std::{
2 fs::{self, File, Metadata},
3 io::{self, Read},
4 path::Path,
5};
6
7pub fn read_file_bounded(path: impl AsRef<Path>, max_bytes: usize) -> io::Result<Vec<u8>> {
17 let path = path.as_ref();
18 check_metadata(&fs::metadata(path)?, max_bytes)?;
19 read_file_after_metadata(path, max_bytes)
20}
21
22fn read_file_after_metadata(path: &Path, max_bytes: usize) -> io::Result<Vec<u8>> {
23 #[cfg(unix)]
24 let file = {
25 use rustix::fs::{Mode, OFlags, open};
26
27 File::from(open(path, OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC, Mode::empty())?)
28 };
29 #[cfg(not(unix))]
30 let file = File::open(path)?;
31 check_metadata(&file.metadata()?, max_bytes)?;
32 read_bounded(file, max_bytes)
33}
34
35fn check_metadata(metadata: &Metadata, max_bytes: usize) -> io::Result<()> {
36 if !metadata.is_file() {
37 return Err(io::Error::new(io::ErrorKind::InvalidInput, "expected a regular file"));
38 }
39 if metadata.len() > max_bytes as u64 {
40 return Err(size_error(max_bytes));
41 }
42 Ok(())
43}
44
45fn read_bounded(mut reader: impl Read, max_bytes: usize) -> io::Result<Vec<u8>> {
46 let mut bytes = Vec::new();
47 reader.by_ref().take(max_bytes as u64).read_to_end(&mut bytes)?;
48 match reader.read_exact(&mut [0]) {
49 Ok(()) => Err(size_error(max_bytes)),
50 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => Ok(bytes),
51 Err(err) => Err(err),
52 }
53}
54
55fn size_error(max_bytes: usize) -> io::Error {
56 io::Error::new(
57 io::ErrorKind::InvalidData,
58 format!("file exceeds size limit of {max_bytes} bytes"),
59 )
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn bounded_read_accepts_the_limit_and_rejects_an_extra_byte() {
68 assert_eq!(read_bounded(&b"abc"[..], 3).unwrap(), b"abc");
69 assert_eq!(read_bounded(&b"ab"[..], 3).unwrap(), b"ab");
70 let err = read_bounded(&b"abcd"[..], 3).unwrap_err();
71 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
72 assert_eq!(err.to_string(), "file exceeds size limit of 3 bytes");
73 assert!(read_bounded(&b""[..], 0).unwrap().is_empty());
74 let err = read_bounded(&b"a"[..], 0).unwrap_err();
75 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
76 assert_eq!(err.to_string(), "file exceeds size limit of 0 bytes");
77 }
78
79 #[test]
80 fn bounded_read_stops_after_the_limit_and_one_probe_byte() {
81 let mut source = io::Cursor::new(b"abcdef");
82 assert!(read_bounded(&mut source, 3).is_err());
83 assert_eq!(source.position(), 4);
84 }
85
86 #[test]
87 fn bounded_read_retries_an_interrupted_overflow_probe() {
88 struct InterruptedProbe<'a> {
89 remaining: &'a [u8],
90 interrupted: bool,
91 }
92
93 impl Read for InterruptedProbe<'_> {
94 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
95 if !self.interrupted {
96 self.interrupted = true;
97 return Err(io::ErrorKind::Interrupted.into());
98 }
99 self.remaining.read(buf)
100 }
101 }
102
103 for remaining in [b"".as_slice(), b"d".as_slice()] {
104 let mut source =
105 io::Cursor::new(b"abc").chain(InterruptedProbe { remaining, interrupted: false });
106 let result = read_bounded(&mut source, 3);
107 if remaining.is_empty() {
108 assert_eq!(result.unwrap(), b"abc");
109 } else {
110 let err = result.unwrap_err();
111 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
112 assert_eq!(err.to_string(), "file exceeds size limit of 3 bytes");
113 }
114 }
115 }
116
117 #[test]
118 fn bounded_file_rejects_directories() {
119 assert_eq!(
120 read_file_bounded(std::env::temp_dir(), 3).unwrap_err().kind(),
121 io::ErrorKind::InvalidInput
122 );
123 }
124
125 #[cfg(unix)]
126 #[test]
127 fn bounded_file_rejects_devices() {
128 assert_eq!(
129 read_file_bounded("/dev/zero", 3).unwrap_err().kind(),
130 io::ErrorKind::InvalidInput
131 );
132 }
133
134 #[cfg(unix)]
135 #[test]
136 fn bounded_file_rejects_fifo_replacing_a_regular_file_without_blocking() {
137 use std::{
138 process::Command,
139 thread,
140 time::{Duration, Instant},
141 };
142
143 const CHILD_PATH: &str = "MIDEN_TEST_REPLACED_FIFO_PATH";
144 if let Some(path) = std::env::var_os(CHILD_PATH) {
145 let err = read_file_after_metadata(Path::new(&path), 3).unwrap_err();
147 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
148 assert_eq!(err.to_string(), "expected a regular file");
149 return;
150 }
151
152 let dir = tempfile::tempdir().unwrap();
153 let path = dir.path().join("replaced");
154 fs::write(&path, b"abc").unwrap();
155 check_metadata(&fs::metadata(&path).unwrap(), 3).unwrap();
156 fs::remove_file(&path).unwrap();
157 assert!(
159 Command::new("mkfifo")
160 .args(["-m", "600"])
161 .arg(&path)
162 .status()
163 .unwrap()
164 .success()
165 );
166
167 let mut child = Command::new(std::env::current_exe().unwrap())
169 .args([
170 "--exact",
171 "file::tests::bounded_file_rejects_fifo_replacing_a_regular_file_without_blocking",
172 ])
173 .env(CHILD_PATH, &path)
174 .spawn()
175 .unwrap();
176 let deadline = Instant::now() + Duration::from_secs(10);
177 loop {
178 if let Some(status) = child.try_wait().unwrap() {
179 assert!(status.success(), "replacement FIFO child failed: {status}");
180 break;
181 }
182 if Instant::now() >= deadline {
183 child.kill().unwrap();
184 child.wait().unwrap();
185 panic!("opening a replacement FIFO blocked");
186 }
187 thread::sleep(Duration::from_millis(10));
188 }
189 }
190}