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 list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
166 if Self::is_root(path) {
167 return Ok(Self::NAMES.iter().map(|n| Self::entry(n)).collect());
168 }
169 if Self::device(path).is_some() {
170 return Err(io::Error::new(
171 io::ErrorKind::NotADirectory,
172 format!("not a directory: /dev/{}", path.display()),
173 ));
174 }
175 Err(Self::not_found(path))
176 }
177
178 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
179 if Self::is_root(path) {
180 return Ok(DirEntry {
181 name: "dev".to_string(),
182 kind: DirEntryKind::Directory,
183 size: 0,
184 modified: None,
185 permissions: None,
186 symlink_target: None,
187 });
188 }
189 match Self::device(path) {
190 Some(_) => Ok(Self::entry(
191 path.to_str().unwrap_or_default().trim_start_matches('/'),
192 )),
193 None => Err(Self::not_found(path)),
194 }
195 }
196
197 async fn mkdir(&self, path: &Path) -> io::Result<()> {
198 Err(io::Error::new(
199 io::ErrorKind::PermissionDenied,
200 format!("/dev is read-only: cannot create {}", path.display()),
201 ))
202 }
203
204 async fn remove(&self, path: &Path) -> io::Result<()> {
205 Err(io::Error::new(
206 io::ErrorKind::PermissionDenied,
207 format!("/dev is read-only: cannot remove {}", path.display()),
208 ))
209 }
210
211 fn read_only(&self) -> bool {
212 false
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[tokio::test]
224 async fn null_reads_empty() {
225 let fs = DevFs::new();
226 assert_eq!(fs.read(Path::new("null")).await.unwrap(), b"");
227 assert_eq!(
229 fs.read_range(Path::new("null"), Some(ReadRange::bytes(0, 16)))
230 .await
231 .unwrap(),
232 b""
233 );
234 }
235
236 #[tokio::test]
237 async fn null_discards_writes() {
238 let fs = DevFs::new();
239 fs.write(Path::new("null"), b"anything at all").await.unwrap();
240 }
241
242 #[tokio::test]
243 async fn zero_counted_read_yields_zeros() {
244 let fs = DevFs::new();
245 let out = fs
246 .read_range(Path::new("zero"), Some(ReadRange::bytes(0, 8)))
247 .await
248 .unwrap();
249 assert_eq!(out, vec![0u8; 8]);
250 }
251
252 #[tokio::test]
253 async fn zero_whole_read_is_loud_error() {
254 let fs = DevFs::new();
255 let err = fs.read(Path::new("zero")).await.unwrap_err();
256 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
257 assert!(err.to_string().contains("head -c"), "should name the fix: {err}");
258
259 let err = fs.read_range(Path::new("zero"), None).await.unwrap_err();
261 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
262 }
263
264 #[tokio::test]
265 async fn zero_read_cap_is_enforced() {
266 let fs = DevFs::new();
267 let err = fs
268 .read_range(Path::new("zero"), Some(ReadRange::bytes(0, MAX_DEVICE_READ_BYTES + 1)))
269 .await
270 .unwrap_err();
271 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
272 assert!(err.to_string().contains("cap"), "should mention the cap: {err}");
273 }
274
275 #[tokio::test]
276 async fn urandom_counted_read_is_random_and_sized() {
277 let fs = DevFs::new();
278 let a = fs
279 .read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
280 .await
281 .unwrap();
282 assert_eq!(a.len(), 32, "exact byte count");
283 let b = fs
285 .read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
286 .await
287 .unwrap();
288 assert_ne!(a, b, "entropy: two draws must differ");
289 let c = fs
291 .read_range(Path::new("random"), Some(ReadRange::bytes(0, 8)))
292 .await
293 .unwrap();
294 assert_eq!(c.len(), 8);
295 }
296
297 #[tokio::test]
298 async fn urandom_whole_read_is_loud_error() {
299 let fs = DevFs::new();
300 let err = fs.read(Path::new("urandom")).await.unwrap_err();
301 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
302 assert!(err.to_string().contains("head -c"), "names the fix: {err}");
303 }
304
305 #[tokio::test]
306 async fn unknown_device_is_not_found() {
307 let fs = DevFs::new();
308 assert_eq!(
309 fs.read(Path::new("sda")).await.unwrap_err().kind(),
310 io::ErrorKind::NotFound
311 );
312 assert_eq!(
313 fs.write(Path::new("sda"), b"x").await.unwrap_err().kind(),
314 io::ErrorKind::NotFound
315 );
316 }
317
318 #[tokio::test]
319 async fn list_shows_devices() {
320 let fs = DevFs::new();
321 let names: Vec<_> = fs
322 .list(Path::new(""))
323 .await
324 .unwrap()
325 .into_iter()
326 .map(|e| e.name)
327 .collect();
328 assert_eq!(
329 names,
330 vec![
331 "null".to_string(),
332 "random".to_string(),
333 "urandom".to_string(),
334 "zero".to_string()
335 ]
336 );
337 }
338
339 #[tokio::test]
340 async fn stat_devices_and_root() {
341 let fs = DevFs::new();
342 assert_eq!(fs.stat(Path::new("")).await.unwrap().kind, DirEntryKind::Directory);
343 for dev in ["null", "zero", "urandom", "random"] {
344 let e = fs.stat(Path::new(dev)).await.unwrap();
345 assert_eq!(e.kind, DirEntryKind::File, "{dev}");
346 assert_eq!(e.name, dev, "stat names the device");
347 }
348 assert_eq!(
349 fs.stat(Path::new("nope")).await.unwrap_err().kind(),
350 io::ErrorKind::NotFound
351 );
352 }
353}