Files
moss-kernel/src/fs/mod.rs
2025-12-20 19:06:07 -05:00

478 lines
16 KiB
Rust

use alloc::boxed::Box;
use alloc::{collections::btree_map::BTreeMap, sync::Arc};
use async_trait::async_trait;
use core::sync::atomic::{AtomicU64, Ordering};
use dir::DirFile;
use libkernel::error::{FsError, KernelError, Result};
use libkernel::fs::attr::FilePermissions;
use libkernel::fs::path::Path;
use libkernel::fs::{BlockDevice, FS_ID_START, FileType, Filesystem, Inode, InodeId, OpenFlags};
use open_file::OpenFile;
use reg::RegFile;
use crate::drivers::{DM, Driver};
use crate::process::Task;
use crate::sync::SpinLock;
use alloc::vec::Vec;
pub mod dir;
pub mod fops;
pub mod open_file;
pub mod pipe;
pub mod reg;
pub mod syscalls;
/// A dummy inode used as a placeholder before the root filesystem is mounted.
pub struct DummyInode {}
impl Inode for DummyInode {
fn id(&self) -> InodeId {
InodeId::dummy()
}
}
/// Represents a mounted filesystem.
struct Mount {
fs: Arc<dyn Filesystem>,
root_inode: Arc<dyn Inode>,
}
/// This trait represents a type of filesystem, like "ext4" or "tmpfs". It acts
/// as a factory for creating mounted instances.
#[async_trait]
pub trait FilesystemDriver: Driver + Send + Sync {
async fn construct(
&self,
fs_id: u64,
blk_dev: Option<Box<dyn BlockDevice>>,
) -> Result<Arc<dyn Filesystem>>;
}
/// The internal state of the VFS.
///
/// This struct consolidates the filesystem-wide collections (the list of all
/// registered filesystem instances and the mapping of mount points).
struct VfsState {
/// A map from an InodeId of a directory to the Mount that is mounted there.
mounts: BTreeMap<InodeId, Mount>,
/// A map from a filesystem ID to the corresponding filesystem instance.
filesystems: BTreeMap<u64, Arc<dyn Filesystem>>,
}
impl VfsState {
/// Creates a new, empty VfsState.
const fn new() -> Self {
Self {
mounts: BTreeMap::new(),
filesystems: BTreeMap::new(),
}
}
/// Registers a new filesystem and its mount point.
fn add_mount(&mut self, mount_point_id: InodeId, mount: Mount) {
self.filesystems.insert(mount.fs.id(), mount.fs.clone());
self.mounts.insert(mount_point_id, mount);
}
/// Checks if an inode is a mount point and returns the root inode of the
/// mounted filesystem if it is.
fn get_mount_root(&self, inode_id: &InodeId) -> Option<Arc<dyn Inode>> {
self.mounts
.get(inode_id)
.map(|mount| mount.root_inode.clone())
}
}
#[allow(clippy::upper_case_acronyms)]
pub struct VFS {
next_fs_id: AtomicU64,
state: SpinLock<VfsState>,
root_inode: SpinLock<Option<Arc<dyn Inode>>>,
}
impl VFS {
const fn new() -> Self {
Self {
next_fs_id: AtomicU64::new(FS_ID_START),
state: SpinLock::new(VfsState::new()),
root_inode: SpinLock::new(None),
}
}
/// Creates an instance of a filesystem from a registered driver.
///
/// This does not mount the filesystem, but prepares an instance that can
/// then be attached to a mount point.
async fn create_fs_instance(
&self,
driver_name: &str,
blkdev: Option<Box<dyn BlockDevice>>,
) -> Result<Arc<dyn Filesystem>> {
let driver = DM
.lock_save_irq()
.find_by_name(driver_name)
.ok_or(FsError::DriverNotFound)?
.as_filesystem_driver()
.ok_or(FsError::DriverNotFound)?;
let id = self.next_fs_id.fetch_add(1, Ordering::SeqCst);
driver.construct(id, blkdev).await
}
/// Mounts the root filesystem.
pub async fn mount_root(
&self,
driver_name: &str,
blkdev: Option<Box<dyn BlockDevice>>,
) -> Result<()> {
let fs = self.create_fs_instance(driver_name, blkdev).await?;
let root_inode = fs.root_inode().await?;
let mount = Mount {
fs,
root_inode: root_inode.clone(),
};
// Lock the state to add the new mount and filesystem.
self.state.lock_save_irq().add_mount(root_inode.id(), mount);
// Set the global root inode.
*self.root_inode.lock_save_irq() = Some(root_inode);
Ok(())
}
/// Mounts a filesystem at a given directory (mount point).
pub async fn mount(
&self,
mount_point: Arc<dyn Inode>,
driver_name: &str,
blkdev: Option<Box<dyn BlockDevice>>,
) -> Result<()> {
if mount_point.getattr().await?.file_type != FileType::Directory {
return Err(FsError::NotADirectory.into());
}
let fs = self.create_fs_instance(driver_name, blkdev).await?;
let mount_point_id = mount_point.id();
let root_inode = fs.root_inode().await?;
let new_mount = Mount { fs, root_inode };
// Lock the state and insert the new mount.
self.state
.lock_save_irq()
.add_mount(mount_point_id, new_mount);
Ok(())
}
/// Resolves a path string to an Inode, starting from a given root for
/// relative paths.
pub async fn resolve_path(
&self,
path: &Path,
root: Arc<dyn Inode>,
task: Arc<Task>,
) -> Result<Arc<dyn Inode>> {
let mut current_inode = if path.is_absolute() {
task.root.lock_save_irq().0.clone() // use the task's root inode, in case a custom chroot was set
} else {
root
};
for component in path.components() {
// Before looking up the component, check if the current inode is a
// mount point. If so, traverse into the mounted filesystem's root.
if let Some(mount_root) = self
.state
.lock_save_irq()
.get_mount_root(&current_inode.id())
{
current_inode = mount_root;
}
// Delegate the lookup to the underlying filesystem.
current_inode = current_inode.lookup(component).await?;
}
// After the final lookup, check if the destination is itself a mount point.
if let Some(mount_root) = self
.state
.lock_save_irq()
.get_mount_root(&current_inode.id())
{
current_inode = mount_root;
}
Ok(current_inode)
}
/// Resolves a path string to an Inode, starting from a given root for
/// relative paths, and using the filesystem root inode for absolute paths.
pub async fn resolve_path_absolute(
&self,
path: &Path,
root: Arc<dyn Inode>,
) -> Result<Arc<dyn Inode>> {
let mut current_inode = if path.is_absolute() {
self.root_inode
.lock_save_irq()
.as_ref()
.cloned()
.ok_or(FsError::NotFound)?
} else {
root
};
for component in path.components() {
// Before looking up the component, check if the current inode is a
// mount point. If so, traverse into the mounted filesystem's root.
if let Some(mount_root) = self
.state
.lock_save_irq()
.get_mount_root(&current_inode.id())
{
current_inode = mount_root;
}
// Delegate the lookup to the underlying filesystem.
current_inode = current_inode.lookup(component).await?;
}
// After the final lookup, check if the destination is itself a mount point.
if let Some(mount_root) = self
.state
.lock_save_irq()
.get_mount_root(&current_inode.id())
{
current_inode = mount_root;
}
Ok(current_inode)
}
/// Returns a clone of the root inode.
pub fn root_inode(&self) -> Arc<dyn Inode> {
self.root_inode.lock_save_irq().as_ref().unwrap().clone()
}
pub async fn open(
&self,
path: &Path,
flags: OpenFlags,
root: Arc<dyn Inode>,
mode: FilePermissions,
task: Arc<Task>,
) -> Result<Arc<OpenFile>> {
// Attempt to resolve the full path first.
let resolve_result = self.resolve_path(path, root.clone(), task.clone()).await;
let target_inode = match resolve_result {
// The file/directory exists.
Ok(inode) => {
if flags.contains(OpenFlags::O_CREAT | OpenFlags::O_EXCL) {
// O_CREAT and O_EXCL were passed and the file exists. This is
// an error.
return Err(FsError::AlreadyExists.into());
}
// The file exists, and we're not exclusively creating. Proceed.
inode
}
// The path was not found.
Err(KernelError::Fs(FsError::NotFound)) => {
// If O_CREAT is specified, we should create it.
if flags.contains(OpenFlags::O_CREAT) {
// Determine the target name and parent directory. If the path has no
// explicit parent component (e.g., "foo"), use the provided `root`
// (cwd or dirfd) as the parent directory.
let file_name = path.file_name().ok_or(FsError::InvalidInput)?;
let parent_inode = if let Some(parent_path) = path.parent() {
self.resolve_path(parent_path, root.clone(), task).await?
} else {
root.clone()
};
// Ensure the parent is actually a directory before creating a
// file in it.
if parent_inode.getattr().await?.file_type != FileType::Directory {
return Err(FsError::NotADirectory.into());
}
parent_inode.create(file_name, FileType::File, mode).await?
} else {
// O_CREAT was not specified, so NotFound is the correct error.
return Err(FsError::NotFound.into());
}
}
// Some other error occurred during resolution (e.g., NotADirectory
// mid-path).
Err(e) => return Err(e),
};
let attr = target_inode.getattr().await?;
if flags.contains(OpenFlags::O_DIRECTORY) && attr.file_type != FileType::Directory {
return Err(FsError::NotADirectory.into());
}
if attr.file_type == FileType::Directory
&& (flags.contains(OpenFlags::O_WRONLY) || flags.contains(OpenFlags::O_RDWR))
{
return Err(FsError::IsADirectory.into());
}
if flags.contains(OpenFlags::O_TRUNC)
&& attr.file_type == FileType::File
&& (flags.contains(OpenFlags::O_WRONLY) || flags.contains(OpenFlags::O_RDWR))
{
// TODO: Check for write permissions on the inode itself.
target_inode.truncate(0).await?;
}
match attr.file_type {
FileType::File => {
let mut open_file =
OpenFile::new(Box::new(RegFile::new(target_inode.clone())), flags);
open_file.set_inode(target_inode);
Ok(Arc::new(open_file))
}
FileType::Directory => {
let mut open_file =
OpenFile::new(Box::new(DirFile::new(target_inode.clone())), flags);
open_file.set_inode(target_inode);
Ok(Arc::new(open_file))
}
FileType::Symlink => todo!(),
FileType::BlockDevice(_) => todo!(),
FileType::CharDevice(char_dev_descriptor) => {
let char_driver = DM
.lock_save_irq()
.find_char_driver(char_dev_descriptor.major)
.ok_or(FsError::NoDevice)?;
Ok(char_driver
.get_device(char_dev_descriptor.minor)
.ok_or(FsError::NoDevice)?
.open(flags)?)
}
FileType::Fifo => todo!(),
FileType::Socket => todo!(),
}
}
pub async fn mkdir(
&self,
path: &Path,
root: Arc<dyn Inode>,
mode: FilePermissions,
task: Arc<Task>,
) -> Result<()> {
// Try to resolve the target directory first.
match self.resolve_path(path, root.clone(), task.clone()).await {
// The path already exists, this is an error.
Ok(_) => Err(FsError::AlreadyExists.into()),
// The path does not exist, we need to create it.
Err(KernelError::Fs(FsError::NotFound)) => {
// Determine the new directory name.
let dir_name = path.file_name().ok_or(FsError::InvalidInput)?;
// Resolve the parent directory. If the path has no parent
// component (e.g., \"foo\"), treat the provided `root`
// directory (AT_FDCWD / cwd / dirfd) as the parent.
let parent_inode = if let Some(parent_path) = path.parent() {
self.resolve_path(parent_path, root.clone(), task).await?
} else {
root.clone()
};
// Verify that the parent is actually a directory.
if parent_inode.getattr().await?.file_type != FileType::Directory {
return Err(FsError::NotADirectory.into());
}
// Delegate the creation to the filesystem-specific inode.
parent_inode
.create(dir_name, FileType::Directory, mode)
.await?;
Ok(())
}
// Propagate any other errors up the stack.
Err(e) => Err(e),
}
}
pub async fn unlink(
&self,
path: &Path,
root: Arc<dyn Inode>,
remove_dir: bool,
task: Arc<Task>,
) -> Result<()> {
// First, resolve the target inode so we can inspect its type.
let target_inode = self.resolve_path(path, root.clone(), task.clone()).await?;
let attr = target_inode.getattr().await?;
// Validate flag and file-type combinations.
match attr.file_type {
FileType::Directory if !remove_dir => {
return Err(FsError::IsADirectory.into());
}
FileType::Directory => { /* OK: rmdir semantics */ }
_ if remove_dir => {
return Err(FsError::NotADirectory.into());
}
_ => { /* Regular unlink */ }
}
// Determine the parent directory inode in which to perform the unlink.
let parent_inode = if let Some(parent_path) = path.parent() {
self.resolve_path(parent_path, root.clone(), task).await?
} else {
root.clone()
};
// Ensure the parent really is a directory.
if parent_inode.getattr().await?.file_type != FileType::Directory {
return Err(FsError::NotADirectory.into());
}
// Extract the final component (name) and perform the unlink on the parent.
let name = path.file_name().ok_or(FsError::InvalidInput)?;
parent_inode.unlink(name).await?;
Ok(())
}
}
pub static VFS: VFS = VFS::new();
impl VFS {
/// Flushes all mounted filesystems and their underlying block devices.
/// Any individual error is logged and ignored so that a single faulty
/// filesystem does not block the shutdown sequence.
pub async fn sync_all(&self) -> Result<()> {
let filesystems: Vec<_> = {
let state = self.state.lock_save_irq();
state.filesystems.values().cloned().collect()
};
for fs in filesystems {
// Ignore per-filesystem errors; best-effort
let _ = fs.sync().await;
}
Ok(())
}
}