//! VFS (virtual file system): the layer every concrete file system plugs into. //! //! Modelled (very loosely) on Linux: //! //! * [`Inode`] -- one file or directory *inside some file system*. Knows how to //! read/write its bytes at an offset and (for directories) look up, create, //! list and remove children. ramfs, FAT32, ext2, exFAT and (read-only) //! btrfs implement it. //! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on vda1"); //! gives you its root inode. //! * the **mount table** -- which file system is attached at which path ("/", //! "/tmp", "/mnt/disk0"). `lookup("/tmp/x")` finds the longest matching //! mount point and walks the rest of the path from that file system's root. //! * [`file::File`] -- an *open* file (what a file descriptor points to): an //! inode plus a current offset, or the console, or a pipe. The per-process //! fd table holds `Arc`s, so syscalls don't care what's behind it. //! //! Paths: every path is first turned into a clean absolute path by //! [`normalize`] (relative paths are joined to the process's current //! directory; ".", ".." and "//" are resolved by plain string processing). //! That makes ".." work across mount points for free. Real kernels track //! parent dentries instead. //! //! Symlinks (ext2 has them): [`lookup`] follows them. When a path component //! is a symlink, its target is joined to the directory the link is in, the //! rest of the path is appended, and the lookup starts over (at most 8 //! times, then ELOOP). Since ".." is handled as text, `link/..` is the //! directory holding `link`, not the parent of the link's target. Linux does //! the latter; ours is simpler and fine for a hobby kernel. pub mod btrfs; pub mod exfat; pub mod lock; pub mod lzo; pub mod ext2; pub mod fat; pub mod file; pub mod pipe; pub mod procfs; pub mod ramfs; use crate::errno::Errno; use alloc::{format, string::String, sync::Arc, vec::Vec}; use spin::Mutex; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InodeKind { File, Directory, /// A symbolic link: a small file holding a path ([`Inode::readlink`]). Symlink, /// A device file in /dev (opening it gives the driver's own `File`). Device, /// Stage 18: a Unix socket's name in the file system, made by `bind`. /// It holds no data: `connect` uses it to find the listening socket /// (net/unix.rs); `open` on it fails with ENXIO, like Linux. Socket, } /// One entry of a directory listing. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DirEntry { pub name: String, pub kind: InodeKind, /// The child's inode number (stage 17: what getdents64 reports as /// d_ino), or 0 if the file system has none (FAT, exFAT, /dev). pub ino: u64, } /// A file or directory inside a file system. /// /// Default method bodies return the error a "wrong kind" of inode would give /// (e.g. `lookup` on a file -> ENOTDIR), so implementations only override /// what makes sense for them. pub trait Inode: Send + Sync { fn kind(&self) -> InodeKind; /// Size in bytes (files) or number of entries (directories). fn size(&self) -> usize; /// Unix permission bits like 0o644, or 0 if the file system has none /// (FAT, ramfs). fn mode(&self) -> u32 { 0 } /// The target path of a symlink. fn readlink(&self) -> Result { Err(Errno::EINVAL) } /// Read bytes starting at `offset`; returns how many were read (0 = EOF). fn read_at(&self, _offset: usize, _buf: &mut [u8]) -> Result { Err(Errno::EISDIR) } /// Write bytes at `offset` (growing the file if needed). fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result { Err(Errno::EISDIR) } /// Cut the file to length 0 (for `open(..., O_TRUNC)`, i.e. shell `>`). fn truncate(&self) -> Result<(), Errno> { Err(Errno::EISDIR) } /// Find a child of this directory by name. fn lookup(&self, _name: &str) -> Result, Errno> { Err(Errno::ENOTDIR) } /// Create a child of this directory. fn create(&self, _name: &str, _kind: InodeKind) -> Result, Errno> { Err(Errno::ENOTDIR) } /// All children of this directory (without "." and ".."). fn list(&self) -> Result, Errno> { Err(Errno::ENOTDIR) } /// Delete a child (a file, or an *empty* directory). fn remove(&self, _name: &str) -> Result<(), Errno> { Err(Errno::ENOTDIR) } /// Device files: the `File` that `open` should return (None = an /// ordinary inode, opened as an `InodeFile`). fn open_device(&self) -> Result>, Errno> { Ok(None) } // --- Stage 16: changing a file's size, permissions and times --------- // (truncate/ftruncate, chmod & co., utimensat.) /// Make the file exactly `len` bytes long: cut off the end, or grow it /// with zero bytes. This default works for any file system that can /// truncate to 0 and write: growing writes one zero byte at the new /// end (the gap reads as zeros), shrinking keeps the first `len` /// bytes, truncates to 0 and writes them back. File systems that can /// do better (ramfs, FAT) override it. fn set_len(&self, len: usize) -> Result<(), Errno> { if self.kind() != InodeKind::File { return Err(Errno::EISDIR); } let size = self.size(); if len == size { return Ok(()); } if len > size { self.write_at(len - 1, &[0])?; return Ok(()); } let mut keep = alloc::vec![0u8; len]; let mut done = 0; while done < len { match self.read_at(done, &mut keep[done..])? { 0 => break, n => done += n, } } self.truncate()?; if done > 0 { self.write_at(0, &keep[..done])?; } Ok(()) } /// chmod: set the permission bits (0o7777 part of `mode`). EPERM if the /// file system can't store them (FAT, exFAT, /dev), EROFS if it's /// read-only (btrfs). fn set_mode(&self, _mode: u32) -> Result<(), Errno> { Err(Errno::EPERM) } /// The access, modification and status-change times, each as /// [seconds, nanoseconds] since 1970 -- or None if the file system /// doesn't keep them (stat then shows 0). fn times(&self) -> Option<[[i64; 2]; 3]> { None } /// utimensat: set the access and/or modification time (None = leave /// it). The default accepts and forgets them: FAT and exFAT have time /// fields, but our drivers don't write them yet. That's what `touch` /// needs to not fail. fn set_times(&self, _atime: Option<[i64; 2]>, _mtime: Option<[i64; 2]>) -> Result<(), Errno> { Ok(()) } // --- Stage 17: links, owners, renames (coreutils / busybox) ---------- /// A real inode number (ext2's, btrfs's, a ramfs node's address), or /// None: stat then hashes the path instead (FAT, exFAT, /dev). fn ino(&self) -> Option { None } /// How many names (hard links) the inode has; None = "1, or 2 for a /// directory" (file systems without hard links). fn nlink(&self) -> Option { None } /// Owner (uid, gid). File systems without owners say root (0, 0). fn owner(&self) -> (u32, u32) { (0, 0) } /// chown: set the owner and/or group (None = leave it). EPERM where /// the file system can't store them (FAT, exFAT, /dev, like Linux's /// vfat), EROFS on read-only btrfs. fn set_owner(&self, _uid: Option, _gid: Option) -> Result<(), Errno> { Err(Errno::EPERM) } /// Create a symlink `name` -> `target` in this directory. EPERM where /// the file system has no symlinks (FAT and exFAT: Linux says EPERM too). fn symlink(&self, _name: &str, _target: &str) -> Result, Errno> { Err(Errno::EPERM) } /// Hard link: add `name` in this directory for the existing `target` /// (same file system, checked by the caller). EPERM where there are no /// hard links (FAT, exFAT). fn link(&self, _name: &str, _target: &Arc) -> Result<(), Errno> { Err(Errno::EPERM) } /// Move entry `old` of this directory to `new` in `new_dir` (same file /// system), replacing what's there (the caller checked that's allowed). /// ENOSYS = "can't do it directly": `rename` then copies and deletes. fn rename_entry(&self, _old: &str, _new_dir: &Arc, _new: &str) -> Result<(), Errno> { Err(Errno::ENOSYS) } /// The concrete type behind the trait object, so a file system can /// recognise its own inodes (for link and rename). None = don't care. fn as_any(self: Arc) -> Option> { None } } /// Downcast an inode to a file system's own type (stage 17). pub fn downcast(inode: &Arc) -> Option> { inode.clone().as_any()?.downcast::().ok() } /// What statfs/fstatfs report about a file system (stage 17). #[derive(Debug, Clone, Copy, Default)] pub struct StatFs { /// The file system's magic number (f_type), as in . pub magic: u64, /// Block size in bytes, and the counts in those blocks. pub block_size: u64, pub blocks: u64, pub free_blocks: u64, /// Free blocks an ordinary user may use (ext2 keeps some for root). pub avail_blocks: u64, /// Inodes (0 = the file system doesn't count them). pub files: u64, pub free_files: u64, pub name_max: u64, pub read_only: bool, } pub const RAMFS_MAGIC: u64 = 0x8584_58f6; pub const TMPFS_MAGIC: u64 = 0x0102_1994; pub const EXT2_MAGIC: u64 = 0xEF53; pub const MSDOS_MAGIC: u64 = 0x4d44; pub const EXFAT_MAGIC: u64 = 0x2011_BAB0; pub const BTRFS_MAGIC: u64 = 0x9123_683E; /// The wall-clock time as [seconds, nanoseconds] since 1970 (file times). pub fn now_timespec() -> [i64; 2] { let us = crate::time::realtime_us(); [(us / 1_000_000) as i64, (us % 1_000_000 * 1000) as i64] } /// `Inode::mode` of a file whose mode was set to exactly 0 with chmod /// (plain 0 means "this file system has no permissions"). pub const MODE_SET: u32 = 1 << 31; /// A mounted file system. pub trait FileSystem: Send + Sync { /// Type name, like the `-t` argument of `mount` ("ramfs", "fat32", ...). fn name(&self) -> &'static str; fn root(&self) -> Arc; /// Sizes and counts for statfs (stage 17). The default suits a /// memory-only file system that keeps no counts (/dev: devtmpfs). fn statfs(&self) -> StatFs { StatFs { magic: TMPFS_MAGIC, block_size: 4096, name_max: 255, ..Default::default() } } } struct Mount { path: String, // normalized, e.g. "/" or "/tmp" fs: Arc, } /// The mount table. Lookups take the lock only briefly (never while doing /// I/O), so it's fine as a spinlock. Not used from interrupt handlers. static MOUNTS: Mutex> = Mutex::new(Vec::new()); /// Attach `fs` at `path` (absolute, like "/" or "/tmp"). pub fn mount(path: &str, fs: Arc) -> Result<(), Errno> { if !path.starts_with('/') { return Err(Errno::EINVAL); } let path = normalize("/", path)?; let mut mounts = MOUNTS.lock(); if mounts.iter().any(|m| m.path == path) { return Err(Errno::EEXIST); } mounts.push(Mount { path, fs }); Ok(()) } /// (mount path, fs type) of every mount, for printing. pub fn mounts() -> Vec<(String, &'static str)> { MOUNTS.lock().iter().map(|m| (m.path.clone(), m.fs.name())).collect() } /// Does mount point `mount` contain `path`? ("/tmp" contains "/tmp/x" but /// not "/tmpfoo"; "/" contains everything.) fn mount_contains(mount: &str, path: &str) -> bool { mount == "/" || path == mount || (path.starts_with(mount) && path.as_bytes().get(mount.len()) == Some(&b'/')) } /// Turn `path` into a clean absolute path. Relative paths start at `cwd` /// (which must be absolute). "." is dropped, ".." removes the previous /// component (".." of "/" is "/", like Unix). /// normalize("/home", "../etc//motd") == "/etc/motd" pub fn normalize(cwd: &str, path: &str) -> Result { if path.is_empty() { return Err(Errno::ENOENT); // like Linux: "" is not a path } let mut parts: Vec<&str> = Vec::new(); let joined = if path.starts_with('/') { String::from(path) } else { format!("{}/{}", cwd, path) }; for component in joined.split('/') { match component { "" | "." => {} ".." => { parts.pop(); } name if name.len() > 255 => return Err(Errno::ENAMETOOLONG), name => parts.push(name), } } let result = format!("/{}", parts.join("/")); if result.len() > 4096 { return Err(Errno::ENAMETOOLONG); } Ok(result) } /// Resolve an absolute path to an inode, following symlinks. pub fn lookup(path: &str) -> Result, Errno> { walk(path, true, 0).map(|(inode, _)| inode) } /// Like [`lookup`] (or `lookup_nofollow`), but also say where the inode /// really is: the path with every symlink on the way replaced by its /// target (stage 18). stat uses it for st_dev (a link on one file system /// can point into another), and Unix sockets for their key. pub fn lookup_resolved(path: &str, follow_last: bool) -> Result<(Arc, String), Errno> { walk(path, follow_last, 0) } /// Like [`lookup`], but if the *last* component is a symlink, return the /// link itself (for `readlink`, and for deleting a link, not its target). pub fn lookup_nofollow(path: &str) -> Result, Errno> { walk(path, false, 0).map(|(inode, _)| inode) } /// The target of the symlink at absolute `path`. pub fn readlink(path: &str) -> Result { lookup_nofollow(path)?.readlink() } /// How many symlinks one lookup may follow (Linux allows 40). const MAX_SYMLINKS: usize = 8; fn walk(path: &str, follow_last: bool, links_followed: usize) -> Result<(Arc, String), Errno> { if !path.starts_with('/') { return Err(Errno::EINVAL); // callers resolve relative paths first } let path = normalize("/", path)?; // Longest mount point that contains the path wins. let (root, mount_path, rest) = { let mounts = MOUNTS.lock(); let m = mounts .iter() .filter(|m| mount_contains(&m.path, &path)) .max_by_key(|m| m.path.len()) .ok_or(Errno::ENOENT)?; let rest = String::from(if m.path == "/" { &path[..] } else { &path[m.path.len()..] }); (m.fs.root(), m.path.clone(), rest) }; // Walk the remaining components ("a/b/c") from that file system's root. let names: Vec<&str> = rest.split('/').filter(|c| !c.is_empty()).collect(); let mut inode = root; let mut dir = mount_path; // absolute path of `inode` so far for (i, name) in names.iter().enumerate() { let child = inode.lookup(name)?; let last = i + 1 == names.len(); if child.kind() == InodeKind::Symlink && (follow_last || !last) { if links_followed == MAX_SYMLINKS { return Err(Errno::ELOOP); } // Replace "dir/link/rest..." by "dir//rest..." and start over. let mut new_path = normalize(&dir, &child.readlink()?)?; for rest in &names[i + 1..] { new_path.push('/'); new_path.push_str(rest); } return walk(&new_path, follow_last, links_followed + 1); } dir = if dir == "/" { format!("/{}", name) } else { format!("{}/{}", dir, name) }; inode = child; } Ok((inode, dir)) } /// Split a normalized path into (parent directory inode, last component). fn parent_of(path: &str) -> Result<(Arc, String), Errno> { let path = normalize("/", path)?; let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?; if name.is_empty() { return Err(Errno::EEXIST); // "/" itself } Ok((lookup(if parent.is_empty() { "/" } else { parent })?, String::from(name))) } /// Create a file or directory at absolute `path` (parent must exist). pub fn create(path: &str, kind: InodeKind) -> Result, Errno> { let (parent, name) = parent_of(path)?; parent.create(&name, kind) } /// Delete the file (`want_dir` false, like `unlink`) or empty directory /// (`want_dir` true, like `rmdir`) at absolute `path`. pub fn remove(path: &str, want_dir: bool) -> Result<(), Errno> { let normalized = normalize("/", path)?; if MOUNTS.lock().iter().any(|m| m.path == normalized) { return Err(Errno::EBUSY); // can't delete a mount point } // Don't follow a symlink here: `rm link` deletes the link. let inode = lookup_nofollow(&normalized)?; match (inode.kind(), want_dir) { (InodeKind::Directory, false) => return Err(Errno::EISDIR), (InodeKind::File | InodeKind::Symlink | InodeKind::Socket, true) => return Err(Errno::ENOTDIR), _ => {} } let (parent, name) = parent_of(&normalized)?; parent.remove(&name) } /// The mount point `path` (absolute, normalized) lives under. fn mount_of(path: &str) -> Option { let mounts = MOUNTS.lock(); mounts.iter().filter(|m| mount_contains(&m.path, path)).max_by_key(|m| m.path.len()).map(|m| m.path.clone()) } /// The file system `path` lives on and its place in the mount table /// (stage 17: statfs, and st_dev = place + 1 so that inode numbers of /// different file systems never look like the same file). pub fn fs_of(path: &str) -> Option<(usize, Arc)> { let path = normalize("/", path).ok()?; let mounts = MOUNTS.lock(); mounts .iter() .enumerate() .filter(|(_, m)| mount_contains(&m.path, &path)) .max_by_key(|(_, m)| m.path.len()) .map(|(i, m)| (i, m.fs.clone())) } /// st_dev for a path: which mounted file system (see `fs_of`). pub fn dev_of(path: &str) -> u64 { fs_of(path).map(|(i, _)| i as u64 + 1).unwrap_or(0) } /// symlink(target, path): a new symlink at absolute `path` holding /// `target` exactly as given (it's resolved when the link is followed). pub fn symlink(target: &str, path: &str) -> Result, Errno> { if target.is_empty() { return Err(Errno::ENOENT); } if lookup_nofollow(path).is_ok() { return Err(Errno::EEXIST); } let (parent, name) = parent_of(path)?; parent.symlink(&name, target) } /// link(old, new): a second name for the file at `old` (`follow`: if `old` /// is a symlink, link what it points to, like linkat's AT_SYMLINK_FOLLOW). pub fn link(old: &str, new: &str, follow: bool) -> Result<(), Errno> { let (old, new) = (normalize("/", old)?, normalize("/", new)?); let src = if follow { lookup(&old)? } else { lookup_nofollow(&old)? }; link_inode(&src, &old, &new) } /// Hard-link the inode `src` (found at `src_path`) as `new`. pub fn link_inode(src: &Arc, src_path: &str, new: &str) -> Result<(), Errno> { if src.kind() == InodeKind::Directory { return Err(Errno::EPERM); // no hard links to directories, ever } let new = normalize("/", new)?; if lookup_nofollow(&new).is_ok() { return Err(Errno::EEXIST); } if mount_of(src_path) != mount_of(&new) { return Err(Errno::EXDEV); } let (parent, name) = parent_of(&new)?; parent.link(&name, src) } /// rename(old, new), and renameat2's RENAME_NOREPLACE (`noreplace`: fail /// with EEXIST instead of replacing `new`). /// /// ramfs, ext2 and FAT move the directory entry itself (stage 17, /// `Inode::rename_entry`): any file, symlink or directory, full or not, /// and hard links stay links. Other file systems (exFAT) get the long way /// round, enough for "save to a temp file, rename it over the real one": /// - a file: copy its bytes to `new` (replacing a file there), then /// delete `old`; /// - an empty directory: create `new` (or reuse an empty one), delete `old`; /// - anything else (a non-empty directory, a symlink), or across file /// systems: EXDEV, which tells `mv` to copy and delete itself, like /// Linux does across file systems. /// Neither is atomic like Linux's: a crash in the middle can leave both. pub fn rename(old: &str, new: &str, noreplace: bool) -> Result<(), Errno> { let (old, new) = (normalize("/", old)?, normalize("/", new)?); let src = lookup_nofollow(&old)?; if new.starts_with(&old) && new.as_bytes().get(old.len()) == Some(&b'/') { return Err(Errno::EINVAL); // a directory into itself } let existing = lookup_nofollow(&new).ok(); if noreplace && existing.is_some() { return Err(Errno::EEXIST); // (even renaming a file onto itself) } if old == new { return Ok(()); } if MOUNTS.lock().iter().any(|m| m.path == old || m.path == new) { return Err(Errno::EBUSY); } if mount_of(&old) != mount_of(&new) { return Err(Errno::EXDEV); } // What may be replaced: a directory only by a directory, and only an // empty one; anything else only by a non-directory. if let Some(dst) = &existing { if src.ino().is_some() && src.ino() == dst.ino() { return Ok(()); // two hard links of one file: nothing to do (POSIX) } match (src.kind() == InodeKind::Directory, dst.kind() == InodeKind::Directory) { (true, false) => return Err(Errno::ENOTDIR), (false, true) => return Err(Errno::EISDIR), (true, true) if !dst.list()?.is_empty() => return Err(Errno::ENOTEMPTY), _ => {} } } let (old_parent, old_name) = parent_of(&old)?; let (new_parent, new_name) = parent_of(&new)?; match old_parent.rename_entry(&old_name, &new_parent, &new_name) { Err(Errno::ENOSYS) => {} // not supported here: the long way below other => return other, } match src.kind() { InodeKind::File => { if existing.is_some() { remove(&new, false)?; } let data = read_all(&old)?; let dst = create(&new, InodeKind::File)?; let mut done = 0; while done < data.len() { done += dst.write_at(done, &data[done..])?; } remove(&old, false) } InodeKind::Directory => { if !src.list()?.is_empty() { return Err(Errno::EXDEV); } if existing.is_none() { create(&new, InodeKind::Directory)?; } remove(&old, true) } _ => Err(Errno::EXDEV), } } /// Read a whole file into memory (used to load programs). pub fn read_all(path: &str) -> Result, Errno> { let inode = lookup(path)?; if inode.kind() == InodeKind::Directory { return Err(Errno::EISDIR); } let mut data = alloc::vec![0u8; inode.size()]; let mut done = 0; while done < data.len() { match inode.read_at(done, &mut data[done..])? { 0 => break, n => done += n, } } data.truncate(done); Ok(data) } /// Boot-time file system setup: a ramfs as "/", with a few files, and a /// second, empty ramfs mounted at "/tmp" (to exercise the mount table). pub fn init() { let root = ramfs::RamFs::new(); let dir = root.root(); let hello = dir.create("hello.txt", InodeKind::File).unwrap(); hello.write_at(0, b"Hello from the ramfs! This is /hello.txt.\n").unwrap(); let etc = dir.create("etc", InodeKind::Directory).unwrap(); let motd = etc.create("motd", InodeKind::File).unwrap(); motd.write_at(0, b"Welcome to Tyler's kernel. Next up: SMP, then networking.\n").unwrap(); dir.create("tmp", InodeKind::Directory).unwrap(); // mount point dir.create("mnt", InodeKind::Directory).unwrap(); // disks go to /mnt/diskN dir.create("dev", InodeKind::Directory).unwrap(); // device files (dev.rs) dir.create("proc", InodeKind::Directory).unwrap(); // stage 18: /proc/self (procfs.rs) mount("/", root).unwrap(); mount("/tmp", ramfs::RamFs::new()).unwrap(); mount("/dev", Arc::new(crate::dev::DevFs)).unwrap(); mount("/proc", Arc::new(procfs::ProcFs)).unwrap(); } /// Try to mount every registered partition (and unpartitioned disk) at /// /mnt/disk0, /mnt/disk1, ... Returns log lines. /// /// Which driver? Like Linux's `mount` without `-t`, we look for each file /// system's *magic number*: ext2 has 0xEF53 at byte 56 of its superblock /// (which starts at byte 1024); exFAT says "EXFAT " at byte 3 of its boot /// sector; btrfs has "_BHRfS_M" at byte 64 of its superblock (at 64 KiB); /// anything else is tried as FAT (fatfs checks the boot sector itself). /// A disk whose driver refuses it (say, a btrfs with sha256 checksums) /// gets a log line saying why, and no /mnt/diskN. pub fn mount_disks() -> Vec { let mut log = Vec::new(); let devices = crate::block::devices(); let mut n = 0; for (name, dev) in &devices { // Skip a whole disk if it has partitions ("vda" when "vda1" exists). let has_partitions = devices.iter().any(|(other, _)| other.len() > name.len() && other.starts_with(name.as_str())); if has_partitions { continue; } let path = format!("/mnt/disk{}", n); let (fs, description): (Arc, String) = if ext2::probe(&**dev) { match ext2::Ext2Fs::mount(dev.clone()) { Ok(fs) => { let d = fs.describe(); (fs, d) } Err(e) => { log.push(format!("{}: ext2, but can't mount it: {:?}", name, e)); continue; } } } else if exfat::probe(&**dev) { match exfat::ExfatFs::mount(dev.clone()) { Ok(fs) => { let d = fs.describe(); (fs, d) } Err(e) => { log.push(format!("{}: exFAT, but can't mount it: {:?}", name, e)); continue; } } } else if btrfs::probe(&**dev) { match btrfs::BtrfsFs::mount(dev.clone()) { Ok(fs) => { let d = fs.describe(); (fs, d) } Err(e) => { log.push(format!("{}: btrfs, but can't mount it: {}", name, e)); continue; } } } else { match fat::FatFs::mount(dev.clone()) { Ok(fs) => { let d = format!("FAT32 volume \"{}\"", fs.label()); (fs, d) } Err(e) => { log.push(format!("{}: no file system we know ({:?})", name, e)); continue; } } }; let _ = create(&path, InodeKind::Directory); // mount point (may exist) match mount(&path, fs) { Ok(()) => log.push(format!("{}: {} mounted at {}", name, description, path)), Err(e) => log.push(format!("{}: mount at {} failed: {:?}", name, path, e)), } n += 1; } log }