use std::io;
use std::time::{SystemTime, UNIX_EPOCH};
use super::{Attrs, DirEntry, MountFs, NodeKind};
const S_IFCHR: u32 = 0o020_000;
const S_IFDIR: u32 = 0o040_000;
const S_IFBLK: u32 = 0o060_000;
const S_IFLNK: u32 = 0o120_000;
const CHR_MODE: u32 = S_IFCHR | 0o666;
const BLK_MODE: u32 = S_IFBLK | 0o666;
const ROOT_INODE: u64 = 1;
const fn makedev(major: u64, minor: u64) -> u64 {
((major & 0xfff) << 8) | (minor & 0xff) | ((minor & !0xff) << 12) | ((major & !0xfff) << 32)
}
const DEVICES: &[(&str, u64, u64, u64)] = &[
("null", 2, 1, 3),
("zero", 3, 1, 5),
("full", 4, 1, 7),
("random", 5, 1, 8),
("urandom", 6, 1, 9),
("tty", 7, 5, 0),
("console", 8, 5, 1),
("ptmx", 9, 5, 2),
("kmsg", 10, 1, 11),
("loop-control", 17, 10, 237),
("rtc0", 18, 254, 0),
("hpet", 19, 10, 228),
("fuse", 20, 10, 229),
("vga_arbiter", 21, 10, 63),
];
const NET_DEVICES: &[(&str, u64, u64, u64)] = &[("tun", 22, 10, 200)];
const BLOCK_DEVICES: &[(&str, u64, u64, u64)] = &[
("loop0", 23, 7, 0),
("loop1", 24, 7, 1),
("loop2", 25, 7, 2),
("loop3", 26, 7, 3),
("loop4", 27, 7, 4),
("loop5", 28, 7, 5),
("loop6", 29, 7, 6),
("loop7", 30, 7, 7),
("ram0", 31, 1, 0),
];
const DIRS: &[(&str, u64)] = &[
("pts", 11),
("shm", 12),
("net", 32),
("mapper", 33),
("disk", 34),
("char", 35),
("block", 36),
("hugepages", 37),
];
const SYMLINKS: &[(&str, u64, &str)] = &[
("fd", 13, "/proc/self/fd"),
("stdin", 14, "/proc/self/fd/0"),
("stdout", 15, "/proc/self/fd/1"),
("stderr", 16, "/proc/self/fd/2"),
("rtc", 38, "rtc0"),
];
#[derive(Debug)]
pub struct DevFs {
rng: u64,
}
impl Default for DevFs {
fn default() -> Self {
Self::new()
}
}
impl DevFs {
#[must_use]
pub fn new() -> Self {
Self { rng: 0 }
}
fn lookup(name: &str) -> Option<(u64, u64)> {
DEVICES
.iter()
.find(|(n, ..)| *n == name)
.map(|(_, inode, major, minor)| (*inode, makedev(*major, *minor)))
}
fn block_lookup(name: &str) -> Option<(u64, u64)> {
BLOCK_DEVICES
.iter()
.find(|(n, ..)| *n == name)
.map(|(_, inode, major, minor)| (*inode, makedev(*major, *minor)))
}
fn net_lookup(rel: &str) -> Option<(u64, u64)> {
let name = rel.strip_prefix("net/")?;
NET_DEVICES
.iter()
.find(|(n, ..)| *n == name)
.map(|(_, inode, major, minor)| (*inode, makedev(*major, *minor)))
}
fn dir_lookup(name: &str) -> Option<u64> {
DIRS.iter()
.find(|(n, _)| *n == name)
.map(|(_, inode)| *inode)
}
fn symlink_lookup(name: &str) -> Option<(u64, &'static str)> {
SYMLINKS
.iter()
.find(|(n, ..)| *n == name)
.map(|(_, inode, target)| (*inode, *target))
}
fn dev_attrs((inode, rdev): (u64, u64)) -> Attrs {
Attrs {
kind: NodeKind::CharDevice,
size: 0,
mode: CHR_MODE,
uid: 0,
gid: 0,
mtime: 0,
inode,
nlink: 1,
rdev,
}
}
fn blk_attrs((inode, rdev): (u64, u64)) -> Attrs {
Attrs {
kind: NodeKind::BlockDevice,
size: 0,
mode: BLK_MODE,
uid: 0,
gid: 0,
mtime: 0,
inode,
nlink: 1,
rdev,
}
}
fn next_u64(&mut self) -> u64 {
if self.rng == 0 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as u64);
self.rng = nanos | 1;
}
let mut x = self.rng;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.rng = x;
x
}
fn fill_random(&mut self, buf: &mut [u8]) {
for chunk in buf.chunks_mut(8) {
let bytes = self.next_u64().to_le_bytes();
chunk.copy_from_slice(&bytes[..chunk.len()]);
}
}
}
fn enoent() -> io::Error {
io::Error::from_raw_os_error(2) }
fn enospc() -> io::Error {
io::Error::from_raw_os_error(28) }
fn eisdir() -> io::Error {
io::Error::from_raw_os_error(21) }
fn enotdir() -> io::Error {
io::Error::from_raw_os_error(20) }
fn einval() -> io::Error {
io::Error::from_raw_os_error(22) }
fn eagain() -> io::Error {
io::Error::from_raw_os_error(11) }
impl MountFs for DevFs {
fn read_only(&self) -> bool {
false
}
fn stat(&mut self, rel: &str) -> Option<Attrs> {
if rel.is_empty() {
return Some(Attrs {
kind: NodeKind::Dir,
size: 0,
mode: S_IFDIR | 0o755,
uid: 0,
gid: 0,
mtime: 0,
inode: ROOT_INODE,
nlink: 1,
rdev: 0,
});
}
if let Some(attrs) = Self::lookup(rel).map(Self::dev_attrs) {
return Some(attrs);
}
if let Some(attrs) = Self::block_lookup(rel).map(Self::blk_attrs) {
return Some(attrs);
}
if let Some(attrs) = Self::net_lookup(rel).map(Self::dev_attrs) {
return Some(attrs);
}
if let Some(inode) = Self::dir_lookup(rel) {
return Some(Attrs {
kind: NodeKind::Dir,
size: 0,
mode: S_IFDIR | 0o755,
uid: 0,
gid: 0,
mtime: 0,
inode,
nlink: 1,
rdev: 0,
});
}
Self::symlink_lookup(rel).map(|(inode, target)| Attrs {
kind: NodeKind::Symlink,
size: target.len() as u64,
mode: S_IFLNK | 0o777,
uid: 0,
gid: 0,
mtime: 0,
inode,
nlink: 1,
rdev: 0,
})
}
fn read_at(&mut self, rel: &str, _off: u64, buf: &mut [u8]) -> io::Result<usize> {
match rel {
"null" | "tty" | "console" | "kmsg" | "ptmx" | "loop-control" | "rtc0" | "hpet"
| "fuse" | "vga_arbiter" => Ok(0),
"zero" | "full" => {
buf.fill(0);
Ok(buf.len())
}
"random" | "urandom" => {
self.fill_random(buf);
Ok(buf.len())
}
"net/tun" => Err(eagain()),
_ if Self::block_lookup(rel).is_some() => Ok(0),
_ if Self::dir_lookup(rel).is_some() => Err(eisdir()),
_ if Self::symlink_lookup(rel).is_some() => Err(einval()),
_ => Err(enoent()),
}
}
fn readdir(&mut self, rel: &str) -> io::Result<Vec<DirEntry>> {
match rel {
"" => {
let mut out: Vec<DirEntry> = DEVICES
.iter()
.map(|(name, inode, ..)| DirEntry {
name: (*name).to_string(),
kind: NodeKind::CharDevice,
inode: *inode,
})
.collect();
out.extend(BLOCK_DEVICES.iter().map(|(name, inode, ..)| DirEntry {
name: (*name).to_string(),
kind: NodeKind::BlockDevice,
inode: *inode,
}));
out.extend(DIRS.iter().map(|(name, inode)| DirEntry {
name: (*name).to_string(),
kind: NodeKind::Dir,
inode: *inode,
}));
out.extend(SYMLINKS.iter().map(|(name, inode, _)| DirEntry {
name: (*name).to_string(),
kind: NodeKind::Symlink,
inode: *inode,
}));
Ok(out)
}
"net" => Ok(NET_DEVICES
.iter()
.map(|(name, inode, ..)| DirEntry {
name: (*name).to_string(),
kind: NodeKind::CharDevice,
inode: *inode,
})
.collect()),
"pts" | "shm" | "mapper" | "disk" | "char" | "block" | "hugepages" => Ok(Vec::new()),
_ if Self::lookup(rel).is_some()
|| Self::block_lookup(rel).is_some()
|| Self::net_lookup(rel).is_some()
|| Self::symlink_lookup(rel).is_some() =>
{
Err(enotdir())
}
_ => Err(enoent()),
}
}
fn write_at(&mut self, rel: &str, _off: u64, buf: &[u8]) -> io::Result<usize> {
match rel {
"null" | "zero" | "random" | "urandom" | "tty" | "console" | "kmsg" | "ptmx"
| "loop-control" | "rtc0" | "hpet" | "fuse" | "vga_arbiter" => Ok(buf.len()),
"full" => Err(enospc()),
"net/tun" => Ok(0),
_ if Self::block_lookup(rel).is_some() => Ok(buf.len()),
_ if Self::dir_lookup(rel).is_some() => Err(eisdir()),
_ => Err(enoent()),
}
}
fn readlink(&mut self, rel: &str) -> io::Result<String> {
if let Some((_, target)) = Self::symlink_lookup(rel) {
return Ok(target.to_string());
}
if rel.is_empty()
|| Self::lookup(rel).is_some()
|| Self::block_lookup(rel).is_some()
|| Self::net_lookup(rel).is_some()
|| Self::dir_lookup(rel).is_some()
{
return Err(einval()); }
Err(enoent())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_reads_zeros() {
let mut fs = DevFs::new();
let mut buf = [0xffu8; 16];
assert_eq!(fs.read_at("zero", 0, &mut buf).unwrap(), 16);
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn null_reads_eof_and_accepts_writes() {
let mut fs = DevFs::new();
let mut buf = [0u8; 8];
assert_eq!(fs.read_at("null", 0, &mut buf).unwrap(), 0);
assert_eq!(fs.write_at("null", 0, b"discarded").unwrap(), 9);
}
#[test]
fn full_write_errors_with_enospc() {
let mut fs = DevFs::new();
let err = fs.write_at("full", 0, b"x").unwrap_err();
assert_eq!(err.raw_os_error(), Some(28));
let mut buf = [0xffu8; 4];
assert_eq!(fs.read_at("full", 0, &mut buf).unwrap(), 4);
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn urandom_fills_non_zero_bytes() {
let mut fs = DevFs::new();
let mut buf = [0u8; 64];
assert_eq!(fs.read_at("urandom", 0, &mut buf).unwrap(), 64);
assert!(buf.iter().any(|&b| b != 0));
}
#[test]
fn random_accepts_writes() {
let mut fs = DevFs::new();
assert_eq!(fs.write_at("random", 0, b"seed").unwrap(), 4);
assert_eq!(fs.write_at("urandom", 0, b"seed").unwrap(), 4);
}
#[test]
fn readdir_lists_all_nodes() {
let mut fs = DevFs::new();
let mut names: Vec<_> = fs
.readdir("")
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
names.sort();
assert_eq!(
names,
vec![
"block",
"char",
"console",
"disk",
"fd",
"full",
"fuse",
"hpet",
"hugepages",
"kmsg",
"loop-control",
"loop0",
"loop1",
"loop2",
"loop3",
"loop4",
"loop5",
"loop6",
"loop7",
"mapper",
"net",
"null",
"ptmx",
"pts",
"ram0",
"random",
"rtc",
"rtc0",
"shm",
"stderr",
"stdin",
"stdout",
"tty",
"urandom",
"vga_arbiter",
"zero",
]
);
assert!(
fs.readdir("")
.unwrap()
.iter()
.filter(|e| DEVICES.iter().any(|(n, ..)| *n == e.name))
.all(|e| e.kind == NodeKind::CharDevice)
);
assert!(
fs.readdir("")
.unwrap()
.iter()
.filter(|e| BLOCK_DEVICES.iter().any(|(n, ..)| *n == e.name))
.all(|e| e.kind == NodeKind::BlockDevice)
);
}
#[test]
fn stat_reports_char_devices() {
let mut fs = DevFs::new();
let a = fs.stat("null").unwrap();
assert_eq!(a.kind, NodeKind::CharDevice);
assert_eq!(a.mode, S_IFCHR | 0o666);
assert_eq!(a.size, 0);
assert_eq!(a.nlink, 1);
assert_ne!(
fs.stat("null").unwrap().inode,
fs.stat("zero").unwrap().inode
);
assert_eq!(fs.stat("").unwrap().kind, NodeKind::Dir);
}
#[test]
fn stat_reports_canonical_device_numbers() {
let mut fs = DevFs::new();
assert_eq!(fs.stat("null").unwrap().rdev, makedev(1, 3));
assert_eq!(fs.stat("zero").unwrap().rdev, makedev(1, 5));
assert_eq!(fs.stat("full").unwrap().rdev, makedev(1, 7));
assert_eq!(fs.stat("random").unwrap().rdev, makedev(1, 8));
assert_eq!(fs.stat("urandom").unwrap().rdev, makedev(1, 9));
assert_eq!(fs.stat("tty").unwrap().rdev, makedev(5, 0));
assert_eq!(fs.stat("null").unwrap().rdev, 256 + 3);
assert_eq!(fs.stat("").unwrap().rdev, 0);
}
#[test]
fn unknown_name_is_enoent() {
let mut fs = DevFs::new();
assert!(fs.stat("nope").is_none());
let mut buf = [0u8; 4];
assert_eq!(
fs.read_at("nope", 0, &mut buf).unwrap_err().raw_os_error(),
Some(2)
);
assert_eq!(
fs.write_at("nope", 0, b"x").unwrap_err().raw_os_error(),
Some(2)
);
}
#[test]
fn not_read_only() {
assert!(!DevFs::new().read_only());
}
#[test]
fn console_ptmx_kmsg_have_canonical_numbers_and_behave_like_devices() {
let mut fs = DevFs::new();
assert_eq!(fs.stat("console").unwrap().rdev, makedev(5, 1));
assert_eq!(fs.stat("ptmx").unwrap().rdev, makedev(5, 2));
assert_eq!(fs.stat("kmsg").unwrap().rdev, makedev(1, 11));
for name in ["console", "ptmx", "kmsg"] {
let a = fs.stat(name).unwrap();
assert_eq!(a.kind, NodeKind::CharDevice);
assert_eq!(a.mode, S_IFCHR | 0o666);
let mut buf = [0u8; 4];
assert_eq!(fs.read_at(name, 0, &mut buf).unwrap(), 0);
assert_eq!(fs.write_at(name, 0, b"x").unwrap(), 1);
}
}
#[test]
fn symlinks_resolve_to_proc_self_fd() {
let mut fs = DevFs::new();
assert_eq!(fs.readlink("fd").unwrap(), "/proc/self/fd");
assert_eq!(fs.readlink("stdin").unwrap(), "/proc/self/fd/0");
assert_eq!(fs.readlink("stdout").unwrap(), "/proc/self/fd/1");
assert_eq!(fs.readlink("stderr").unwrap(), "/proc/self/fd/2");
for name in ["fd", "stdin", "stdout", "stderr"] {
let a = fs.stat(name).unwrap();
assert_eq!(a.kind, NodeKind::Symlink);
assert_eq!(a.mode, S_IFLNK | 0o777);
}
}
#[test]
fn pts_and_shm_dirs_present_and_listable() {
let mut fs = DevFs::new();
for name in ["pts", "shm"] {
assert_eq!(fs.stat(name).unwrap().kind, NodeKind::Dir);
assert_eq!(fs.readdir(name).unwrap().len(), 0);
}
}
#[test]
fn readlink_on_non_symlink_is_einval() {
let mut fs = DevFs::new();
assert_eq!(fs.readlink("null").unwrap_err().raw_os_error(), Some(22));
assert_eq!(fs.readlink("pts").unwrap_err().raw_os_error(), Some(22));
assert_eq!(fs.readlink("").unwrap_err().raw_os_error(), Some(22));
}
#[test]
fn loop0_is_block_device_with_correct_rdev() {
let mut fs = DevFs::new();
let a = fs.stat("loop0").unwrap();
assert_eq!(a.kind, NodeKind::BlockDevice);
assert_eq!(a.mode, S_IFBLK | 0o666);
assert_eq!(a.rdev, makedev(7, 0));
let mut buf = [0xffu8; 8];
assert_eq!(fs.read_at("loop0", 0, &mut buf).unwrap(), 0);
assert_eq!(fs.write_at("loop0", 0, b"data").unwrap(), 4);
}
#[test]
fn loop_devices_and_ram0_have_canonical_numbers() {
let mut fs = DevFs::new();
for i in 0..8u64 {
let name = format!("loop{i}");
assert_eq!(fs.stat(&name).unwrap().rdev, makedev(7, i));
assert_eq!(fs.stat(&name).unwrap().kind, NodeKind::BlockDevice);
}
assert_eq!(fs.stat("ram0").unwrap().rdev, makedev(1, 0));
assert_eq!(fs.stat("ram0").unwrap().kind, NodeKind::BlockDevice);
}
#[test]
fn net_tun_is_char_device_with_correct_rdev_and_stub_io() {
let mut fs = DevFs::new();
let a = fs.stat("net/tun").unwrap();
assert_eq!(a.kind, NodeKind::CharDevice);
assert_eq!(a.mode, S_IFCHR | 0o666);
assert_eq!(a.rdev, makedev(10, 200));
let mut buf = [0u8; 8];
assert_eq!(
fs.read_at("net/tun", 0, &mut buf)
.unwrap_err()
.raw_os_error(),
Some(11) );
assert_eq!(fs.write_at("net/tun", 0, b"packet").unwrap(), 0);
}
#[test]
fn ls_dev_net_lists_tun() {
let mut fs = DevFs::new();
assert_eq!(fs.stat("net").unwrap().kind, NodeKind::Dir);
let names: Vec<_> = fs
.readdir("net")
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(names, vec!["tun"]);
}
#[test]
fn new_char_devices_have_canonical_numbers() {
let mut fs = DevFs::new();
assert_eq!(fs.stat("loop-control").unwrap().rdev, makedev(10, 237));
assert_eq!(fs.stat("rtc0").unwrap().rdev, makedev(254, 0));
assert_eq!(fs.stat("hpet").unwrap().rdev, makedev(10, 228));
assert_eq!(fs.stat("fuse").unwrap().rdev, makedev(10, 229));
assert_eq!(fs.stat("vga_arbiter").unwrap().rdev, makedev(10, 63));
for name in ["loop-control", "rtc0", "hpet", "fuse", "vga_arbiter"] {
let a = fs.stat(name).unwrap();
assert_eq!(a.kind, NodeKind::CharDevice);
assert_eq!(a.mode, S_IFCHR | 0o666);
}
}
#[test]
fn rtc_symlink_resolves_to_rtc0() {
let mut fs = DevFs::new();
assert_eq!(fs.readlink("rtc").unwrap(), "rtc0");
assert_eq!(fs.stat("rtc").unwrap().kind, NodeKind::Symlink);
assert_eq!(fs.stat("rtc0").unwrap().kind, NodeKind::CharDevice);
}
#[test]
fn new_dirs_present_and_listable() {
let mut fs = DevFs::new();
for name in ["net", "mapper", "disk", "char", "block", "hugepages"] {
assert_eq!(fs.stat(name).unwrap().kind, NodeKind::Dir, "{name}");
}
for name in ["mapper", "disk", "char", "block", "hugepages"] {
assert_eq!(fs.readdir(name).unwrap().len(), 0, "{name}");
}
}
#[test]
fn existing_symlinks_still_resolve() {
let mut fs = DevFs::new();
assert_eq!(fs.readlink("fd").unwrap(), "/proc/self/fd");
assert_eq!(fs.readlink("stdin").unwrap(), "/proc/self/fd/0");
assert_eq!(fs.readlink("stdout").unwrap(), "/proc/self/fd/1");
assert_eq!(fs.readlink("stderr").unwrap(), "/proc/self/fd/2");
}
}