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 pub const DEVICE_MODE: u32 = 0o666;
105
106 pub const DIRECTORY_MODE: u32 = 0o555;
115
116 fn entry(name: &str) -> DirEntry {
117 DirEntry {
118 name: name.to_string(),
119 kind: DirEntryKind::File,
120 size: 0,
121 modified: None,
122 permissions: Some(Self::DEVICE_MODE),
123 symlink_target: None,
124 }
125 }
126}
127
128#[async_trait]
129impl Filesystem for DevFs {
130 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
131 match Self::device(path) {
132 Some(Device::Null) => Ok(Vec::new()),
133 Some(dev) => Err(Self::unbounded(dev.name())), None => Err(Self::not_found(path)),
135 }
136 }
137
138 async fn read_range(&self, path: &Path, range: Option<ReadRange>) -> io::Result<Vec<u8>> {
139 let Some(dev) = Self::device(path) else {
140 return Err(Self::not_found(path));
141 };
142 if dev == Device::Null {
144 return Ok(Vec::new());
145 }
146 let limit = match range.and_then(|r| r.limit) {
149 Some(n) => n,
150 None => return Err(Self::unbounded(dev.name())),
151 };
152 if limit > MAX_DEVICE_READ_BYTES {
153 return Err(io::Error::new(
154 io::ErrorKind::InvalidInput,
155 format!(
156 "requested {limit} bytes from /dev/{} exceeds the device read cap \
157 of {MAX_DEVICE_READ_BYTES} bytes",
158 dev.name()
159 ),
160 ));
161 }
162 let mut buf = vec![0u8; limit as usize];
163 if dev == Device::Random {
164 getrandom::fill(&mut buf).map_err(|e| {
165 io::Error::other(format!("/dev/{}: entropy source failed: {e}", dev.name()))
166 })?;
167 }
168 Ok(buf) }
170
171 async fn write(&self, path: &Path, _data: &[u8]) -> io::Result<()> {
172 match Self::device(path) {
175 Some(_) => Ok(()),
176 None => Err(Self::not_found(path)),
177 }
178 }
179
180 async fn append(&self, path: &Path, data: &[u8]) -> io::Result<()> {
181 self.write(path, data).await
187 }
188
189 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
190 if Self::is_root(path) {
191 return Ok(Self::NAMES.iter().map(|n| Self::entry(n)).collect());
192 }
193 if Self::device(path).is_some() {
194 return Err(io::Error::new(
195 io::ErrorKind::NotADirectory,
196 format!("not a directory: /dev/{}", path.display()),
197 ));
198 }
199 Err(Self::not_found(path))
200 }
201
202 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
203 if Self::is_root(path) {
204 return Ok(DirEntry {
205 name: "dev".to_string(),
206 kind: DirEntryKind::Directory,
207 size: 0,
208 modified: None,
209 permissions: Some(Self::DIRECTORY_MODE),
210 symlink_target: None,
211 });
212 }
213 match Self::device(path) {
214 Some(_) => Ok(Self::entry(
215 path.to_str().unwrap_or_default().trim_start_matches('/'),
216 )),
217 None => Err(Self::not_found(path)),
218 }
219 }
220
221 async fn mkdir(&self, path: &Path) -> io::Result<()> {
222 Err(io::Error::new(
223 io::ErrorKind::PermissionDenied,
224 format!("/dev is read-only: cannot create {}", path.display()),
225 ))
226 }
227
228 async fn remove(&self, path: &Path) -> io::Result<()> {
229 Err(io::Error::new(
230 io::ErrorKind::PermissionDenied,
231 format!("/dev is read-only: cannot remove {}", path.display()),
232 ))
233 }
234
235 fn read_only(&self) -> bool {
236 false
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[tokio::test]
250 async fn null_reads_empty() {
251 let fs = DevFs::new();
252 assert_eq!(fs.read(Path::new("null")).await.unwrap(), b"");
253 assert_eq!(
255 fs.read_range(Path::new("null"), Some(ReadRange::bytes(0, 16)))
256 .await
257 .unwrap(),
258 b""
259 );
260 }
261
262 #[tokio::test]
263 async fn null_discards_writes() {
264 let fs = DevFs::new();
265 fs.write(Path::new("null"), b"anything at all").await.unwrap();
266 }
267
268 #[tokio::test]
269 async fn zero_counted_read_yields_zeros() {
270 let fs = DevFs::new();
271 let out = fs
272 .read_range(Path::new("zero"), Some(ReadRange::bytes(0, 8)))
273 .await
274 .unwrap();
275 assert_eq!(out, vec![0u8; 8]);
276 }
277
278 #[tokio::test]
279 async fn zero_whole_read_is_loud_error() {
280 let fs = DevFs::new();
281 let err = fs.read(Path::new("zero")).await.unwrap_err();
282 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
283 assert!(err.to_string().contains("head -c"), "should name the fix: {err}");
284
285 let err = fs.read_range(Path::new("zero"), None).await.unwrap_err();
287 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
288 }
289
290 #[tokio::test]
291 async fn zero_read_cap_is_enforced() {
292 let fs = DevFs::new();
293 let err = fs
294 .read_range(Path::new("zero"), Some(ReadRange::bytes(0, MAX_DEVICE_READ_BYTES + 1)))
295 .await
296 .unwrap_err();
297 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
298 assert!(err.to_string().contains("cap"), "should mention the cap: {err}");
299 }
300
301 #[tokio::test]
302 async fn urandom_counted_read_is_random_and_sized() {
303 let fs = DevFs::new();
304 let a = fs
305 .read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
306 .await
307 .unwrap();
308 assert_eq!(a.len(), 32, "exact byte count");
309 let b = fs
311 .read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
312 .await
313 .unwrap();
314 assert_ne!(a, b, "entropy: two draws must differ");
315 let c = fs
317 .read_range(Path::new("random"), Some(ReadRange::bytes(0, 8)))
318 .await
319 .unwrap();
320 assert_eq!(c.len(), 8);
321 }
322
323 #[tokio::test]
324 async fn urandom_whole_read_is_loud_error() {
325 let fs = DevFs::new();
326 let err = fs.read(Path::new("urandom")).await.unwrap_err();
327 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
328 assert!(err.to_string().contains("head -c"), "names the fix: {err}");
329 }
330
331 #[tokio::test]
332 async fn unknown_device_is_not_found() {
333 let fs = DevFs::new();
334 assert_eq!(
335 fs.read(Path::new("sda")).await.unwrap_err().kind(),
336 io::ErrorKind::NotFound
337 );
338 assert_eq!(
339 fs.write(Path::new("sda"), b"x").await.unwrap_err().kind(),
340 io::ErrorKind::NotFound
341 );
342 }
343
344 #[tokio::test]
345 async fn list_shows_devices() {
346 let fs = DevFs::new();
347 let names: Vec<_> = fs
348 .list(Path::new(""))
349 .await
350 .unwrap()
351 .into_iter()
352 .map(|e| e.name)
353 .collect();
354 assert_eq!(
355 names,
356 vec![
357 "null".to_string(),
358 "random".to_string(),
359 "urandom".to_string(),
360 "zero".to_string()
361 ]
362 );
363 }
364
365 #[tokio::test]
366 async fn stat_devices_and_root() {
367 let fs = DevFs::new();
368 assert_eq!(fs.stat(Path::new("")).await.unwrap().kind, DirEntryKind::Directory);
369 for dev in ["null", "zero", "urandom", "random"] {
370 let e = fs.stat(Path::new(dev)).await.unwrap();
371 assert_eq!(e.kind, DirEntryKind::File, "{dev}");
372 assert_eq!(e.name, dev, "stat names the device");
373 }
374 assert_eq!(
375 fs.stat(Path::new("nope")).await.unwrap_err().kind(),
376 io::ErrorKind::NotFound
377 );
378 }
379}