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 --- shell/src/lib.rs | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 2 deletions(-) (limited to 'shell/src') diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 500eaae..7e1439e 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -2,6 +2,8 @@ extern crate alloc; +use alloc::string::ToString; + /// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. use rsnix::ps2::{self, KeyEvent}; use rsnix::{gop_print, gop_println, serial_print, serial_println}; @@ -52,6 +54,12 @@ pub fn run() -> ! { "stress" => cmd_stress(args), "date" => cmd_date(), "pci" => cmd_pci(), + "ls" => cmd_ls(args), + "cat" => cmd_cat(args), + "hexdump" => cmd_hexdump(args), + "mkdir" => cmd_mkdir(args), + "touch" => cmd_touch(args), + "rm" => cmd_rm(args), _ => { gop_println!("unknown command: {}", cmd); serial_println!("unknown command: {}", cmd); @@ -128,15 +136,220 @@ fn cmd_help() { gop_println!(" stress — stress-test heap/page allocators"); gop_println!(" date — show current date/time (RTC)"); gop_println!(" pci — list PCI devices"); + gop_println!(" ls — list a directory"); + gop_println!(" cat — print files (e.g. cat /dev/rtc)"); + gop_println!(" hexdump — hex-dump a file (e.g. hexdump /dev/mem)"); + gop_println!(" mkdir — create a directory"); + gop_println!(" touch — create an empty file"); + gop_println!(" rm — remove a file or empty dir"); + gop_println!(" echo > /dev/null, > file, >> file"); gop_println!(" halt — halt the system (hlt loop)"); gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); gop_println!(" exit — alias for poweroff"); serial_println!("available commands:"); - serial_println!(" help, echo, clear, hello, info, mem, mmap, stress, date, pci, halt, poweroff, exit"); + serial_println!( + " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, halt, poweroff, exit" + ); } fn cmd_echo(args: &str) { + // Redirection: `text > path` (overwrite) or `text >> path` (append). + if let Some(idx) = args.find('>') { + let append = args[idx..].starts_with(">>"); + let text = &args[..idx]; + let path = args[idx + 1 + append as usize..].trim(); + let path = path.split_whitespace().next().unwrap_or(""); + if !path.is_empty() { + let content = alloc::format!("{}\n", text.trim_end()); + let result = rsnix::fs::ensure_file(path).and_then(|h| { + if append { + rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)) + .map(|_| ()) + } else { + let _ = rsnix::fs::set_len(h, 0); + rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ()) + } + }); + match result { + Ok(()) => {} + Err(()) => { + gop_println!("echo: cannot write '{}'", path); + serial_println!("echo: cannot write '{}'", path); + } + } + return; + } + } gop_println!("{}", args); + serial_println!("{}", args); +} + +fn cmd_ls(args: &str) { + let path = args.split_whitespace().next().unwrap_or("/"); + let mut out = alloc::string::String::new(); + match rsnix::fs::resolve(path) { + None => { + out.push_str(&alloc::format!("ls: {}: no such file or directory\n", path)); + } + Some(h) => { + let list = if rsnix::fs::kind(h) == rsnix::fs::FileType::Dir { + rsnix::fs::children(h) + } else { + alloc::vec![( + path.rsplit('/').next().unwrap_or(path).to_string(), + rsnix::fs::kind(h), + rsnix::fs::size(h) + )] + }; + for (name, kind, size) in list { + match kind { + rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)), + rsnix::fs::FileType::Char => { + out.push_str(&alloc::format!("{}@\n", name)) + } + rsnix::fs::FileType::Regular => { + out.push_str(&alloc::format!("{} {} B\n", name, size)) + } + } + } + } + } + gop_print!("{}", out); + serial_print!("{}", out); +} + +fn cmd_cat(args: &str) { + let paths: alloc::vec::Vec<&str> = args.split_whitespace().collect(); + if paths.is_empty() { + gop_println!("usage: cat [path...]"); + serial_println!("usage: cat [path...]"); + return; + } + let mut out = alloc::string::String::new(); + let mut buf = [0u8; 512]; + for p in paths { + match rsnix::fs::resolve(p) { + None => out.push_str(&alloc::format!("cat: {}: no such file\n", p)), + Some(h) => { + let mut pos = 0u64; + loop { + match rsnix::fs::read(h, &mut buf, pos) { + Ok(0) | Err(_) => break, + Ok(n) => { + out.push_str(core::str::from_utf8(&buf[..n]).unwrap_or("?")); + pos += n as u64; + } + } + } + } + } + } + gop_print!("{}", out); + serial_print!("{}", out); +} + +fn cmd_hexdump(args: &str) { + let path = args.split_whitespace().next().unwrap_or(""); + if path.is_empty() { + gop_println!("usage: hexdump "); + serial_println!("usage: hexdump "); + return; + } + let h = match rsnix::fs::resolve(path) { + Some(h) => h, + None => { + gop_println!("hexdump: {}: no such file", path); + serial_println!("hexdump: {}: no such file", path); + return; + } + }; + let mut out = alloc::string::String::new(); + let mut buf = [0u8; 512]; + let mut pos = 0u64; + use core::fmt::Write as _; + 'outer: loop { + let n = match rsnix::fs::read(h, &mut buf, pos) { + Ok(0) | Err(_) => break, + Ok(n) => n, + }; + for i in (0..n).step_by(16) { + let _ = core::write!(out, "{:08x} ", pos + i as u64); + for j in 0..16 { + if i + j < n { + let _ = core::write!(out, "{:02x} ", buf[i + j]); + } else { + out.push_str(" "); + } + } + out.push_str(" |"); + for j in 0..16 { + if i + j >= n { + break; + } + let b = buf[i + j]; + out.push(if b.is_ascii_graphic() || b == b' ' { + b as char + } else { + '.' + }); + } + out.push_str("|\n"); + if i + 16 >= n { + pos += n as u64; + continue 'outer; + } + } + } + gop_print!("{}", out); + serial_print!("{}", out); +} + +fn cmd_mkdir(args: &str) { + let path = args.split_whitespace().next().unwrap_or(""); + if path.is_empty() { + gop_println!("usage: mkdir "); + serial_println!("usage: mkdir "); + return; + } + match rsnix::fs::mkdir(path) { + Ok(()) => {} + Err(()) => { + gop_println!("mkdir: cannot create directory '{}'", path); + serial_println!("mkdir: cannot create directory '{}'", path); + } + } +} + +fn cmd_touch(args: &str) { + let path = args.split_whitespace().next().unwrap_or(""); + if path.is_empty() { + gop_println!("usage: touch "); + serial_println!("usage: touch "); + return; + } + match rsnix::fs::touch(path) { + Ok(()) => {} + Err(()) => { + gop_println!("touch: cannot create '{}'", path); + serial_println!("touch: cannot create '{}'", path); + } + } +} + +fn cmd_rm(args: &str) { + let path = args.split_whitespace().next().unwrap_or(""); + if path.is_empty() { + gop_println!("usage: rm "); + serial_println!("usage: rm "); + return; + } + match rsnix::fs::remove_path(path) { + Ok(()) => {} + Err(()) => { + gop_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); + serial_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); + } + } } fn cmd_clear() { @@ -372,7 +585,12 @@ fn cmd_date() { let t = rsnix::rtc::now(); let line = alloc::format!( "{}-{:02}-{:02} {:02}:{:02}:{:02}", - t.year, t.month, t.day, t.hour, t.minute, t.second + t.year, + t.month, + t.day, + t.hour, + t.minute, + t.second ); gop_println!("{}", line); serial_println!("{}", line); -- cgit v1.2.3