pub mod devfs;
#[cfg(feature = "fstool")]
pub mod fstool;
pub mod procfs;
use std::collections::BTreeMap;
use crate::emulator::context::{FileAccess, VirtualFs};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeKind {
File,
Dir,
Symlink,
CharDevice,
}
#[derive(Debug, Clone, Copy)]
pub struct Attrs {
pub kind: NodeKind,
pub size: u64,
pub mode: u32,
pub mtime: u64,
pub inode: u64,
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: String,
pub kind: NodeKind,
}
pub trait MountFs: std::fmt::Debug {
fn stat(&mut self, rel: &str) -> Option<Attrs>;
fn read_at(&mut self, rel: &str, off: u64, buf: &mut [u8]) -> std::io::Result<usize>;
fn readdir(&mut self, rel: &str) -> std::io::Result<Vec<DirEntry>>;
fn read_only(&self) -> bool {
true
}
fn write_at(&mut self, _rel: &str, _off: u64, _data: &[u8]) -> std::io::Result<usize> {
Err(read_only_err())
}
fn create(&mut self, _rel: &str, _mode: u32) -> std::io::Result<()> {
Err(read_only_err())
}
fn mkdir(&mut self, _rel: &str, _mode: u32) -> std::io::Result<()> {
Err(read_only_err())
}
fn unlink(&mut self, _rel: &str) -> std::io::Result<()> {
Err(read_only_err())
}
fn rmdir(&mut self, _rel: &str) -> std::io::Result<()> {
Err(read_only_err())
}
fn truncate(&mut self, _rel: &str, _len: u64) -> std::io::Result<()> {
Err(read_only_err())
}
fn symlink(&mut self, _target: &str, _rel: &str) -> std::io::Result<()> {
Err(read_only_err())
}
fn rename(&mut self, _old_rel: &str, _new_rel: &str) -> std::io::Result<()> {
Err(read_only_err())
}
fn hardlink(&mut self, _target_rel: &str, _new_rel: &str) -> std::io::Result<()> {
Err(read_only_err())
}
fn readlink(&mut self, _rel: &str) -> std::io::Result<String> {
Err(std::io::Error::from(std::io::ErrorKind::InvalidInput))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn read_only_err() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "read-only filesystem")
}
const OVERLAY_HANDLE_BASE: u32 = 0x6A00_0000;
#[derive(Debug, Clone)]
struct OverlayHandle {
mount: usize,
path: String,
pos: u64,
access: FileAccess,
}
struct Mount {
point: String,
fs: Box<dyn MountFs>,
}
impl std::fmt::Debug for Mount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Mount({:?})", self.point)
}
}
#[derive(Debug)]
pub struct MountTable {
root: VirtualFs,
overlays: Vec<Mount>,
handles: BTreeMap<u32, OverlayHandle>,
next_handle: u32,
}
impl Default for MountTable {
fn default() -> Self {
Self::new()
}
}
impl MountTable {
#[must_use]
pub fn new() -> Self {
Self::with_root(VirtualFs::new())
}
#[must_use]
pub fn with_root(root: VirtualFs) -> Self {
Self {
root,
overlays: Vec::new(),
handles: BTreeMap::new(),
next_handle: 0,
}
}
#[must_use]
pub fn root(&self) -> &VirtualFs {
&self.root
}
pub fn root_mut(&mut self) -> &mut VirtualFs {
&mut self.root
}
pub fn mount(&mut self, point: &str, fs: Box<dyn MountFs>) {
let point = norm_abs(point);
self.overlays.retain(|m| m.point != point);
self.overlays.push(Mount { point, fs });
self.overlays
.sort_by(|a, b| b.point.len().cmp(&a.point.len()));
}
fn resolve(&self, abs: &str) -> Option<(usize, String)> {
let abs = norm_abs(abs);
for (i, m) in self.overlays.iter().enumerate() {
if let Some(rel) = under_mount(&m.point, &abs) {
return Some((i, rel));
}
}
None
}
pub fn insert(&mut self, path: &str, bytes: Vec<u8>) {
self.root.insert(path, bytes);
}
#[must_use]
pub fn read(&self, path: &str) -> Option<&[u8]> {
self.root.read(path)
}
#[must_use]
pub fn contains(&mut self, path: &str) -> bool {
if let Some((i, rel)) = self.resolve(path) {
return self.overlays[i].fs.stat(&rel).is_some();
}
self.root.contains(path)
}
pub fn write_path(&mut self, path: &str, bytes: Vec<u8>) {
self.root.write_path(path, bytes);
}
pub fn remove(&mut self, path: &str) -> bool {
self.root.remove(path)
}
pub fn list(&self) -> impl Iterator<Item = (&str, usize)> {
self.root.list()
}
pub fn open(&mut self, path: &str, access: FileAccess) -> Option<u32> {
let Some((mount, rel)) = self.resolve(path) else {
return self.root.open(path, access);
};
let fs = &mut self.overlays[mount].fs;
let exists = fs.stat(&rel).is_some();
if !exists {
if !access_writes(access) || fs.read_only() {
return None;
}
fs.create(&rel, 0o644).ok()?;
}
let handle = OVERLAY_HANDLE_BASE.wrapping_add(self.next_handle);
self.next_handle = self.next_handle.wrapping_add(1);
self.handles.insert(
handle,
OverlayHandle {
mount,
path: rel,
pos: 0,
access,
},
);
Some(handle)
}
pub fn close(&mut self, handle: u32) -> bool {
if self.handles.remove(&handle).is_some() {
return true;
}
self.root.close(handle)
}
pub fn read_handle(&mut self, handle: u32, buf: &mut [u8]) -> Option<usize> {
let Some(h) = self.handles.get_mut(&handle) else {
return self.root.read_handle(handle, buf);
};
if !access_reads(h.access) {
return None;
}
let n = self.overlays[h.mount]
.fs
.read_at(&h.path, h.pos, buf)
.ok()?;
h.pos = h.pos.wrapping_add(n as u64);
Some(n)
}
pub fn write_handle(&mut self, handle: u32, data: &[u8]) -> Option<usize> {
let Some(h) = self.handles.get_mut(&handle) else {
return self.root.write_handle(handle, data);
};
if !access_writes(h.access) {
return None;
}
let n = self.overlays[h.mount]
.fs
.write_at(&h.path, h.pos, data)
.ok()?;
h.pos = h.pos.wrapping_add(n as u64);
Some(n)
}
pub fn seek(&mut self, handle: u32, pos: u64) -> Option<u64> {
if let Some(h) = self.handles.get_mut(&handle) {
h.pos = pos;
return Some(pos);
}
self.root.seek(handle, pos)
}
pub fn seek_handle(&mut self, handle: u32, off: i32, whence: u8) -> Option<u64> {
let Some(h) = self.handles.get(&handle) else {
return self.root.seek_handle(handle, off, whence);
};
let base = match whence {
1 => h.pos as i64,
2 => self.size(handle)? as i64,
_ => 0,
};
let pos = (base + i64::from(off)).max(0) as u64;
self.handles.get_mut(&handle)?.pos = pos;
Some(pos)
}
#[must_use]
pub fn size(&mut self, handle: u32) -> Option<u64> {
if let Some(h) = self.handles.get(&handle) {
let (mount, path) = (h.mount, h.path.clone());
return Some(self.overlays[mount].fs.stat(&path)?.size);
}
self.root.size(handle)
}
#[must_use]
pub fn owns(&self, handle: u32) -> bool {
self.handles.contains_key(&handle) || self.root.owns(handle)
}
#[must_use]
pub fn resolve_symlinks(&mut self, path: &str) -> String {
let mut p = norm_abs(path);
for _ in 0..16 {
match self.stat_path(&p) {
Some(a) if matches!(a.kind, NodeKind::Symlink) => {
let Ok(target) = self.readlink_path(&p) else {
break;
};
p = if target.starts_with('/') {
norm_abs(&target)
} else {
let dir = p.rsplit_once('/').map_or("", |(d, _)| d);
norm_abs(&format!("{dir}/{target}"))
};
}
_ => break,
}
}
p
}
#[must_use]
pub fn stat_follow(&mut self, abs: &str) -> Option<Attrs> {
let target = self.resolve_symlinks(abs);
self.stat_path(&target)
}
#[must_use]
pub fn read_file(&mut self, path: &str) -> Option<Vec<u8>> {
let p = self.resolve_symlinks(path);
let h = self.open(&p, FileAccess::Read)?;
let mut out = Vec::new();
let mut buf = vec![0u8; 64 * 1024];
while let Some(n) = self.read_handle(h, &mut buf) {
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
}
self.close(h);
Some(out)
}
#[must_use]
pub fn stat_path(&mut self, abs: &str) -> Option<Attrs> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.stat(&rel);
}
let (is_dir, size) = self.root.stat_node(abs)?;
Some(Attrs {
kind: if is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
size,
mode: if is_dir { 0o755 } else { 0o644 },
mtime: 0,
inode: 0,
})
}
pub fn readdir_path(&mut self, abs: &str) -> std::io::Result<Vec<DirEntry>> {
if let Some((i, rel)) = self.resolve(abs) {
let mut entries = self.overlays[i].fs.readdir(&rel)?;
entries.retain(|e| e.name != "." && e.name != "..");
return Ok(entries);
}
let mut out = match self.root.readdir(abs) {
Some(children) => children
.into_iter()
.map(|(name, is_dir)| DirEntry {
name,
kind: if is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
})
.collect::<Vec<_>>(),
None => return Err(std::io::Error::from_raw_os_error(20)), };
let parent = norm_abs(abs);
for m in &self.overlays {
if let Some((dir, base)) = m.point.rsplit_once('/') {
let dir = if dir.is_empty() { "/" } else { dir };
if dir == parent && !out.iter().any(|e| e.name == base) {
out.push(DirEntry {
name: base.to_string(),
kind: NodeKind::Dir,
});
}
}
}
Ok(out)
}
pub fn mkdir_path(&mut self, abs: &str, mode: u32) -> std::io::Result<()> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.mkdir(&rel, mode);
}
Ok(()) }
pub fn unlink_path(&mut self, abs: &str) -> std::io::Result<()> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.unlink(&rel);
}
if self.root.remove(abs) {
Ok(())
} else {
Err(std::io::Error::from_raw_os_error(2)) }
}
pub fn rmdir_path(&mut self, abs: &str) -> std::io::Result<()> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.rmdir(&rel);
}
Ok(()) }
pub fn symlink_path(&mut self, target: &str, abs: &str) -> std::io::Result<()> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.symlink(target, &rel);
}
Err(read_only_err()) }
pub fn readlink_path(&mut self, abs: &str) -> std::io::Result<String> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.readlink(&rel);
}
Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)) }
pub fn rename_path(&mut self, old_abs: &str, new_abs: &str) -> std::io::Result<()> {
let old = self.resolve(old_abs);
let new = self.resolve(new_abs);
if let (Some((i, old_rel)), Some((j, new_rel))) = (old, new) {
if i == j {
return self.overlays[i].fs.rename(&old_rel, &new_rel);
}
}
Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
}
pub fn hardlink_path(&mut self, target_abs: &str, new_abs: &str) -> std::io::Result<()> {
let target = self.resolve(target_abs);
let new = self.resolve(new_abs);
if let (Some((i, target_rel)), Some((j, new_rel))) = (target, new) {
if i == j {
return self.overlays[i].fs.hardlink(&target_rel, &new_rel);
}
}
Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
}
pub fn truncate_path(&mut self, abs: &str, len: u64) -> std::io::Result<()> {
if let Some((i, rel)) = self.resolve(abs) {
return self.overlays[i].fs.truncate(&rel, len);
}
if self.root.truncate(abs, len) {
Ok(())
} else {
Err(std::io::Error::from_raw_os_error(2)) }
}
pub fn flush_all(&mut self) -> std::io::Result<()> {
let mut first_err = None;
for m in &mut self.overlays {
if let Err(e) = m.fs.flush() {
first_err.get_or_insert(e);
}
}
first_err.map_or(Ok(()), Err)
}
}
impl Drop for MountTable {
fn drop(&mut self) {
let _ = self.flush_all();
}
}
fn access_reads(a: FileAccess) -> bool {
matches!(a, FileAccess::Read | FileAccess::ReadWrite)
}
fn access_writes(a: FileAccess) -> bool {
matches!(a, FileAccess::Write | FileAccess::ReadWrite)
}
fn norm_abs(path: &str) -> String {
let mut s = String::with_capacity(path.len() + 1);
if !path.starts_with('/') && !path.starts_with('\\') {
s.push('/');
}
let mut prev_slash = false;
for c in path.chars() {
let c = if c == '\\' { '/' } else { c };
if c == '/' {
if prev_slash {
continue;
}
prev_slash = true;
} else {
prev_slash = false;
}
s.push(c);
}
if s.len() > 1 && s.ends_with('/') {
s.pop();
}
s
}
fn under_mount(point: &str, abs: &str) -> Option<String> {
if point == "/" {
return Some(abs.to_string());
}
if abs == point {
return Some("/".to_string());
}
let with_slash = format!("{point}/");
abs.strip_prefix(&with_slash).map(|rest| format!("/{rest}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_table_delegates_to_root() {
let mut t = MountTable::new();
t.insert("/etc/hosts", b"127.0.0.1 localhost\n".to_vec());
let h = t.open("/etc/hosts", FileAccess::Read).unwrap();
let mut buf = [0u8; 9];
assert_eq!(t.read_handle(h, &mut buf), Some(9));
assert_eq!(&buf, b"127.0.0.1");
assert!(t.owns(h));
assert!(t.close(h));
assert!(t.contains("/etc/hosts"));
}
#[test]
fn overlay_read_open_routes_to_backend() {
let mut t = MountTable::new();
t.mount(
"/dev",
Box::new(crate::emulator::fsmount::devfs::DevFs::new()),
);
let h = t
.open("/dev/urandom", FileAccess::Read)
.expect("open urandom");
assert!(
h >= OVERLAY_HANDLE_BASE,
"overlay handle, not a root handle"
);
let mut buf = [0u8; 8];
assert_eq!(t.read_handle(h, &mut buf), Some(8));
assert!(t.open("/dev/nope", FileAccess::Read).is_none());
assert!(t.open("/etc/x", FileAccess::Read).is_none());
}
#[test]
fn root_dir_synthesis_stat_and_readdir() {
let mut t = MountTable::new();
t.insert("/work/a.txt", b"hello".to_vec());
t.insert("/work/sub/c.txt", b"xyz".to_vec());
let a = t.stat_path("/work/a.txt").expect("file stat");
assert!(matches!(a.kind, NodeKind::File));
assert_eq!(a.size, 5);
let d = t.stat_path("/work").expect("dir stat");
assert!(matches!(d.kind, NodeKind::Dir));
let mut names: Vec<_> = t
.readdir_path("/work")
.unwrap()
.into_iter()
.map(|e| (e.name, matches!(e.kind, NodeKind::Dir)))
.collect();
names.sort();
assert_eq!(
names,
vec![("a.txt".to_string(), false), ("sub".to_string(), true)]
);
assert!(t.readdir_path("/work/a.txt").is_err());
}
#[test]
fn root_readdir_surfaces_overlay_mount_points() {
let mut t = MountTable::new();
t.mount(
"/dev",
Box::new(crate::emulator::fsmount::devfs::DevFs::new()),
);
let names: Vec<_> = t
.readdir_path("/")
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(
names.iter().any(|n| n == "dev"),
"mount point listed: {names:?}"
);
}
#[test]
fn norm_and_resolution() {
assert_eq!(norm_abs("/proc//self/"), "/proc/self");
assert_eq!(norm_abs("proc"), "/proc");
assert_eq!(
under_mount("/proc", "/proc/self/maps").as_deref(),
Some("/self/maps")
);
assert_eq!(under_mount("/proc", "/proc").as_deref(), Some("/"));
assert_eq!(under_mount("/proc", "/process"), None);
assert_eq!(under_mount("/", "/etc/x").as_deref(), Some("/etc/x"));
}
}