// --------------------------------------------------------------------------- // 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, Klog, Null, Zero, Console, Serial, Mouse, } 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 { vol: usize, // index into fat::VOLS (which FAT32 volume this lives on) 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 && 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( "klog", Some(dev), FileType::Char, NodeData::Char(CharDevice::Klog), ); 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( "mouse", Some(dev), FileType::Char, NodeData::Char(CharDevice::Mouse), ); 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 every FAT32 volume under /disk// (UID = label-serial). // `rescan_disks()` also keeps this table current after hotplug events. let _ = rescan_disks(); let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); let files = nodes().len() - dirs; (dirs, files) } /// (Re)sync the /disk mount table with the FAT volumes: create the /disk /// dir lazily, ensure a mount node for every mounted volume (refreshing /// its backing info), and tombstone nodes whose volume was unmounted. /// Returns (added, removed) node ops. pub fn rescan_disks() -> (usize, usize) { let mut added = 0usize; let mut removed = 0usize; let disk_dir = match lookup(0, "disk") { Some(h) => h, None => add("disk", Some(0), FileType::Dir, NodeData::Dir(Vec::new())), }; for vol in 0..crate::fat::volume_count() { if !crate::fat::is_mounted(vol) { continue; } let uid = String::from(crate::fat::uid(vol)); let root_cl = crate::fat::root_cluster(vol); match lookup(disk_dir, &uid) { Some(h) => { // Refresh the backing info in case the volume's slot moved. if let NodeData::Disk(i) = &mut nodes()[h].data { i.vol = vol; i.parent = root_cl; i.cluster = root_cl; } nodes()[h].removed = false; } None => { add( &uid, Some(disk_dir), FileType::Dir, NodeData::Disk(DiskInfo { vol, parent: root_cl, name: String::new(), is_dir: true, size: 0, cluster: root_cl, }), ); added += 1; } } } if let NodeData::Dir(v) = &nodes()[disk_dir].data { for &c in v.clone().iter() { if nodes()[c].removed { continue; } if let NodeData::Disk(i) = &nodes()[c].data && !crate::fat::is_mounted(i.vol) { nodes()[c].removed = true; removed += 1; } } } (added, removed) } 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 // (vol, cluster, parent, name) of directories descended into under /disk. let mut dstack: Vec<(usize, u32, u32, String)> = Vec::new(); for seg in path.split('/') { match seg { "" | "." => {} ".." => { if let Some((vol, cl, p, n)) = dstack.pop() { cur = materialize_disk(DiskInfo { vol, parent: p, name: n, is_dir: true, size: 0, cluster: cl, }); } else if let Some(p) = nodes()[cur].parent { cur = p; } } name => { let disk_ctx: Option<(usize, u32)> = match &nodes()[cur].data { NodeData::Disk(info) => Some((info.vol, info.cluster)), _ => None, }; if let Some((vol, dir_cluster)) = disk_ctx { let e = crate::fat::find(vol, dir_cluster, name).ok()?; let d = DiskInfo { vol, parent: dir_cluster, name: e.name.clone(), is_dir: e.is_dir, size: e.size, cluster: e.cluster, }; if e.is_dir { dstack.push((vol, 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(i.vol, 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.vol, 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.vol, i.parent, &i.name, buf, pos), NodeData::Dir(_) => Err(()), } } /// Handle of `crash.log` on the crash-log volume, if it exists. The /// crash-log volume is the one labeled CRASHLOG (a dedicated stick that /// survives reboot writes), else the first mounted volume. Resolves the /// FAT path (`/disk//crash.log`) because Disk nodes hold no cached /// children. pub fn read_crash_log() -> Option { let disk_dir = lookup(0, "disk")?; let children = match &nodes()[disk_dir].data { NodeData::Dir(v) => v.clone(), _ => return None, }; // Prefer a disk node whose name starts with "CRASHLOG-". let mut first = None; for &c in &children { if nodes()[c].removed || !matches!(nodes()[c].data, NodeData::Disk(_)) { continue; } let name = &nodes()[c].name; let label = name.split_once('-').map(|(l, _)| l).unwrap_or(name); if label.eq_ignore_ascii_case("CRASHLOG") { return resolve(&alloc::format!("/disk/{}/crash.log", name)); } if first.is_none() { first = Some(c); } } first.and_then(|disk| { let name = nodes()[disk].name.clone(); resolve(&alloc::format!("/disk/{}/crash.log", name)) }) } /// Persist a crash report to the mounted FAT volume so it survives a reboot /// (real hardware has no serial console). Best-effort: any failure is /// ignored — the caller (fault handler) must never fault here; the /// in-dump ISR guard catches that case and halts. /// /// Goes straight at the FAT layer (the crash-log volume + its root /// cluster) rather than through the fs node table: the fault may have /// interrupted the fs mid-operation, and a stale DiskInfo/ACTIVE_VOL /// would make node-based writes fail with "no volume at slot N". The /// crash-log volume is the one labeled CRASHLOG (a dedicated stick that /// survives reboot writes), else slot 0 (first mounted) as before. pub fn write_crash_log(lines: &[&str]) { // Assemble the report on the stack (heap-free — a corrupted heap is // exactly the situation that produces crash reports). let mut text = [0u8; 640]; let mut n = 0usize; for line in lines { for &b in line.as_bytes() { if n < text.len() { text[n] = b; n += 1; } } if n < text.len() { text[n] = b'\n'; n += 1; } } let Some(vol) = crate::fat::crash_vol() else { crate::serial::write_raw("crashlog: no volume\r\n"); return; }; // Trace the target so a test can verify the right stick got the log. let label = crate::fat::volume_label(vol).unwrap_or("?"); let mut tb = [0u8; 48]; let tn = { let mut w = crate::strbuf::FmtBuf::new(&mut tb); let _ = core::fmt::write(&mut w, core::format_args!("crashlog: vol {} ({})\r\n", vol, label)); w.len() }; crate::serial::write_raw(core::str::from_utf8(&tb[..tn]).unwrap_or("?")); let root = crate::fat::root_cluster(vol); if crate::fat::create(vol, root, "crash.log", false).is_err() && crate::fat::find(vol, root, "crash.log").is_err() { crate::serial::write_raw("crashlog: create failed\r\n"); return; } if crate::fat::set_len(vol, root, "crash.log", 0).is_err() { crate::serial::write_raw("crashlog: set_len failed\r\n"); return; } match crate::fat::write_file(vol, root, "crash.log", &text[..n], 0) { Ok(_) => crate::serial::write_raw("crashlog: wrote\r\n"), Err(_) => crate::serial::write_raw("crashlog: write failed\r\n"), } } /// 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.vol, 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<(usize, u32)> = match &nodes()[parent].data { NodeData::Disk(i) => { if !i.is_dir { return Err(()); } Some((i.vol, i.cluster)) } _ => None, }; if let Some((vol, p)) = disk_parent { if crate::fat::find(vol, p, name).is_ok() { return Err(()); } crate::fat::create(vol, p, name, kind == FileType::Dir)?; let e = crate::fat::find(vol, p, name)?; return Ok(materialize_disk(DiskInfo { vol, 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 vol = i.vol; let p = i.parent; let n = i.name.clone(); return crate::fat::remove(vol, 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::Klog => crate::klog::dump(), CharDevice::Mouse => mouse_text(), CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), CharDevice::Zero => String::new(), } } /// Render `/dev/mouse`: the current cursor state (position + buttons). /// The cursor task owns the PS/2 ring, so this is a position device — /// the GUI-correct semantic — rather than an event drain. fn mouse_text() -> String { if !crate::ps2::mouse_present() { return String::from("mouse: no PS/2 mouse\n"); } let (x, y) = crate::gop::cursor_pos(); let b = crate::gop::cursor_buttons(); alloc::format!( "x={} y={} buttons={}{}{}\n", x, y, if b & 1 != 0 { 'L' } else { '-' }, if b & 2 != 0 { 'R' } else { '-' }, if b & 4 != 0 { 'M' } else { '-' } ) } // --- 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 }