1use crate::traits::{DirEntry, DirEntryKind, Filesystem, ReadRange};
20use async_trait::async_trait;
21use std::io;
22use std::path::Path;
23
24const MAX_DEVICE_READ_BYTES: u64 = 64 * 1024 * 1024;
28
29#[derive(Debug, Default, Clone, Copy)]
31pub struct DevFs;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum Device {
36 Null,
38 Zero,
40 Random,
43}
44
45impl Device {
46 fn name(self) -> &'static str {
48 match self {
49 Device::Null => "null",
50 Device::Zero => "zero",
51 Device::Random => "urandom",
52 }
53 }
54}
55
56impl DevFs {
57 pub fn new() -> Self {
59 Self
60 }
61
62 const NAMES: [&'static str; 4] = ["null", "random", "urandom", "zero"];
64
65 fn device(path: &Path) -> Option<Device> {
68 match path.to_str()?.trim_start_matches('/') {
69 "null" => Some(Device::Null),
70 "zero" => Some(Device::Zero),
71 "urandom" | "random" => Some(Device::Random),
72 _ => None,
73 }
74 }
75
76 fn is_root(path: &Path) -> bool {
78 let s = path.to_string_lossy();
79 let trimmed = s.trim_matches('/');
80 trimmed.is_empty() || trimmed == "."
81 }
82
83 fn not_found(path: &Path) -> io::Error {
84 io::Error::new(
85 io::ErrorKind::NotFound,
86 format!("no such device: /dev/{}", path.display()),
87 )
88 }
89
90 fn unbounded(name: &str) -> io::Error {
92 io::Error::new(
93 io::ErrorKind::InvalidInput,
94 format!(
95 "/dev/{name} is an endless device; reading the whole of it is unbounded. \
96 Read a fixed number of bytes instead, e.g. `head -c 32 /dev/{name}`"
97 ),
98 )
99 }
100
101 fn entry(name: &str) -> DirEntry {
102 DirEntry {
103 name: name.to_string(),
104 kind: DirEntryKind::File,
105 size: 0,
106 modified: None,
107 permissions: None,
108 symlink_target: None,
109 }
110 }
111}
112
113#[async_trait]
114impl Filesystem for DevFs {
115 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
116 match Self::device(path) {
117 Some(Device::Null) => Ok(Vec::new()),
118 Some(dev) => Err(Self::unbounded(dev.name())), None => Err(Self::not_found(path)),
120 }
121 }
122
123 async fn read_range(&self, path: &Path, range: Option<ReadRange>) -> io::Result<Vec<u8>> {
124 let Some(dev) = Self::device(path) else {
125 return Err(Self::not_found(path));
126 };
127 if dev == Device::Null {
129 return Ok(Vec::new());
130 }
131 let limit = match range.and_then(|r| r.limit) {
134 Some(n) => n,
135 None => return Err(Self::unbounded(dev.name())),
136 };
137 if limit > MAX_DEVICE_READ_BYTES {
138 return Err(io::Error::new(
139 io::ErrorKind::InvalidInput,
140 format!(
141 "requested {limit} bytes from /dev/{} exceeds the device read cap \
142 of {MAX_DEVICE_READ_BYTES} bytes",
143 dev.name()
144 ),
145 ));
146 }
147 let mut buf = vec![0u8; limit as usize];
148 if dev == Device::Random {
149 getrandom::fill(&mut buf).map_err(|e| {
150 io::Error::other(format!("/dev/{}: entropy source failed: {e}", dev.name()))
151 })?;
152 }
153 Ok(buf) }
155
156 async fn write(&self, path: &Path, _data: &[u8]) -> io::Result<()> {
157 match Self::device(path) {
160 Some(_) => Ok(()),
161 None => Err(Self::not_found(path)),
162 }
163 }
164
165 async fn append(&self, path: &Path, data: &[u8]) -> io::Result<()> {
166 self.write(path, data).await
172 }
173
174 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
175 if Self::is_root(path) {
176 return Ok(Self::NAMES.iter().map(|n| Self::entry(n)).collect());
177 }
178 if Self::device(path).is_some() {
179 return Err(io::Error::new(
180 io::ErrorKind::NotADirectory,
181 format!("not a directory: /dev/{}", path.display()),
182 ));
183 }
184 Err(Self::not_found(path))
185 }
186
187 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
188 if Self::is_root(path) {
189 return Ok(DirEntry {
190 name: "dev".to_string(),
191 kind: DirEntryKind::Directory,
192 size: 0,
193 modified: None,
194 permissions: None,
195 symlink_target: None,
196 });
197 }
198 match Self::device(path) {
199 Some(_) => Ok(Self::entry(
200 path.to_str().unwrap_or_default().trim_start_matches('/'),
201 )),
202 None => Err(Self::not_found(path)),
203 }
204 }
205
206 async fn mkdir(&self, path: &Path) -> io::Result<()> {
207 Err(io::Error::new(
208 io::ErrorKind::PermissionDenied,
209 format!("/dev is read-only: cannot create {}", path.display()),
210 ))
211 }
212
213 async fn remove(&self, path: &Path) -> io::Result<()> {
214 Err(io::Error::new(
215 io::ErrorKind::PermissionDenied,
216 format!("/dev is read-only: cannot remove {}", path.display()),
217 ))
218 }
219
220 fn read_only(&self) -> bool {
221 false
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[tokio::test]
233 async fn null_reads_empty() {
234 let fs = DevFs::new();
235 assert_eq!(fs.read(Path::new("null")).await.unwrap(), b"");
236 assert_eq!(
238 fs.read_range(Path::new("null"), Some(ReadRange::bytes(0, 16)))
239 .await
240 .unwrap(),
241 b""
242 );
243 }
244
245 #[tokio::test]
246 async fn null_discards_writes() {
247 let fs = DevFs::new();
248 fs.write(Path::new("null"), b"anything at all").await.unwrap();
249 }
250
251 #[tokio::test]
252 async fn zero_counted_read_yields_zeros() {
253 let fs = DevFs::new();
254 let out = fs
255 .read_range(Path::new("zero"), Some(ReadRange::bytes(0, 8)))
256 .await
257 .unwrap();
258 assert_eq!(out, vec![0u8; 8]);
259 }
260
261 #[tokio::test]
262 async fn zero_whole_read_is_loud_error() {
263 let fs = DevFs::new();
264 let err = fs.read(Path::new("zero")).await.unwrap_err();
265 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
266 assert!(err.to_string().contains("head -c"), "should name the fix: {err}");
267
268 let err = fs.read_range(Path::new("zero"), None).await.unwrap_err();
270 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
271 }
272
273 #[tokio::test]
274 async fn zero_read_cap_is_enforced() {
275 let fs = DevFs::new();
276 let err = fs
277 .read_range(Path::new("zero"), Some(ReadRange::bytes(0, MAX_DEVICE_READ_BYTES + 1)))
278 .await
279 .unwrap_err();
280 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
281 assert!(err.to_string().contains("cap"), "should mention the cap: {err}");
282 }
283
284 #[tokio::test]
285 async fn urandom_counted_read_is_random_and_sized() {
286 let fs = DevFs::new();
287 let a = fs
288 .read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
289 .await
290 .unwrap();
291 assert_eq!(a.len(), 32, "exact byte count");
292 let b = fs
294 .read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
295 .await
296 .unwrap();
297 assert_ne!(a, b, "entropy: two draws must differ");
298 let c = fs
300 .read_range(Path::new("random"), Some(ReadRange::bytes(0, 8)))
301 .await
302 .unwrap();
303 assert_eq!(c.len(), 8);
304 }
305
306 #[tokio::test]
307 async fn urandom_whole_read_is_loud_error() {
308 let fs = DevFs::new();
309 let err = fs.read(Path::new("urandom")).await.unwrap_err();
310 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
311 assert!(err.to_string().contains("head -c"), "names the fix: {err}");
312 }
313
314 #[tokio::test]
315 async fn unknown_device_is_not_found() {
316 let fs = DevFs::new();
317 assert_eq!(
318 fs.read(Path::new("sda")).await.unwrap_err().kind(),
319 io::ErrorKind::NotFound
320 );
321 assert_eq!(
322 fs.write(Path::new("sda"), b"x").await.unwrap_err().kind(),
323 io::ErrorKind::NotFound
324 );
325 }
326
327 #[tokio::test]
328 async fn list_shows_devices() {
329 let fs = DevFs::new();
330 let names: Vec<_> = fs
331 .list(Path::new(""))
332 .await
333 .unwrap()
334 .into_iter()
335 .map(|e| e.name)
336 .collect();
337 assert_eq!(
338 names,
339 vec![
340 "null".to_string(),
341 "random".to_string(),
342 "urandom".to_string(),
343 "zero".to_string()
344 ]
345 );
346 }
347
348 #[tokio::test]
349 async fn stat_devices_and_root() {
350 let fs = DevFs::new();
351 assert_eq!(fs.stat(Path::new("")).await.unwrap().kind, DirEntryKind::Directory);
352 for dev in ["null", "zero", "urandom", "random"] {
353 let e = fs.stat(Path::new(dev)).await.unwrap();
354 assert_eq!(e.kind, DirEntryKind::File, "{dev}");
355 assert_eq!(e.name, dev, "stat names the device");
356 }
357 assert_eq!(
358 fs.stat(Path::new("nope")).await.unwrap_err().kind(),
359 io::ErrorKind::NotFound
360 );
361 }
362}