use crate::file::{File, FileError, FileMetadata, FileType, SeekFrom};
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::Write;
pub trait VirtualFileSystem: Send + Sync {
fn name(&self) -> &'static str;
fn open_file(&self, path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError>;
fn stat_path(&self, path: &str) -> Result<FileMetadata, FileError>;
fn readdir_path(&self, path: &str) -> Result<Vec<String>, FileError>;
fn create_file(&self, _path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError> {
Err(FileError::PermissionDenied)
}
fn create_dir(&self, _path: &str) -> Result<(), FileError> {
Err(FileError::PermissionDenied)
}
fn remove_file(&self, _path: &str) -> Result<(), FileError> {
Err(FileError::PermissionDenied)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VirtualNodeKind {
Directory,
StaticFile,
MountsFile,
ComponentListFile,
NullDevice,
ZeroDevice,
}
#[derive(Debug, Clone, Copy)]
pub struct VirtualNode {
pub path: &'static str,
pub kind: VirtualNodeKind,
pub mode: u32,
pub content: &'static [u8],
}
impl VirtualNode {
pub const fn directory(path: &'static str) -> Self {
Self {
path,
kind: VirtualNodeKind::Directory,
mode: 0o555,
content: b"",
}
}
pub const fn file(path: &'static str, content: &'static [u8]) -> Self {
Self {
path,
kind: VirtualNodeKind::StaticFile,
mode: 0o444,
content,
}
}
pub const fn null_device(path: &'static str) -> Self {
Self {
path,
kind: VirtualNodeKind::NullDevice,
mode: 0o666,
content: b"",
}
}
pub const fn zero_device(path: &'static str) -> Self {
Self {
path,
kind: VirtualNodeKind::ZeroDevice,
mode: 0o666,
content: b"",
}
}
pub const fn mounts_file(path: &'static str) -> Self {
Self {
path,
kind: VirtualNodeKind::MountsFile,
mode: 0o444,
content: b"",
}
}
pub const fn component_list_file(path: &'static str) -> Self {
Self {
path,
kind: VirtualNodeKind::ComponentListFile,
mode: 0o444,
content: b"",
}
}
}
pub struct StaticVirtualFileSystem {
name: &'static str,
nodes: &'static [VirtualNode],
}
impl StaticVirtualFileSystem {
pub const fn new(name: &'static str, nodes: &'static [VirtualNode]) -> Self {
Self { name, nodes }
}
fn normalize(path: &str) -> &str {
path.trim_matches('/')
}
fn find_node(&self, path: &str) -> Result<&'static VirtualNode, FileError> {
let normalized = Self::normalize(path);
self.nodes
.iter()
.find(|node| node.path == normalized)
.ok_or(FileError::NotFound)
}
fn file_type(kind: VirtualNodeKind) -> FileType {
match kind {
VirtualNodeKind::Directory => FileType::Directory,
VirtualNodeKind::StaticFile
| VirtualNodeKind::MountsFile
| VirtualNodeKind::ComponentListFile => FileType::RegularFile,
VirtualNodeKind::NullDevice | VirtualNodeKind::ZeroDevice => FileType::CharDevice,
}
}
}
impl VirtualFileSystem for StaticVirtualFileSystem {
fn name(&self) -> &'static str {
self.name
}
fn open_file(&self, path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError> {
let node = self.find_node(path)?;
if node.kind == VirtualNodeKind::Directory {
return Err(FileError::IsDirectory);
}
let file = match node.kind {
VirtualNodeKind::StaticFile => VirtualFile::from_static(node.content),
VirtualNodeKind::MountsFile => VirtualFile::from_owned(render_mounts_snapshot()),
VirtualNodeKind::ComponentListFile => {
VirtualFile::from_owned(crate::component::render_builtin_component_listing())
}
VirtualNodeKind::NullDevice => VirtualFile::null_device(),
VirtualNodeKind::ZeroDevice => VirtualFile::zero_device(),
VirtualNodeKind::Directory => unreachable!("directories returned above"),
};
Ok(Arc::new(spin::Mutex::new(file)))
}
fn stat_path(&self, path: &str) -> Result<FileMetadata, FileError> {
let node = self.find_node(path)?;
let size = match node.kind {
VirtualNodeKind::MountsFile => render_mounts_snapshot().len(),
VirtualNodeKind::ComponentListFile => {
crate::component::render_builtin_component_listing().len()
}
_ => node.content.len(),
};
Ok(FileMetadata {
file_type: Self::file_type(node.kind),
size,
mode: node.mode,
created: 0,
modified: 0,
})
}
fn readdir_path(&self, path: &str) -> Result<Vec<String>, FileError> {
let dir = self.find_node(path)?;
if dir.kind != VirtualNodeKind::Directory {
return Err(FileError::NotDirectory);
}
let parent = Self::normalize(path);
let mut entries = Vec::new();
for node in self.nodes.iter() {
if node.path.is_empty() || node.path == parent {
continue;
}
let child = if parent.is_empty() {
node.path
} else if let Some(rest) = node.path.strip_prefix(parent) {
if let Some(rest) = rest.strip_prefix('/') {
rest
} else {
continue;
}
} else {
continue;
};
if child.is_empty() || child.contains('/') {
continue;
}
entries.push(String::from(child));
}
Ok(entries)
}
}
pub struct VirtualFile {
backing: VirtualFileBacking,
offset: usize,
}
enum VirtualFileBacking {
Static(&'static [u8]),
Owned(Vec<u8>),
NullDevice,
ZeroDevice,
}
impl VirtualFile {
fn from_static(content: &'static [u8]) -> Self {
Self {
backing: VirtualFileBacking::Static(content),
offset: 0,
}
}
fn from_owned(content: Vec<u8>) -> Self {
Self {
backing: VirtualFileBacking::Owned(content),
offset: 0,
}
}
fn null_device() -> Self {
Self {
backing: VirtualFileBacking::NullDevice,
offset: 0,
}
}
fn zero_device() -> Self {
Self {
backing: VirtualFileBacking::ZeroDevice,
offset: 0,
}
}
fn content(&self) -> Option<&[u8]> {
match &self.backing {
VirtualFileBacking::Static(content) => Some(content),
VirtualFileBacking::Owned(content) => Some(content.as_slice()),
VirtualFileBacking::NullDevice | VirtualFileBacking::ZeroDevice => None,
}
}
}
impl File for VirtualFile {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, FileError> {
match self.content() {
Some(content) => {
if self.offset >= content.len() {
return Ok(0);
}
let end = core::cmp::min(self.offset.saturating_add(buf.len()), content.len());
let n = end - self.offset;
buf[..n].copy_from_slice(&content[self.offset..end]);
self.offset = end;
Ok(n)
}
None => match self.backing {
VirtualFileBacking::NullDevice => Ok(0),
VirtualFileBacking::ZeroDevice => {
for byte in buf.iter_mut() {
*byte = 0;
}
Ok(buf.len())
}
VirtualFileBacking::Static(_) | VirtualFileBacking::Owned(_) => unreachable!(),
},
}
}
fn write(&mut self, buf: &[u8]) -> Result<usize, FileError> {
match self.backing {
VirtualFileBacking::NullDevice | VirtualFileBacking::ZeroDevice => Ok(buf.len()),
VirtualFileBacking::Static(_) | VirtualFileBacking::Owned(_) => {
Err(FileError::PermissionDenied)
}
}
}
fn seek(&mut self, pos: SeekFrom) -> Result<usize, FileError> {
let size = self.content().map_or(0, |content| content.len());
let new_offset = match pos {
SeekFrom::Start(offset) => offset,
SeekFrom::Current(delta) if delta >= 0 => self.offset.saturating_add(delta as usize),
SeekFrom::Current(delta) => self.offset.saturating_sub((-delta) as usize),
SeekFrom::End(delta) if delta >= 0 => size.saturating_add(delta as usize),
SeekFrom::End(delta) => size.saturating_sub((-delta) as usize),
};
self.offset = new_offset;
Ok(self.offset)
}
}
const PROC_NODES: &[VirtualNode] = &[
VirtualNode::directory(""),
VirtualNode::file(
"version",
b"AhRightOS 0.1.0 teaching kernel\nLinux-inspired procfs module\n",
),
VirtualNode::file("meminfo", b"MemTotal: 64 MiB\nMemFree: teaching-snapshot\n"),
VirtualNode::mounts_file("mounts"),
];
const SYS_NODES: &[VirtualNode] = &[
VirtualNode::directory(""),
VirtualNode::directory("kernel"),
VirtualNode::file("kernel/name", b"AhRightOS\n"),
VirtualNode::file("kernel/ostype", b"AhRightOS\n"),
VirtualNode::file("kernel/release", b"0.1.0\n"),
VirtualNode::directory("fs"),
VirtualNode::file(
"fs/componentization",
b"Cargo workspace + ahrightos-fs VirtualFileSystem trait + VFS mount table + component registry\n",
),
VirtualNode::directory("fs/ahrightos"),
VirtualNode::component_list_file("fs/ahrightos/components"),
VirtualNode::file(
"fs/ahrightos/manifest-format",
b"AhRightOS component manifest v0: id, version, layer, stability, capabilities, requires, provides\n",
),
];
const DEV_NODES: &[VirtualNode] = &[
VirtualNode::directory(""),
VirtualNode::null_device("null"),
VirtualNode::zero_device("zero"),
];
pub fn default_procfs() -> Arc<dyn VirtualFileSystem> {
Arc::new(StaticVirtualFileSystem::new("procfs", PROC_NODES))
}
pub fn default_sysfs() -> Arc<dyn VirtualFileSystem> {
Arc::new(StaticVirtualFileSystem::new("sysfs", SYS_NODES))
}
pub fn default_devtmpfs() -> Arc<dyn VirtualFileSystem> {
Arc::new(StaticVirtualFileSystem::new("devtmpfs", DEV_NODES))
}
fn render_mounts_snapshot() -> Vec<u8> {
let manager = crate::manager::FS_MANAGER.lock();
let mut mounts = String::from("ramfs / ramfs rw\n");
for mount in manager.mounts.iter() {
let fs_name = match mount.fs_type {
crate::manager::FsType::RamFS => "ramfs",
crate::manager::FsType::BlkFS => "blkfs",
crate::manager::FsType::ProcFS => "procfs",
crate::manager::FsType::SysFS => "sysfs",
crate::manager::FsType::DevTmpFS => "devtmpfs",
};
let options = match mount.fs_type {
crate::manager::FsType::ProcFS | crate::manager::FsType::SysFS => "ro",
crate::manager::FsType::RamFS
| crate::manager::FsType::BlkFS
| crate::manager::FsType::DevTmpFS => "rw",
};
let _ = writeln!(mounts, "{} {} {} {}", fs_name, mount.path, fs_name, options);
}
mounts.into_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
static TEST_NODES: &[VirtualNode] = &[
VirtualNode::directory(""),
VirtualNode::directory("kernel"),
VirtualNode::file("kernel/name", b"AhRightOS\n"),
VirtualNode::file("kernel2", b"same-prefix sibling\n"),
];
#[test]
fn readdir_lists_only_direct_children() {
let fs = StaticVirtualFileSystem::new("testfs", TEST_NODES);
assert_eq!(
fs.readdir_path("").unwrap(),
vec![String::from("kernel"), String::from("kernel2")]
);
assert_eq!(
fs.readdir_path("kernel").unwrap(),
vec![String::from("name")]
);
}
}