1use crate::file::{File, FileError, FileMetadata, FileType, SeekFrom};
14use alloc::string::String;
15use alloc::sync::Arc;
16use alloc::vec::Vec;
17use core::fmt::Write;
18
19pub trait VirtualFileSystem: Send + Sync {
21 fn name(&self) -> &'static str;
23
24 fn open_file(&self, path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError>;
26
27 fn stat_path(&self, path: &str) -> Result<FileMetadata, FileError>;
29
30 fn readdir_path(&self, path: &str) -> Result<Vec<String>, FileError>;
32
33 fn create_file(&self, _path: &str) -> Result<Arc<spin::Mutex<dyn File>>, FileError> {
35 Err(FileError::PermissionDenied)
36 }
37
38 fn create_dir(&self, _path: &str) -> Result<(), FileError> {
40 Err(FileError::PermissionDenied)
41 }
42
43 fn remove_file(&self, _path: &str) -> Result<(), FileError> {
45 Err(FileError::PermissionDenied)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum VirtualNodeKind {
52 Directory,
53 StaticFile,
54 MountsFile,
55 ComponentListFile,
56 NullDevice,
57 ZeroDevice,
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct VirtualNode {
63 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
126pub 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
235pub 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}