//! 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 //! children. A FAT32, ext2, exFAT or btrfs driver implements this trait. //! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on disk 0"); //! gives you its root inode. //! * the **mount table** -- which file system is attached at which path ("/", //! "/tmp", ...). `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 a device like the console. The per-process //! fd table holds `Arc`s, so syscalls don't care what's behind it. //! //! Today there is one implementation, [`ramfs`] (everything in RAM). Later: a //! block device layer (virtio-blk / ATA PIO), then FAT32, ext2, exFAT and //! btrfs (read-only first) as further `FileSystem`s. pub mod file; pub mod ramfs; use crate::errno::Errno; use alloc::{string::String, sync::Arc, vec::Vec}; use spin::Mutex; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InodeKind { File, Directory, } /// 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; /// 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) } /// 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) } /// Names of all children of this directory. fn list(&self) -> Result, Errno> { Err(Errno::ENOTDIR) } } /// 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; } 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` (must be absolute, like "/" or "/tmp"). pub fn mount(path: &str, fs: Arc) -> Result<(), Errno> { if !path.starts_with('/') { return Err(Errno::EINVAL); } let path = String::from(if path.len() > 1 { path.trim_end_matches('/') } else { 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'/')) } /// Resolve an absolute path to an inode. pub fn lookup(path: &str) -> Result, Errno> { if !path.starts_with('/') { return Err(Errno::EINVAL); // no current directory yet } if path.len() > 255 { return Err(Errno::ENAMETOOLONG); } // Longest mount point that contains the path wins. let (root, 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 = if m.path == "/" { path } else { &path[m.path.len()..] }; (m.fs.root(), rest) }; // Walk the remaining components ("a/b/c") from that file system's root. let mut inode = root; for name in rest.split('/').filter(|c| !c.is_empty() && *c != ".") { if name == ".." { return Err(Errno::EINVAL); // not supported yet } inode = inode.lookup(name)?; } Ok(inode) } /// Create a file at `path` (its parent directory must exist). pub fn create(path: &str, kind: InodeKind) -> Result, Errno> { let path = path.trim_end_matches('/'); let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?; if name.is_empty() { return Err(Errno::EINVAL); } let parent = lookup(if parent.is_empty() { "/" } else { parent })?; parent.create(name, kind) } /// 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: FAT32, ext2, exFAT, btrfs.\n").unwrap(); dir.create("tmp", InodeKind::Directory).unwrap(); // mount point mount("/", root).unwrap(); mount("/tmp", ramfs::RamFs::new()).unwrap(); }