use crate::traits::{DirEntry, DirEntryKind, Filesystem, ReadRange};
use async_trait::async_trait;
use std::io;
use std::path::Path;
const MAX_DEVICE_READ_BYTES: u64 = 64 * 1024 * 1024;
#[derive(Debug, Default, Clone, Copy)]
pub struct DevFs;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Device {
Null,
Zero,
Random,
}
impl Device {
fn name(self) -> &'static str {
match self {
Device::Null => "null",
Device::Zero => "zero",
Device::Random => "urandom",
}
}
}
impl DevFs {
pub fn new() -> Self {
Self
}
const NAMES: [&'static str; 4] = ["null", "random", "urandom", "zero"];
fn device(path: &Path) -> Option<Device> {
match path.to_str()?.trim_start_matches('/') {
"null" => Some(Device::Null),
"zero" => Some(Device::Zero),
"urandom" | "random" => Some(Device::Random),
_ => None,
}
}
fn is_root(path: &Path) -> bool {
let s = path.to_string_lossy();
let trimmed = s.trim_matches('/');
trimmed.is_empty() || trimmed == "."
}
fn not_found(path: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::NotFound,
format!("no such device: /dev/{}", path.display()),
)
}
fn unbounded(name: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"/dev/{name} is an endless device; reading the whole of it is unbounded. \
Read a fixed number of bytes instead, e.g. `head -c 32 /dev/{name}`"
),
)
}
fn entry(name: &str) -> DirEntry {
DirEntry {
name: name.to_string(),
kind: DirEntryKind::File,
size: 0,
modified: None,
permissions: None,
symlink_target: None,
}
}
}
#[async_trait]
impl Filesystem for DevFs {
async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
match Self::device(path) {
Some(Device::Null) => Ok(Vec::new()),
Some(dev) => Err(Self::unbounded(dev.name())), None => Err(Self::not_found(path)),
}
}
async fn read_range(&self, path: &Path, range: Option<ReadRange>) -> io::Result<Vec<u8>> {
let Some(dev) = Self::device(path) else {
return Err(Self::not_found(path));
};
if dev == Device::Null {
return Ok(Vec::new());
}
let limit = match range.and_then(|r| r.limit) {
Some(n) => n,
None => return Err(Self::unbounded(dev.name())),
};
if limit > MAX_DEVICE_READ_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"requested {limit} bytes from /dev/{} exceeds the device read cap \
of {MAX_DEVICE_READ_BYTES} bytes",
dev.name()
),
));
}
let mut buf = vec![0u8; limit as usize];
if dev == Device::Random {
getrandom::fill(&mut buf).map_err(|e| {
io::Error::other(format!("/dev/{}: entropy source failed: {e}", dev.name()))
})?;
}
Ok(buf) }
async fn write(&self, path: &Path, _data: &[u8]) -> io::Result<()> {
match Self::device(path) {
Some(_) => Ok(()),
None => Err(Self::not_found(path)),
}
}
async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
if Self::is_root(path) {
return Ok(Self::NAMES.iter().map(|n| Self::entry(n)).collect());
}
if Self::device(path).is_some() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("not a directory: /dev/{}", path.display()),
));
}
Err(Self::not_found(path))
}
async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
if Self::is_root(path) {
return Ok(DirEntry {
name: "dev".to_string(),
kind: DirEntryKind::Directory,
size: 0,
modified: None,
permissions: None,
symlink_target: None,
});
}
match Self::device(path) {
Some(_) => Ok(Self::entry(
path.to_str().unwrap_or_default().trim_start_matches('/'),
)),
None => Err(Self::not_found(path)),
}
}
async fn mkdir(&self, path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("/dev is read-only: cannot create {}", path.display()),
))
}
async fn remove(&self, path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("/dev is read-only: cannot remove {}", path.display()),
))
}
fn read_only(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn null_reads_empty() {
let fs = DevFs::new();
assert_eq!(fs.read(Path::new("null")).await.unwrap(), b"");
assert_eq!(
fs.read_range(Path::new("null"), Some(ReadRange::bytes(0, 16)))
.await
.unwrap(),
b""
);
}
#[tokio::test]
async fn null_discards_writes() {
let fs = DevFs::new();
fs.write(Path::new("null"), b"anything at all").await.unwrap();
}
#[tokio::test]
async fn zero_counted_read_yields_zeros() {
let fs = DevFs::new();
let out = fs
.read_range(Path::new("zero"), Some(ReadRange::bytes(0, 8)))
.await
.unwrap();
assert_eq!(out, vec![0u8; 8]);
}
#[tokio::test]
async fn zero_whole_read_is_loud_error() {
let fs = DevFs::new();
let err = fs.read(Path::new("zero")).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("head -c"), "should name the fix: {err}");
let err = fs.read_range(Path::new("zero"), None).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn zero_read_cap_is_enforced() {
let fs = DevFs::new();
let err = fs
.read_range(Path::new("zero"), Some(ReadRange::bytes(0, MAX_DEVICE_READ_BYTES + 1)))
.await
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("cap"), "should mention the cap: {err}");
}
#[tokio::test]
async fn urandom_counted_read_is_random_and_sized() {
let fs = DevFs::new();
let a = fs
.read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
.await
.unwrap();
assert_eq!(a.len(), 32, "exact byte count");
let b = fs
.read_range(Path::new("urandom"), Some(ReadRange::bytes(0, 32)))
.await
.unwrap();
assert_ne!(a, b, "entropy: two draws must differ");
let c = fs
.read_range(Path::new("random"), Some(ReadRange::bytes(0, 8)))
.await
.unwrap();
assert_eq!(c.len(), 8);
}
#[tokio::test]
async fn urandom_whole_read_is_loud_error() {
let fs = DevFs::new();
let err = fs.read(Path::new("urandom")).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("head -c"), "names the fix: {err}");
}
#[tokio::test]
async fn unknown_device_is_not_found() {
let fs = DevFs::new();
assert_eq!(
fs.read(Path::new("sda")).await.unwrap_err().kind(),
io::ErrorKind::NotFound
);
assert_eq!(
fs.write(Path::new("sda"), b"x").await.unwrap_err().kind(),
io::ErrorKind::NotFound
);
}
#[tokio::test]
async fn list_shows_devices() {
let fs = DevFs::new();
let names: Vec<_> = fs
.list(Path::new(""))
.await
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(
names,
vec![
"null".to_string(),
"random".to_string(),
"urandom".to_string(),
"zero".to_string()
]
);
}
#[tokio::test]
async fn stat_devices_and_root() {
let fs = DevFs::new();
assert_eq!(fs.stat(Path::new("")).await.unwrap().kind, DirEntryKind::Directory);
for dev in ["null", "zero", "urandom", "random"] {
let e = fs.stat(Path::new(dev)).await.unwrap();
assert_eq!(e.kind, DirEntryKind::File, "{dev}");
assert_eq!(e.name, dev, "stat names the device");
}
assert_eq!(
fs.stat(Path::new("nope")).await.unwrap_err().kind(),
io::ErrorKind::NotFound
);
}
}