Skip to main content

ahrightos_fs/
virtualfs.rs

1//! Linux-inspired virtual filesystems for AhRightOS.
2//!
3//! This module provides small, original teaching implementations of the
4//! concepts behind Linux `/proc`, `/sys`, and `/dev`:
5//! - pseudo files expose kernel/system facts as regular readable files;
6//! - attributes are small ASCII values;
7//! - character devices such as `null` and `zero` implement the `File` trait.
8//!
9//! The implementation is intentionally table-driven and read-mostly so it can
10//! live inside the portable `ahrightos-fs` crate without depending on kernel
11//! hardware state.
12
13use crate::file::{File, FileError, FileMetadata, FileType, SeekFrom};
14use alloc::string::String;
15use alloc::sync::Arc;
16use alloc::vec::Vec;
17use core::fmt::Write;
18
19/// Filesystem interface used by the VFS mount table for pseudo filesystems.
20pub trait VirtualFileSystem: Send + Sync {
21    /// Human-readable filesystem name such as `procfs`, `sysfs`, or `devtmpfs`.
22    fn name(&self) -> &'static str;
23
24    /// Open a file path relative to this virtual filesystem's mount root.
25    fn open_file(&self, path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError>;
26
27    /// Return file metadata for a path relative to this mount.
28    fn stat_path(&self, path: &str) -> Result<FileMetadata, FileError>;
29
30    /// List a virtual directory.
31    fn readdir_path(&self, path: &str) -> Result<Vec<String>, FileError>;
32
33    /// Virtual filesystems are read-mostly by default.
34    fn create_file(&self, _path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError> {
35        Err(FileError::PermissionDenied)
36    }
37
38    /// Virtual filesystems are read-mostly by default.
39    fn create_dir(&self, _path: &str) -> Result<(), FileError> {
40        Err(FileError::PermissionDenied)
41    }
42
43    /// Virtual filesystems are read-mostly by default.
44    fn remove_file(&self, _path: &str) -> Result<(), FileError> {
45        Err(FileError::PermissionDenied)
46    }
47}
48
49/// Node kind in a static virtual filesystem tree.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum VirtualNodeKind {
52    Directory,
53    StaticFile,
54    MountsFile,
55    ComponentListFile,
56    NullDevice,
57    ZeroDevice,
58}
59
60/// Static virtual filesystem node.
61#[derive(Debug, Clone, Copy)]
62pub struct VirtualNode {
63    /// Path relative to mount root; root itself is represented by `""`.
64    pub path: &'static str,
65    pub kind: VirtualNodeKind,
66    pub mode: u32,
67    pub content: &'static [u8],
68}
69
70impl VirtualNode {
71    pub const fn directory(path: &'static str) -> Self {
72        Self {
73            path,
74            kind: VirtualNodeKind::Directory,
75            mode: 0o555,
76            content: b"",
77        }
78    }
79
80    pub const fn file(path: &'static str, content: &'static [u8]) -> Self {
81        Self {
82            path,
83            kind: VirtualNodeKind::StaticFile,
84            mode: 0o444,
85            content,
86        }
87    }
88
89    pub const fn null_device(path: &'static str) -> Self {
90        Self {
91            path,
92            kind: VirtualNodeKind::NullDevice,
93            mode: 0o666,
94            content: b"",
95        }
96    }
97
98    pub const fn zero_device(path: &'static str) -> Self {
99        Self {
100            path,
101            kind: VirtualNodeKind::ZeroDevice,
102            mode: 0o666,
103            content: b"",
104        }
105    }
106
107    pub const fn mounts_file(path: &'static str) -> Self {
108        Self {
109            path,
110            kind: VirtualNodeKind::MountsFile,
111            mode: 0o444,
112            content: b"",
113        }
114    }
115
116    pub const fn component_list_file(path: &'static str) -> Self {
117        Self {
118            path,
119            kind: VirtualNodeKind::ComponentListFile,
120            mode: 0o444,
121            content: b"",
122        }
123    }
124}
125
126/// A compact table-backed pseudo filesystem.
127pub struct StaticVirtualFileSystem {
128    name: &'static str,
129    nodes: &'static [VirtualNode],
130}
131
132impl StaticVirtualFileSystem {
133    pub const fn new(name: &'static str, nodes: &'static [VirtualNode]) -> Self {
134        Self { name, nodes }
135    }
136
137    fn normalize(path: &str) -> &str {
138        path.trim_matches('/')
139    }
140
141    fn find_node(&self, path: &str) -> Result<&'static VirtualNode, FileError> {
142        let normalized = Self::normalize(path);
143        self.nodes
144            .iter()
145            .find(|node| node.path == normalized)
146            .ok_or(FileError::NotFound)
147    }
148
149    fn file_type(kind: VirtualNodeKind) -> FileType {
150        match kind {
151            VirtualNodeKind::Directory => FileType::Directory,
152            VirtualNodeKind::StaticFile
153            | VirtualNodeKind::MountsFile
154            | VirtualNodeKind::ComponentListFile => FileType::RegularFile,
155            VirtualNodeKind::NullDevice | VirtualNodeKind::ZeroDevice => FileType::CharDevice,
156        }
157    }
158}
159
160impl VirtualFileSystem for StaticVirtualFileSystem {
161    fn name(&self) -> &'static str {
162        self.name
163    }
164
165    fn open_file(&self, path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError> {
166        let node = self.find_node(path)?;
167        if node.kind == VirtualNodeKind::Directory {
168            return Err(FileError::IsDirectory);
169        }
170        let file = match node.kind {
171            VirtualNodeKind::StaticFile => VirtualFile::from_static(node.content),
172            VirtualNodeKind::MountsFile => VirtualFile::from_owned(render_mounts_snapshot()),
173            VirtualNodeKind::ComponentListFile => {
174                VirtualFile::from_owned(crate::component::render_builtin_component_listing())
175            }
176            VirtualNodeKind::NullDevice => VirtualFile::null_device(),
177            VirtualNodeKind::ZeroDevice => VirtualFile::zero_device(),
178            VirtualNodeKind::Directory => unreachable!("directories returned above"),
179        };
180        Ok(Arc::new(spin::Mutex::new(file)))
181    }
182
183    fn stat_path(&self, path: &str) -> Result<FileMetadata, FileError> {
184        let node = self.find_node(path)?;
185        let size = match node.kind {
186            VirtualNodeKind::MountsFile => render_mounts_snapshot().len(),
187            VirtualNodeKind::ComponentListFile => {
188                crate::component::render_builtin_component_listing().len()
189            }
190            _ => node.content.len(),
191        };
192        Ok(FileMetadata {
193            file_type: Self::file_type(node.kind),
194            size,
195            mode: node.mode,
196            created: 0,
197            modified: 0,
198        })
199    }
200
201    fn readdir_path(&self, path: &str) -> Result<Vec<String>, FileError> {
202        let dir = self.find_node(path)?;
203        if dir.kind != VirtualNodeKind::Directory {
204            return Err(FileError::NotDirectory);
205        }
206
207        let parent = Self::normalize(path);
208        let mut entries = Vec::new();
209        for node in self.nodes.iter() {
210            if node.path.is_empty() || node.path == parent {
211                continue;
212            }
213
214            let child = if parent.is_empty() {
215                node.path
216            } else if let Some(rest) = node.path.strip_prefix(parent) {
217                if let Some(rest) = rest.strip_prefix('/') {
218                    rest
219                } else {
220                    continue;
221                }
222            } else {
223                continue;
224            };
225
226            if child.is_empty() || child.contains('/') {
227                continue;
228            }
229            entries.push(String::from(child));
230        }
231        Ok(entries)
232    }
233}
234
235/// Open file handle for a virtual node.
236pub struct VirtualFile {
237    backing: VirtualFileBacking,
238    offset: usize,
239}
240
241enum VirtualFileBacking {
242    Static(&'static [u8]),
243    Owned(Vec<u8>),
244    NullDevice,
245    ZeroDevice,
246}
247
248impl VirtualFile {
249    fn from_static(content: &'static [u8]) -> Self {
250        Self {
251            backing: VirtualFileBacking::Static(content),
252            offset: 0,
253        }
254    }
255
256    fn from_owned(content: Vec<u8>) -> Self {
257        Self {
258            backing: VirtualFileBacking::Owned(content),
259            offset: 0,
260        }
261    }
262
263    fn null_device() -> Self {
264        Self {
265            backing: VirtualFileBacking::NullDevice,
266            offset: 0,
267        }
268    }
269
270    fn zero_device() -> Self {
271        Self {
272            backing: VirtualFileBacking::ZeroDevice,
273            offset: 0,
274        }
275    }
276
277    fn content(&self) -> Option<&[u8]> {
278        match &self.backing {
279            VirtualFileBacking::Static(content) => Some(content),
280            VirtualFileBacking::Owned(content) => Some(content.as_slice()),
281            VirtualFileBacking::NullDevice | VirtualFileBacking::ZeroDevice => None,
282        }
283    }
284}
285
286impl File for VirtualFile {
287    fn read(&mut self, buf: &mut [u8]) -> Result<usize, FileError> {
288        match self.content() {
289            Some(content) => {
290                if self.offset >= content.len() {
291                    return Ok(0);
292                }
293                let end = core::cmp::min(self.offset.saturating_add(buf.len()), content.len());
294                let n = end - self.offset;
295                buf[..n].copy_from_slice(&content[self.offset..end]);
296                self.offset = end;
297                Ok(n)
298            }
299            None => match self.backing {
300                VirtualFileBacking::NullDevice => Ok(0),
301                VirtualFileBacking::ZeroDevice => {
302                    for byte in buf.iter_mut() {
303                        *byte = 0;
304                    }
305                    Ok(buf.len())
306                }
307                VirtualFileBacking::Static(_) | VirtualFileBacking::Owned(_) => unreachable!(),
308            },
309        }
310    }
311
312    fn write(&mut self, buf: &[u8]) -> Result<usize, FileError> {
313        match self.backing {
314            VirtualFileBacking::NullDevice | VirtualFileBacking::ZeroDevice => Ok(buf.len()),
315            VirtualFileBacking::Static(_) | VirtualFileBacking::Owned(_) => {
316                Err(FileError::PermissionDenied)
317            }
318        }
319    }
320
321    fn seek(&mut self, pos: SeekFrom) -> Result<usize, FileError> {
322        let size = self.content().map_or(0, |content| content.len());
323        let new_offset = match pos {
324            SeekFrom::Start(offset) => offset,
325            SeekFrom::Current(delta) if delta >= 0 => self.offset.saturating_add(delta as usize),
326            SeekFrom::Current(delta) => self.offset.saturating_sub((-delta) as usize),
327            SeekFrom::End(delta) if delta >= 0 => size.saturating_add(delta as usize),
328            SeekFrom::End(delta) => size.saturating_sub((-delta) as usize),
329        };
330        self.offset = new_offset;
331        Ok(self.offset)
332    }
333}
334
335const PROC_NODES: &[VirtualNode] = &[
336    VirtualNode::directory(""),
337    VirtualNode::file(
338        "version",
339        b"AhRightOS 0.1.0 teaching kernel\nLinux-inspired procfs module\n",
340    ),
341    VirtualNode::file("meminfo", b"MemTotal: 64 MiB\nMemFree: teaching-snapshot\n"),
342    VirtualNode::mounts_file("mounts"),
343];
344
345const SYS_NODES: &[VirtualNode] = &[
346    VirtualNode::directory(""),
347    VirtualNode::directory("kernel"),
348    VirtualNode::file("kernel/name", b"AhRightOS\n"),
349    VirtualNode::file("kernel/ostype", b"AhRightOS\n"),
350    VirtualNode::file("kernel/release", b"0.1.0\n"),
351    VirtualNode::directory("fs"),
352    VirtualNode::file(
353        "fs/componentization",
354        b"Cargo workspace + ahrightos-fs VirtualFileSystem trait + VFS mount table + component registry\n",
355    ),
356    VirtualNode::directory("fs/ahrightos"),
357    VirtualNode::component_list_file("fs/ahrightos/components"),
358    VirtualNode::file(
359        "fs/ahrightos/manifest-format",
360        b"AhRightOS component manifest v0: id, version, layer, stability, capabilities, requires, provides\n",
361    ),
362];
363
364const DEV_NODES: &[VirtualNode] = &[
365    VirtualNode::directory(""),
366    VirtualNode::null_device("null"),
367    VirtualNode::zero_device("zero"),
368];
369
370pub fn default_procfs() -> Arc<dyn VirtualFileSystem> {
371    Arc::new(StaticVirtualFileSystem::new("procfs", PROC_NODES))
372}
373
374pub fn default_sysfs() -> Arc<dyn VirtualFileSystem> {
375    Arc::new(StaticVirtualFileSystem::new("sysfs", SYS_NODES))
376}
377
378pub fn default_devtmpfs() -> Arc<dyn VirtualFileSystem> {
379    Arc::new(StaticVirtualFileSystem::new("devtmpfs", DEV_NODES))
380}
381
382fn render_mounts_snapshot() -> Vec<u8> {
383    let manager = crate::manager::FS_MANAGER.lock();
384    let mut mounts = String::from("ramfs / ramfs rw\n");
385    for mount in manager.mounts.iter() {
386        let fs_name = match mount.fs_type {
387            crate::manager::FsType::RamFS => "ramfs",
388            crate::manager::FsType::BlkFS => "blkfs",
389            crate::manager::FsType::ProcFS => "procfs",
390            crate::manager::FsType::SysFS => "sysfs",
391            crate::manager::FsType::DevTmpFS => "devtmpfs",
392        };
393        let options = match mount.fs_type {
394            crate::manager::FsType::ProcFS | crate::manager::FsType::SysFS => "ro",
395            crate::manager::FsType::RamFS
396            | crate::manager::FsType::BlkFS
397            | crate::manager::FsType::DevTmpFS => "rw",
398        };
399        let _ = writeln!(mounts, "{} {} {} {}", fs_name, mount.path, fs_name, options);
400    }
401    mounts.into_bytes()
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use alloc::vec;
408
409    static TEST_NODES: &[VirtualNode] = &[
410        VirtualNode::directory(""),
411        VirtualNode::directory("kernel"),
412        VirtualNode::file("kernel/name", b"AhRightOS\n"),
413        VirtualNode::file("kernel2", b"same-prefix sibling\n"),
414    ];
415
416    #[test]
417    fn readdir_lists_only_direct_children() {
418        let fs = StaticVirtualFileSystem::new("testfs", TEST_NODES);
419        assert_eq!(
420            fs.readdir_path("").unwrap(),
421            vec![String::from("kernel"), String::from("kernel2")]
422        );
423        assert_eq!(
424            fs.readdir_path("kernel").unwrap(),
425            vec![String::from("name")]
426        );
427    }
428}