From 6767d96e8ed5049af7b551b00d6320fbfb5dcd05 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 16:31:34 +0200 Subject: vfs: everything is a file — dirs, ramfs, character devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fs.rs: node-table VFS (root /dev /tmp /etc). Regular files are in-memory ramfs; char devices regenerate content on every read so 'cat /dev/rtc' shows the live time. Nodes are stable handles with tombstone removal; . and .. path resolution. - /dev devices: mem (raw physical RAM, offset = address), rtc, pci, version, meminfo, null, zero, console (write prints to screen+serial), serial. /etc/motd static file. - shell: ls, cat, hexdump (hexdump -C style), mkdir, touch, rm; echo gains '>' overwrite and '>>' append redirection (append bug fixed: first '>' in '>>' was parsed as overwrite) - boot: fs init + self-test (create/read/write/remove) before the shell - QEMU verified: file lifecycle, redirection, console/null writes, mkdir/touch/rm, hexdump /dev/mem and /tmp/x byte-exact --- rsnix/src/fs.rs | 501 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ rsnix/src/lib.rs | 1 + 2 files changed, 502 insertions(+) create mode 100644 rsnix/src/fs.rs (limited to 'rsnix/src') diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs new file mode 100644 index 0000000..56820bb --- /dev/null +++ b/rsnix/src/fs.rs @@ -0,0 +1,501 @@ +// --------------------------------------------------------------------------- +// 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 +} diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index f84fa73..bb0a3eb 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; pub mod font; +pub mod fs; pub mod gop; pub mod interrupts; pub mod memory; -- cgit v1.2.3