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 --- CONTEXT.md | 6 + rsdos/src/main.rs | 8 + rsnix/src/fs.rs | 501 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ rsnix/src/lib.rs | 1 + shell/src/lib.rs | 222 +++++++++++++++++++++++- 5 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 rsnix/src/fs.rs diff --git a/CONTEXT.md b/CONTEXT.md index 8ee7f6c..5b76eaa 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -98,6 +98,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: - `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback - `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization - `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration +- `fs.rs` — VFS: dirs, ramfs files, char devices (`/dev/mem`, `rtc`, `pci`, + `version`, `meminfo`, `null`, `zero`, `console`, `serial`; `/tmp` ramfs; + `/etc/motd`) - `gop.rs` — framebuffer access and text rendering - `font.rs` — 8×16 bitmap font data - `ps2.rs` — PS/2 keyboard driver (polling) @@ -124,6 +127,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: (ACPI S5; `halt` keeps the plain hlt loop) ✅ **RTC** — CMOS clock read at boot and via `date` (BCD/12h handled) ✅ **PCI** — port-based config access, full bus enumeration via `pci` +✅ **VFS** — “everything is a file”: dirs, ramfs, char devices + (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices + regenerate on read, `/dev/mem` is raw physical RAM) ## Target audience diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index c35d88f..8e49937 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs @@ -154,6 +154,14 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT let devices = rsnix::pci::probe(); rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); + let (dirs, files) = rsnix::fs::init(); + rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files); + if rsnix::fs::self_test() { + rsnix::serial_println!("fs: self-test OK (create/read/write/remove)"); + } else { + rsnix::serial_println!("fs: self-test FAILED"); + } + rsnix::serial_println!("boot: entering shell"); shell::run(); } 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; 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