use std::io;
use super::{Attrs, DirEntry, MountFs};
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 {
f.debug_struct("Mount")
.field("point", &self.point)
.finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
pub struct MountTable {
mounts: Vec<Mount>,
}
impl MountTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn mount(&mut self, point: impl Into<String>, fs: Box<dyn MountFs>) {
self.mounts.push(Mount {
point: normalize(&point.into()),
fs,
});
}
fn best_mount(&self, abs_path: &str) -> Option<usize> {
let mut best: Option<usize> = None;
let mut best_len = 0usize;
for (i, m) in self.mounts.iter().enumerate() {
if path_has_prefix(abs_path, &m.point) && m.point.len() >= best_len {
best = Some(i);
best_len = m.point.len();
}
}
best
}
fn resolve(&mut self, abs_path: &str) -> Option<(&mut dyn MountFs, String)> {
let abs_path = normalize(abs_path);
let i = self.best_mount(&abs_path)?;
let rel = relative_to(&abs_path, &self.mounts[i].point);
Some((self.mounts[i].fs.as_mut(), rel))
}
pub fn stat(&mut self, abs_path: &str) -> Option<Attrs> {
let (fs, rel) = self.resolve(abs_path)?;
fs.stat(&rel)
}
pub fn read_at(&mut self, abs_path: &str, off: u64, buf: &mut [u8]) -> io::Result<usize> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.read_at(&rel, off, buf)
}
pub fn readdir(&mut self, abs_path: &str) -> io::Result<Vec<DirEntry>> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.readdir(&rel)
}
pub fn write_at(&mut self, abs_path: &str, off: u64, buf: &[u8]) -> io::Result<usize> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.write_at(&rel, off, buf)
}
pub fn create(&mut self, abs_path: &str, mode: u32) -> io::Result<()> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.create(&rel, mode)
}
pub fn mkdir(&mut self, abs_path: &str, mode: u32) -> io::Result<()> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.mkdir(&rel, mode)
}
pub fn unlink(&mut self, abs_path: &str) -> io::Result<()> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.unlink(&rel)
}
pub fn rmdir(&mut self, abs_path: &str) -> io::Result<()> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.rmdir(&rel)
}
pub fn truncate(&mut self, abs_path: &str, len: u64) -> io::Result<()> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.truncate(&rel, len)
}
pub fn readlink(&mut self, abs_path: &str) -> io::Result<String> {
let (fs, rel) = self.resolve(abs_path).ok_or_else(enoent)?;
fs.readlink(&rel)
}
pub fn symlink(&mut self, target: &str, abs_link: &str) -> io::Result<()> {
let (fs, rel) = self.resolve(abs_link).ok_or_else(enoent)?;
fs.symlink(target, &rel)
}
pub fn rename(&mut self, from: &str, to: &str) -> io::Result<()> {
let from = normalize(from);
let to = normalize(to);
let from_idx = self.best_mount(&from).ok_or_else(enoent)?;
let to_idx = self.best_mount(&to).ok_or_else(enoent)?;
if from_idx != to_idx {
return Err(io::Error::from_raw_os_error(18)); }
let point = self.mounts[from_idx].point.clone();
let from_rel = relative_to(&from, &point);
let to_rel = relative_to(&to, &point);
self.mounts[from_idx].fs.rename(&from_rel, &to_rel)
}
}
fn enoent() -> io::Error {
io::Error::from_raw_os_error(2)
}
fn normalize(p: &str) -> String {
let mut stack: Vec<&str> = Vec::new();
for comp in p.split('/') {
match comp {
"" | "." => {}
".." => {
stack.pop();
}
other => stack.push(other),
}
}
if stack.is_empty() {
"/".to_string()
} else {
let mut out = String::with_capacity(p.len());
for c in stack {
out.push('/');
out.push_str(c);
}
out
}
}
fn path_has_prefix(path: &str, mp: &str) -> bool {
if mp == "/" {
return true;
}
if let Some(rest) = path.strip_prefix(mp) {
rest.is_empty() || rest.starts_with('/')
} else {
false
}
}
fn relative_to(path: &str, mp: &str) -> String {
let rest = if mp == "/" {
path
} else {
path.strip_prefix(mp).unwrap_or(path)
};
rest.trim_start_matches('/').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::NodeKind;
#[test]
fn longest_prefix_and_relative() {
assert!(path_has_prefix("/work/src/a.rs", "/work"));
assert!(path_has_prefix("/anything", "/"));
assert!(!path_has_prefix("/worker", "/work"));
assert_eq!(relative_to("/work/src/a.rs", "/work"), "src/a.rs");
assert_eq!(relative_to("/bin/sh", "/"), "bin/sh");
}
#[test]
fn normalize_collapses_dot_dotdot_and_slashes() {
assert_eq!(normalize("/"), "/");
assert_eq!(normalize(""), "/");
assert_eq!(normalize("/a/./b"), "/a/b");
assert_eq!(normalize("/a//b"), "/a/b");
assert_eq!(normalize("/a/../b"), "/b");
assert_eq!(normalize("/a/b/"), "/a/b");
assert_eq!(normalize("/../a"), "/a");
}
#[derive(Debug)]
struct TagFs {
tag: &'static str,
}
impl MountFs for TagFs {
fn stat(&mut self, rel: &str) -> Option<Attrs> {
Some(Attrs {
kind: NodeKind::Dir,
size: rel.len() as u64,
mode: 0o755,
uid: 0,
gid: 0,
mtime: 0,
inode: 0,
nlink: 1,
rdev: 0,
})
}
fn read_at(&mut self, _rel: &str, _off: u64, _buf: &mut [u8]) -> io::Result<usize> {
Ok(0)
}
fn readdir(&mut self, rel: &str) -> io::Result<Vec<DirEntry>> {
Ok(vec![DirEntry {
name: format!("{}:{rel}", self.tag),
kind: NodeKind::File,
inode: 0,
}])
}
}
fn tagged(tag: &'static str) -> Box<dyn MountFs> {
Box::new(TagFs { tag })
}
fn route(table: &mut MountTable, path: &str) -> String {
table.readdir(path).unwrap()[0].name.clone()
}
#[test]
fn dev_does_not_match_devfoo() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/dev", tagged("dev"));
assert_eq!(route(&mut table, "/devfoo"), "root:devfoo");
assert_eq!(route(&mut table, "/dev/null"), "dev:null");
assert_eq!(route(&mut table, "/dev"), "dev:");
}
#[test]
fn longest_prefix_picks_deepest_mount() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/dev", tagged("dev"));
table.mount("/dev/pts", tagged("pts"));
assert_eq!(route(&mut table, "/etc/passwd"), "root:etc/passwd");
assert_eq!(route(&mut table, "/dev/console"), "dev:console");
assert_eq!(route(&mut table, "/dev/pts/0"), "pts:0");
assert_eq!(route(&mut table, "/dev/pts"), "pts:");
}
#[test]
fn stat_of_bare_mount_point_works() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/dev", tagged("dev"));
let attrs = table.stat("/dev").expect("stat of mount point itself");
assert_eq!(attrs.size, 0); assert_eq!(route(&mut table, "/dev"), "dev:");
}
#[test]
fn dot_dotdot_and_double_slash_route_identically() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/work", tagged("work"));
let a = route(&mut table, "/work/x/./y");
let b = route(&mut table, "/work/x//y");
let c = route(&mut table, "/work/x/sub/../y");
assert_eq!(a, "work:x/y");
assert_eq!(a, b);
assert_eq!(b, c);
}
#[test]
fn trailing_slash_resolves() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/work", tagged("work"));
assert_eq!(route(&mut table, "/work/"), "work:");
assert_eq!(route(&mut table, "/work"), "work:");
assert_eq!(route(&mut table, "/work/sub/"), "work:sub");
}
#[test]
fn root_fallback_catches_unmounted_paths() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/work", tagged("work"));
assert_eq!(route(&mut table, "/etc/hosts"), "root:etc/hosts");
assert_eq!(route(&mut table, "/"), "root:");
}
#[test]
fn mount_point_itself_is_normalized() {
let mut table = MountTable::new();
table.mount("/", tagged("root"));
table.mount("/dev/", tagged("dev"));
assert_eq!(route(&mut table, "/dev/null"), "dev:null");
assert_eq!(route(&mut table, "/dev"), "dev:");
}
}