// --------------------------------------------------------------------------- // 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 crate::gop; use crate::memory; use crate::pci; use crate::rtc; use crate::serial; use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; /// /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), Disk(DiskInfo), } /// A node backed by the FAT32 volume (`/disk` mount). `parent` is the /// directory cluster holding the entry (used to re-locate it on every /// op, so it stays correct as the volume changes), `cluster` the entry's /// own first cluster. struct DiskInfo { parent: u32, name: String, is_dir: bool, size: u64, cluster: u32, } 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(), ), ); // Mount the FAT32 volume at /disk when one is present. if crate::fat::is_mounted() { let root_cl = crate::fat::root_cluster(); add( "disk", Some(root), FileType::Dir, NodeData::Disk(DiskInfo { parent: root_cl, name: String::new(), is_dir: true, size: 0, cluster: root_cl, }), ); } 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 `..`). Paths under /// `/disk` walk the mounted FAT32 volume through ephemeral Disk nodes. pub fn resolve(path: &str) -> Option { if nodes().is_empty() { return None; } let mut cur = 0usize; // root // (cluster, parent, name) of directories descended into under /disk. let mut dstack: Vec<(u32, u32, String)> = Vec::new(); for seg in path.split('/') { match seg { "" | "." => {} ".." => { if let Some((cl, p, n)) = dstack.pop() { cur = materialize_disk(DiskInfo { parent: p, name: n, is_dir: true, size: 0, cluster: cl, }); } else if let Some(p) = nodes()[cur].parent { cur = p; } } name => { let disk_dir: Option = match &nodes()[cur].data { NodeData::Disk(info) => Some(info.cluster), _ => None, }; if let Some(dir_cluster) = disk_dir { let e = crate::fat::find(dir_cluster, name).ok()?; let d = DiskInfo { parent: dir_cluster, name: e.name.clone(), is_dir: e.is_dir, size: e.size, cluster: e.cluster, }; if e.is_dir { dstack.push((e.cluster, dir_cluster, e.name)); } cur = materialize_disk(d); } else { 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, NodeData::Disk(i) => i.size, _ => 0, } } /// Entries of a directory as (name, kind, size) — tombstoned nodes /// skipped; disk directories are listed fresh from the FAT volume. pub fn children(h: usize) -> Vec<(String, FileType, u64)> { if let NodeData::Disk(i) = &nodes()[h].data { let cl = i.cluster; return crate::fat::list_dir(cl) .map(|es| { es.into_iter() .map(|e| { ( e.name, if e.is_dir { FileType::Dir } else { FileType::Regular }, e.size, ) }) .collect() }) .unwrap_or_default(); } 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::Disk(i) => crate::fat::read_file(i.parent, &i.name, buf, pos), 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::Disk(i) => crate::fat::write_file(i.parent, &i.name, buf, pos), 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(()) } NodeData::Disk(i) => crate::fat::set_len(i.parent, &i.name, len), _ => 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(()); } // Disk directory: create through the FAT volume. let disk_parent: Option = match &nodes()[parent].data { NodeData::Disk(i) => { if !i.is_dir { return Err(()); } Some(i.cluster) } _ => None, }; if let Some(p) = disk_parent { if crate::fat::find(p, name).is_ok() { return Err(()); } crate::fat::create(p, name, kind == FileType::Dir)?; let e = crate::fat::find(p, name)?; return Ok(materialize_disk(DiskInfo { parent: p, name: e.name, is_dir: e.is_dir, size: e.size, cluster: e.cluster, })); } 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<(), ()> { // Disk nodes: delete through the FAT volume (dirs must be empty). if let NodeData::Disk(i) = &nodes()[h].data { if i.name.is_empty() { return Err(()); // the /disk mount itself } let p = i.parent; let n = i.name.clone(); return crate::fat::remove(p, &n); } 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) } // --- disk mount ------------------------------------------------------------ /// Round-robin pool of ephemeral disk nodes so a long-lived shell doesn't /// grow the node table forever. const DISK_SLOTS: usize = 24; static mut DISK_POOL: *mut Vec = core::ptr::null_mut(); static mut DISK_ROBIN: usize = 0; fn materialize_disk(info: DiskInfo) -> usize { unsafe { if DISK_POOL.is_null() { DISK_POOL = Box::into_raw(Box::new(Vec::new())); } let pool = &mut *DISK_POOL; let idx = DISK_ROBIN % DISK_SLOTS; DISK_ROBIN = DISK_ROBIN.wrapping_add(1); let kind = if info.is_dir { FileType::Dir } else { FileType::Regular }; if idx < pool.len() { let h = pool[idx]; let n = &mut nodes()[h]; n.name = info.name.clone(); n.kind = kind; n.parent = None; n.data = NodeData::Disk(info); n.removed = false; return h; } let name = info.name.clone(); let h = add(&name, None, kind, NodeData::Disk(info)); pool.push(h); h } } // --- character devices ---------------------------------------------------- fn version_text() -> String { alloc::format!( "rsnix v{} — Official RS-DOS 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 }