// --------------------------------------------------------------------------- // VFS — "everything is a file". // // A tiny hierarchical filesystem namespace: directories, in-memory regular // files (ramfs, writable via the shell), and character devices under /dev. // Device reads re-generate their content on every access, so // `cat /dev/rtc` shows the *current* time and `cat /dev/mem` hexdumps live // physical memory. Writes to /dev/console print to the screen, writes to // /dev/null disappear. // // Nodes live in a flat table; handles are stable indices (removal leaves a // tombstone). Single-threaded, interrupt-safe because no ISR touches the // table. A real disk filesystem can mount under this same namespace later. // --------------------------------------------------------------------------- extern crate alloc; use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; use crate::gop; use crate::memory; use crate::pci; use crate::rtc; use crate::serial; /// /dev/mem covers the low physical-memory window (identity-mapped RAM). pub const MEM_WINDOW: u64 = 64 * 1024; #[derive(Clone, Copy, PartialEq)] pub enum FileType { Regular, Char, Dir, } #[derive(Clone, Copy)] enum CharDevice { Mem, Rtc, Pci, Version, Meminfo, Null, Zero, Console, Serial, } enum NodeData { Regular(Vec), Dir(Vec), Char(CharDevice), } struct Node { name: String, kind: FileType, parent: Option, data: NodeData, removed: bool, } /// Node table; root is always handle 0. Boxed so the static stays a raw /// pointer (matches the rest of the crate's style). static mut NODES: *mut Vec = core::ptr::null_mut(); fn nodes() -> &'static mut Vec { unsafe { &mut *NODES } } fn add(name: &str, parent: Option, kind: FileType, data: NodeData) -> usize { let h = nodes().len(); nodes().push(Node { name: name.into(), kind, parent, data, removed: false, }); if let Some(p) = parent { if let NodeData::Dir(v) = &mut nodes()[p].data { v.push(h); } } h } /// Build the tree and report (dirs, files). pub fn init() -> (usize, usize) { unsafe { NODES = Box::into_raw(Box::new(Vec::new())); } let root = add("/", None, FileType::Dir, NodeData::Dir(Vec::new())); let dev = add("dev", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); let _tmp = add("tmp", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); let etc = add("etc", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); add("mem", Some(dev), FileType::Char, NodeData::Char(CharDevice::Mem)); add("rtc", Some(dev), FileType::Char, NodeData::Char(CharDevice::Rtc)); add("pci", Some(dev), FileType::Char, NodeData::Char(CharDevice::Pci)); add( "version", Some(dev), FileType::Char, NodeData::Char(CharDevice::Version), ); add( "meminfo", Some(dev), FileType::Char, NodeData::Char(CharDevice::Meminfo), ); add( "null", Some(dev), FileType::Char, NodeData::Char(CharDevice::Null), ); add( "zero", Some(dev), FileType::Char, NodeData::Char(CharDevice::Zero), ); add( "console", Some(dev), FileType::Char, NodeData::Char(CharDevice::Console), ); add( "serial", Some(dev), FileType::Char, NodeData::Char(CharDevice::Serial), ); add( "motd", Some(etc), FileType::Regular, NodeData::Regular( b"Welcome to rs-dos!\nEverything is a file. Try: cat /dev/rtc, cat /dev/pci, cat /dev/mem\n" .to_vec(), ), ); let dirs = nodes() .iter() .filter(|n| n.kind == FileType::Dir) .count(); let files = nodes().len() - dirs; (dirs, files) } fn lookup(dir: usize, name: &str) -> Option { let children = match &nodes()[dir].data { NodeData::Dir(v) => v.clone(), _ => return None, }; for c in children { let n = &nodes()[c]; if !n.removed && n.name == name { return Some(c); } } None } /// Resolve an absolute path (supports `.` and `..`). pub fn resolve(path: &str) -> Option { if nodes().is_empty() { return None; } let mut cur = 0usize; // root for seg in path.split('/') { match seg { "" | "." => {} ".." => { if let Some(p) = nodes()[cur].parent { cur = p; } } name => { cur = lookup(cur, name)?; } } } Some(cur) } pub fn kind(h: usize) -> FileType { nodes()[h].kind } pub fn size(h: usize) -> u64 { match &nodes()[h].data { NodeData::Regular(v) => v.len() as u64, _ => 0, } } /// Entries of a directory as (name, kind, size) — tombstoned nodes skipped. pub fn children(h: usize) -> Vec<(String, FileType, u64)> { let kids = match &nodes()[h].data { NodeData::Dir(v) => v.clone(), _ => return Vec::new(), }; let mut out = Vec::new(); for c in kids { let n = &nodes()[c]; if n.removed { continue; } let sz = match &n.data { NodeData::Regular(v) => v.len() as u64, _ => 0, }; out.push((n.name.clone(), n.kind, sz)); } out } pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result { if buf.is_empty() { return Ok(0); } match &mut nodes()[h].data { NodeData::Regular(v) => { let p = pos as usize; if p >= v.len() { return Ok(0); } let n = buf.len().min(v.len() - p); buf[..n].copy_from_slice(&v[p..p + n]); Ok(n) } NodeData::Char(CharDevice::Mem) => { // /dev/mem is a raw byte window: file offset = physical address. let p = pos as usize; if p >= MEM_WINDOW as usize { return Ok(0); } let n = buf.len().min(MEM_WINDOW as usize - p); for i in 0..n { buf[i] = unsafe { core::ptr::read_volatile((p + i) as *const u8) }; } Ok(n) } NodeData::Char(CharDevice::Zero) => { buf.fill(0); Ok(buf.len()) } NodeData::Char(dev) => { let text = dev_text(*dev, pos); let bytes = text.as_bytes(); let p = pos as usize; if p >= bytes.len() { return Ok(0); } let n = buf.len().min(bytes.len() - p); buf[..n].copy_from_slice(&bytes[p..p + n]); Ok(n) } NodeData::Dir(_) => Err(()), } } pub fn write(h: usize, buf: &[u8], pos: u64) -> Result { match &mut nodes()[h].data { NodeData::Regular(v) => { let p = pos as usize; if p + buf.len() > v.len() { v.resize(p + buf.len(), 0); } v[p..p + buf.len()].copy_from_slice(buf); Ok(buf.len()) } NodeData::Char(dev) => match dev { CharDevice::Null | CharDevice::Zero => Ok(buf.len()), CharDevice::Console => { for &b in buf { if b == b'\n' { gop::put_char('\n'); serial::put_byte(b'\n'); } else if b.is_ascii() { gop::put_char(b as char); serial::put_byte(b); } } Ok(buf.len()) } CharDevice::Serial => { for &b in buf { serial::put_byte(b); } Ok(buf.len()) } _ => Err(()), // read-only devices }, NodeData::Dir(_) => Err(()), } } /// Truncate/resize a regular file. pub fn set_len(h: usize, len: u64) -> Result<(), ()> { match &mut nodes()[h].data { NodeData::Regular(v) => { v.resize(len as usize, 0); Ok(()) } _ => Err(()), } } /// Create a Regular or Dir child. Fails if the name exists or is invalid. pub fn create(parent: usize, name: &str, kind: FileType) -> Result { if name.is_empty() || name.contains('/') || kind == FileType::Char { return Err(()); } if lookup(parent, name).is_some() { return Err(()); } let data = match kind { FileType::Regular => NodeData::Regular(Vec::new()), FileType::Dir => NodeData::Dir(Vec::new()), FileType::Char => return Err(()), }; Ok(add(name, Some(parent), kind, data)) } /// Remove a regular file or an empty directory (tombstone). pub fn remove(h: usize) -> Result<(), ()> { let parent = match nodes()[h].parent { Some(p) => p, None => return Err(()), }; let empty = match &nodes()[h].data { NodeData::Dir(v) => v.is_empty(), _ => true, }; if !empty { return Err(()); } if let NodeData::Dir(v) = &mut nodes()[parent].data { v.retain(|&c| c != h); } let n = &mut nodes()[h]; n.removed = true; n.parent = None; Ok(()) } // --- path helpers --------------------------------------------------------- /// Split `/a/b/c` into (handle of `/a/b`, "c"). fn split_parent_name(path: &str) -> Result<(usize, String), ()> { let path = path.trim_end_matches('/'); if path.is_empty() { return Err(()); } let (parent_path, name) = match path.rfind('/') { Some(i) => (&path[..i], &path[i + 1..]), None => ("/", path), }; if name.is_empty() { return Err(()); } Some((resolve(parent_path).ok_or(())?, name.into())).ok_or(()) } pub fn mkdir(path: &str) -> Result<(), ()> { let (p, n) = split_parent_name(path)?; create(p, &n, FileType::Dir).map(|_| ()) } pub fn touch(path: &str) -> Result<(), ()> { if resolve(path).is_some() { return Ok(()); } let (p, n) = split_parent_name(path)?; create(p, &n, FileType::Regular).map(|_| ()) } /// Resolve a path, creating a regular file if it doesn't exist yet. pub fn ensure_file(path: &str) -> Result { if let Some(h) = resolve(path) { if kind(h) == FileType::Dir { Err(()) } else { Ok(h) } } else { touch(path)?; resolve(path).ok_or(()) } } pub fn remove_path(path: &str) -> Result<(), ()> { let h = resolve(path).ok_or(())?; remove(h) } // --- character devices ---------------------------------------------------- fn version_text() -> String { alloc::format!( "rsnix v{} — TempleOS-inspired toy kernel\nUEFI x86_64, identity paging, free-list heap, bitmap pages\n", env!("CARGO_PKG_VERSION") ) } fn meminfo_text() -> String { let h = memory::heap_stats(); let p = memory::page_stats(); let (map, key) = memory::get_map(); let mut usable_kb = 0u64; for d in map { if d.kind == 7 { usable_kb += d.number_of_pages * 4; } } alloc::format!( "heap: {} KB capacity, {} KB free in {} block(s)\npages: {} free / {} used, {} MB span\nmem: {} entries (key 0x{:x}), {} MB usable\n", h.heap_size >> 10, h.free_bytes >> 10, h.free_blocks, p.free_pages, p.used_pages, p.span_bytes >> 20, map.len(), key, usable_kb >> 10, ) } fn pci_listing() -> String { let mut out = String::new(); let devices = pci::probe(); out += &alloc::format!("{} device(s)\n", devices.len()); for d in &devices { out += &alloc::format!( "{:02x}:{:02x}.{} {:<16} {:04x}:{:04x}\n", d.bus, d.device, d.func, pci::class_name(d.class, d.subclass), d.vendor, d.device_id ); } out } fn dev_text(dev: CharDevice, _pos: u64) -> String { match dev { CharDevice::Mem => String::new(), // raw bytes, special-cased in read() CharDevice::Rtc => { let t = rtc::now(); alloc::format!( "{}-{:02}-{:02} {:02}:{:02}:{:02}\n", t.year, t.month, t.day, t.hour, t.minute, t.second ) } CharDevice::Pci => pci_listing(), CharDevice::Version => version_text(), CharDevice::Meminfo => meminfo_text(), CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), CharDevice::Zero => String::new(), } } // --- self-test ------------------------------------------------------------ pub fn self_test() -> bool { let mut buf = [0u8; 8]; let h = match ensure_file("/tmp/selftest") { Ok(h) => h, Err(_) => return false, }; if write(h, b"hello", 0) != Ok(5) { return false; } if read(h, &mut buf, 0) != Ok(5) || &buf[..5] != b"hello" { return false; } if remove(h).is_err() || resolve("/tmp/selftest").is_some() { return false; } // device write goes nowhere: echo into /dev/null if let Ok(nul) = ensure_file("/dev/null") { let _ = write(nul, b"discard me", 0); } true }