diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 16:31:34 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 16:31:34 +0200 |
| commit | 6767d96e8ed5049af7b551b00d6320fbfb5dcd05 (patch) | |
| tree | 118e6b52d75684e52f96b923e773a1100c02c5cb | |
| parent | rtc + pci: CMOS clock with date command, PCI bus enumeration (diff) | |
| download | rs-dos-6767d96e8ed5049af7b551b00d6320fbfb5dcd05.tar.gz rs-dos-6767d96e8ed5049af7b551b00d6320fbfb5dcd05.zip | |
vfs: everything is a file — dirs, ramfs, character devices
- 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
| -rw-r--r-- | CONTEXT.md | 6 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 8 | ||||
| -rw-r--r-- | rsnix/src/fs.rs | 501 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 1 | ||||
| -rw-r--r-- | shell/src/lib.rs | 222 |
5 files changed, 736 insertions, 2 deletions
| @@ -98,6 +98,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: | |||
| 98 | - `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback | 98 | - `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback |
| 99 | - `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization | 99 | - `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization |
| 100 | - `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration | 100 | - `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration |
| 101 | - `fs.rs` — VFS: dirs, ramfs files, char devices (`/dev/mem`, `rtc`, `pci`, | ||
| 102 | `version`, `meminfo`, `null`, `zero`, `console`, `serial`; `/tmp` ramfs; | ||
| 103 | `/etc/motd`) | ||
| 101 | - `gop.rs` — framebuffer access and text rendering | 104 | - `gop.rs` — framebuffer access and text rendering |
| 102 | - `font.rs` — 8×16 bitmap font data | 105 | - `font.rs` — 8×16 bitmap font data |
| 103 | - `ps2.rs` — PS/2 keyboard driver (polling) | 106 | - `ps2.rs` — PS/2 keyboard driver (polling) |
| @@ -124,6 +127,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: | |||
| 124 | (ACPI S5; `halt` keeps the plain hlt loop) | 127 | (ACPI S5; `halt` keeps the plain hlt loop) |
| 125 | ✅ **RTC** — CMOS clock read at boot and via `date` (BCD/12h handled) | 128 | ✅ **RTC** — CMOS clock read at boot and via `date` (BCD/12h handled) |
| 126 | ✅ **PCI** — port-based config access, full bus enumeration via `pci` | 129 | ✅ **PCI** — port-based config access, full bus enumeration via `pci` |
| 130 | ✅ **VFS** — “everything is a file”: dirs, ramfs, char devices | ||
| 131 | (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices | ||
| 132 | regenerate on read, `/dev/mem` is raw physical RAM) | ||
| 127 | 133 | ||
| 128 | ## Target audience | 134 | ## Target audience |
| 129 | 135 | ||
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 | |||
| 154 | let devices = rsnix::pci::probe(); | 154 | let devices = rsnix::pci::probe(); |
| 155 | rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); | 155 | rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); |
| 156 | 156 | ||
| 157 | let (dirs, files) = rsnix::fs::init(); | ||
| 158 | rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files); | ||
| 159 | if rsnix::fs::self_test() { | ||
| 160 | rsnix::serial_println!("fs: self-test OK (create/read/write/remove)"); | ||
| 161 | } else { | ||
| 162 | rsnix::serial_println!("fs: self-test FAILED"); | ||
| 163 | } | ||
| 164 | |||
| 157 | rsnix::serial_println!("boot: entering shell"); | 165 | rsnix::serial_println!("boot: entering shell"); |
| 158 | shell::run(); | 166 | shell::run(); |
| 159 | } | 167 | } |
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 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // VFS — "everything is a file". | ||
| 3 | // | ||
| 4 | // A tiny hierarchical filesystem namespace: directories, in-memory regular | ||
| 5 | // files (ramfs, writable via the shell), and character devices under /dev. | ||
| 6 | // Device reads re-generate their content on every access, so | ||
| 7 | // `cat /dev/rtc` shows the *current* time and `cat /dev/mem` hexdumps live | ||
| 8 | // physical memory. Writes to /dev/console print to the screen, writes to | ||
| 9 | // /dev/null disappear. | ||
| 10 | // | ||
| 11 | // Nodes live in a flat table; handles are stable indices (removal leaves a | ||
| 12 | // tombstone). Single-threaded, interrupt-safe because no ISR touches the | ||
| 13 | // table. A real disk filesystem can mount under this same namespace later. | ||
| 14 | // --------------------------------------------------------------------------- | ||
| 15 | |||
| 16 | extern crate alloc; | ||
| 17 | |||
| 18 | use alloc::boxed::Box; | ||
| 19 | use alloc::string::String; | ||
| 20 | use alloc::vec::Vec; | ||
| 21 | use crate::gop; | ||
| 22 | use crate::memory; | ||
| 23 | use crate::pci; | ||
| 24 | use crate::rtc; | ||
| 25 | use crate::serial; | ||
| 26 | |||
| 27 | /// /dev/mem covers the low physical-memory window (identity-mapped RAM). | ||
| 28 | pub const MEM_WINDOW: u64 = 64 * 1024; | ||
| 29 | |||
| 30 | #[derive(Clone, Copy, PartialEq)] | ||
| 31 | pub enum FileType { | ||
| 32 | Regular, | ||
| 33 | Char, | ||
| 34 | Dir, | ||
| 35 | } | ||
| 36 | |||
| 37 | #[derive(Clone, Copy)] | ||
| 38 | enum CharDevice { | ||
| 39 | Mem, | ||
| 40 | Rtc, | ||
| 41 | Pci, | ||
| 42 | Version, | ||
| 43 | Meminfo, | ||
| 44 | Null, | ||
| 45 | Zero, | ||
| 46 | Console, | ||
| 47 | Serial, | ||
| 48 | } | ||
| 49 | |||
| 50 | enum NodeData { | ||
| 51 | Regular(Vec<u8>), | ||
| 52 | Dir(Vec<usize>), | ||
| 53 | Char(CharDevice), | ||
| 54 | } | ||
| 55 | |||
| 56 | struct Node { | ||
| 57 | name: String, | ||
| 58 | kind: FileType, | ||
| 59 | parent: Option<usize>, | ||
| 60 | data: NodeData, | ||
| 61 | removed: bool, | ||
| 62 | } | ||
| 63 | |||
| 64 | /// Node table; root is always handle 0. Boxed so the static stays a raw | ||
| 65 | /// pointer (matches the rest of the crate's style). | ||
| 66 | static mut NODES: *mut Vec<Node> = core::ptr::null_mut(); | ||
| 67 | |||
| 68 | fn nodes() -> &'static mut Vec<Node> { | ||
| 69 | unsafe { &mut *NODES } | ||
| 70 | } | ||
| 71 | |||
| 72 | fn add(name: &str, parent: Option<usize>, kind: FileType, data: NodeData) -> usize { | ||
| 73 | let h = nodes().len(); | ||
| 74 | nodes().push(Node { | ||
| 75 | name: name.into(), | ||
| 76 | kind, | ||
| 77 | parent, | ||
| 78 | data, | ||
| 79 | removed: false, | ||
| 80 | }); | ||
| 81 | if let Some(p) = parent { | ||
| 82 | if let NodeData::Dir(v) = &mut nodes()[p].data { | ||
| 83 | v.push(h); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | h | ||
| 87 | } | ||
| 88 | |||
| 89 | /// Build the tree and report (dirs, files). | ||
| 90 | pub fn init() -> (usize, usize) { | ||
| 91 | unsafe { | ||
| 92 | NODES = Box::into_raw(Box::new(Vec::new())); | ||
| 93 | } | ||
| 94 | let root = add("/", None, FileType::Dir, NodeData::Dir(Vec::new())); | ||
| 95 | let dev = add("dev", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); | ||
| 96 | let _tmp = add("tmp", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); | ||
| 97 | let etc = add("etc", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); | ||
| 98 | |||
| 99 | add("mem", Some(dev), FileType::Char, NodeData::Char(CharDevice::Mem)); | ||
| 100 | add("rtc", Some(dev), FileType::Char, NodeData::Char(CharDevice::Rtc)); | ||
| 101 | add("pci", Some(dev), FileType::Char, NodeData::Char(CharDevice::Pci)); | ||
| 102 | add( | ||
| 103 | "version", | ||
| 104 | Some(dev), | ||
| 105 | FileType::Char, | ||
| 106 | NodeData::Char(CharDevice::Version), | ||
| 107 | ); | ||
| 108 | add( | ||
| 109 | "meminfo", | ||
| 110 | Some(dev), | ||
| 111 | FileType::Char, | ||
| 112 | NodeData::Char(CharDevice::Meminfo), | ||
| 113 | ); | ||
| 114 | add( | ||
| 115 | "null", | ||
| 116 | Some(dev), | ||
| 117 | FileType::Char, | ||
| 118 | NodeData::Char(CharDevice::Null), | ||
| 119 | ); | ||
| 120 | add( | ||
| 121 | "zero", | ||
| 122 | Some(dev), | ||
| 123 | FileType::Char, | ||
| 124 | NodeData::Char(CharDevice::Zero), | ||
| 125 | ); | ||
| 126 | add( | ||
| 127 | "console", | ||
| 128 | Some(dev), | ||
| 129 | FileType::Char, | ||
| 130 | NodeData::Char(CharDevice::Console), | ||
| 131 | ); | ||
| 132 | add( | ||
| 133 | "serial", | ||
| 134 | Some(dev), | ||
| 135 | FileType::Char, | ||
| 136 | NodeData::Char(CharDevice::Serial), | ||
| 137 | ); | ||
| 138 | |||
| 139 | add( | ||
| 140 | "motd", | ||
| 141 | Some(etc), | ||
| 142 | FileType::Regular, | ||
| 143 | NodeData::Regular( | ||
| 144 | b"Welcome to rs-dos!\nEverything is a file. Try: cat /dev/rtc, cat /dev/pci, cat /dev/mem\n" | ||
| 145 | .to_vec(), | ||
| 146 | ), | ||
| 147 | ); | ||
| 148 | |||
| 149 | let dirs = nodes() | ||
| 150 | .iter() | ||
| 151 | .filter(|n| n.kind == FileType::Dir) | ||
| 152 | .count(); | ||
| 153 | let files = nodes().len() - dirs; | ||
| 154 | (dirs, files) | ||
| 155 | } | ||
| 156 | |||
| 157 | fn lookup(dir: usize, name: &str) -> Option<usize> { | ||
| 158 | let children = match &nodes()[dir].data { | ||
| 159 | NodeData::Dir(v) => v.clone(), | ||
| 160 | _ => return None, | ||
| 161 | }; | ||
| 162 | for c in children { | ||
| 163 | let n = &nodes()[c]; | ||
| 164 | if !n.removed && n.name == name { | ||
| 165 | return Some(c); | ||
| 166 | } | ||
| 167 | } | ||
| 168 | None | ||
| 169 | } | ||
| 170 | |||
| 171 | /// Resolve an absolute path (supports `.` and `..`). | ||
| 172 | pub fn resolve(path: &str) -> Option<usize> { | ||
| 173 | if nodes().is_empty() { | ||
| 174 | return None; | ||
| 175 | } | ||
| 176 | let mut cur = 0usize; // root | ||
| 177 | for seg in path.split('/') { | ||
| 178 | match seg { | ||
| 179 | "" | "." => {} | ||
| 180 | ".." => { | ||
| 181 | if let Some(p) = nodes()[cur].parent { | ||
| 182 | cur = p; | ||
| 183 | } | ||
| 184 | } | ||
| 185 | name => { | ||
| 186 | cur = lookup(cur, name)?; | ||
| 187 | } | ||
| 188 | } | ||
| 189 | } | ||
| 190 | Some(cur) | ||
| 191 | } | ||
| 192 | |||
| 193 | pub fn kind(h: usize) -> FileType { | ||
| 194 | nodes()[h].kind | ||
| 195 | } | ||
| 196 | |||
| 197 | pub fn size(h: usize) -> u64 { | ||
| 198 | match &nodes()[h].data { | ||
| 199 | NodeData::Regular(v) => v.len() as u64, | ||
| 200 | _ => 0, | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | /// Entries of a directory as (name, kind, size) — tombstoned nodes skipped. | ||
| 205 | pub fn children(h: usize) -> Vec<(String, FileType, u64)> { | ||
| 206 | let kids = match &nodes()[h].data { | ||
| 207 | NodeData::Dir(v) => v.clone(), | ||
| 208 | _ => return Vec::new(), | ||
| 209 | }; | ||
| 210 | let mut out = Vec::new(); | ||
| 211 | for c in kids { | ||
| 212 | let n = &nodes()[c]; | ||
| 213 | if n.removed { | ||
| 214 | continue; | ||
| 215 | } | ||
| 216 | let sz = match &n.data { | ||
| 217 | NodeData::Regular(v) => v.len() as u64, | ||
| 218 | _ => 0, | ||
| 219 | }; | ||
| 220 | out.push((n.name.clone(), n.kind, sz)); | ||
| 221 | } | ||
| 222 | out | ||
| 223 | } | ||
| 224 | |||
| 225 | pub fn read(h: usize, buf: &mut [u8], pos: u64) -> Result<usize, ()> { | ||
| 226 | if buf.is_empty() { | ||
| 227 | return Ok(0); | ||
| 228 | } | ||
| 229 | match &mut nodes()[h].data { | ||
| 230 | NodeData::Regular(v) => { | ||
| 231 | let p = pos as usize; | ||
| 232 | if p >= v.len() { | ||
| 233 | return Ok(0); | ||
| 234 | } | ||
| 235 | let n = buf.len().min(v.len() - p); | ||
| 236 | buf[..n].copy_from_slice(&v[p..p + n]); | ||
| 237 | Ok(n) | ||
| 238 | } | ||
| 239 | NodeData::Char(CharDevice::Mem) => { | ||
| 240 | // /dev/mem is a raw byte window: file offset = physical address. | ||
| 241 | let p = pos as usize; | ||
| 242 | if p >= MEM_WINDOW as usize { | ||
| 243 | return Ok(0); | ||
| 244 | } | ||
| 245 | let n = buf.len().min(MEM_WINDOW as usize - p); | ||
| 246 | for i in 0..n { | ||
| 247 | buf[i] = unsafe { core::ptr::read_volatile((p + i) as *const u8) }; | ||
| 248 | } | ||
| 249 | Ok(n) | ||
| 250 | } | ||
| 251 | NodeData::Char(CharDevice::Zero) => { | ||
| 252 | buf.fill(0); | ||
| 253 | Ok(buf.len()) | ||
| 254 | } | ||
| 255 | NodeData::Char(dev) => { | ||
| 256 | let text = dev_text(*dev, pos); | ||
| 257 | let bytes = text.as_bytes(); | ||
| 258 | let p = pos as usize; | ||
| 259 | if p >= bytes.len() { | ||
| 260 | return Ok(0); | ||
| 261 | } | ||
| 262 | let n = buf.len().min(bytes.len() - p); | ||
| 263 | buf[..n].copy_from_slice(&bytes[p..p + n]); | ||
| 264 | Ok(n) | ||
| 265 | } | ||
| 266 | NodeData::Dir(_) => Err(()), | ||
| 267 | } | ||
| 268 | } | ||
| 269 | |||
| 270 | pub fn write(h: usize, buf: &[u8], pos: u64) -> Result<usize, ()> { | ||
| 271 | match &mut nodes()[h].data { | ||
| 272 | NodeData::Regular(v) => { | ||
| 273 | let p = pos as usize; | ||
| 274 | if p + buf.len() > v.len() { | ||
| 275 | v.resize(p + buf.len(), 0); | ||
| 276 | } | ||
| 277 | v[p..p + buf.len()].copy_from_slice(buf); | ||
| 278 | Ok(buf.len()) | ||
| 279 | } | ||
| 280 | NodeData::Char(dev) => match dev { | ||
| 281 | CharDevice::Null | CharDevice::Zero => Ok(buf.len()), | ||
| 282 | CharDevice::Console => { | ||
| 283 | for &b in buf { | ||
| 284 | if b == b'\n' { | ||
| 285 | gop::put_char('\n'); | ||
| 286 | serial::put_byte(b'\n'); | ||
| 287 | } else if b.is_ascii() { | ||
| 288 | gop::put_char(b as char); | ||
| 289 | serial::put_byte(b); | ||
| 290 | } | ||
| 291 | } | ||
| 292 | Ok(buf.len()) | ||
| 293 | } | ||
| 294 | CharDevice::Serial => { | ||
| 295 | for &b in buf { | ||
| 296 | serial::put_byte(b); | ||
| 297 | } | ||
| 298 | Ok(buf.len()) | ||
| 299 | } | ||
| 300 | _ => Err(()), // read-only devices | ||
| 301 | }, | ||
| 302 | NodeData::Dir(_) => Err(()), | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | /// Truncate/resize a regular file. | ||
| 307 | pub fn set_len(h: usize, len: u64) -> Result<(), ()> { | ||
| 308 | match &mut nodes()[h].data { | ||
| 309 | NodeData::Regular(v) => { | ||
| 310 | v.resize(len as usize, 0); | ||
| 311 | Ok(()) | ||
| 312 | } | ||
| 313 | _ => Err(()), | ||
| 314 | } | ||
| 315 | } | ||
| 316 | |||
| 317 | /// Create a Regular or Dir child. Fails if the name exists or is invalid. | ||
| 318 | pub fn create(parent: usize, name: &str, kind: FileType) -> Result<usize, ()> { | ||
| 319 | if name.is_empty() || name.contains('/') || kind == FileType::Char { | ||
| 320 | return Err(()); | ||
| 321 | } | ||
| 322 | if lookup(parent, name).is_some() { | ||
| 323 | return Err(()); | ||
| 324 | } | ||
| 325 | let data = match kind { | ||
| 326 | FileType::Regular => NodeData::Regular(Vec::new()), | ||
| 327 | FileType::Dir => NodeData::Dir(Vec::new()), | ||
| 328 | FileType::Char => return Err(()), | ||
| 329 | }; | ||
| 330 | Ok(add(name, Some(parent), kind, data)) | ||
| 331 | } | ||
| 332 | |||
| 333 | /// Remove a regular file or an empty directory (tombstone). | ||
| 334 | pub fn remove(h: usize) -> Result<(), ()> { | ||
| 335 | let parent = match nodes()[h].parent { | ||
| 336 | Some(p) => p, | ||
| 337 | None => return Err(()), | ||
| 338 | }; | ||
| 339 | let empty = match &nodes()[h].data { | ||
| 340 | NodeData::Dir(v) => v.is_empty(), | ||
| 341 | _ => true, | ||
| 342 | }; | ||
| 343 | if !empty { | ||
| 344 | return Err(()); | ||
| 345 | } | ||
| 346 | if let NodeData::Dir(v) = &mut nodes()[parent].data { | ||
| 347 | v.retain(|&c| c != h); | ||
| 348 | } | ||
| 349 | let n = &mut nodes()[h]; | ||
| 350 | n.removed = true; | ||
| 351 | n.parent = None; | ||
| 352 | Ok(()) | ||
| 353 | } | ||
| 354 | |||
| 355 | // --- path helpers --------------------------------------------------------- | ||
| 356 | |||
| 357 | /// Split `/a/b/c` into (handle of `/a/b`, "c"). | ||
| 358 | fn split_parent_name(path: &str) -> Result<(usize, String), ()> { | ||
| 359 | let path = path.trim_end_matches('/'); | ||
| 360 | if path.is_empty() { | ||
| 361 | return Err(()); | ||
| 362 | } | ||
| 363 | let (parent_path, name) = match path.rfind('/') { | ||
| 364 | Some(i) => (&path[..i], &path[i + 1..]), | ||
| 365 | None => ("/", path), | ||
| 366 | }; | ||
| 367 | if name.is_empty() { | ||
| 368 | return Err(()); | ||
| 369 | } | ||
| 370 | Some((resolve(parent_path).ok_or(())?, name.into())).ok_or(()) | ||
| 371 | } | ||
| 372 | |||
| 373 | pub fn mkdir(path: &str) -> Result<(), ()> { | ||
| 374 | let (p, n) = split_parent_name(path)?; | ||
| 375 | create(p, &n, FileType::Dir).map(|_| ()) | ||
| 376 | } | ||
| 377 | |||
| 378 | pub fn touch(path: &str) -> Result<(), ()> { | ||
| 379 | if resolve(path).is_some() { | ||
| 380 | return Ok(()); | ||
| 381 | } | ||
| 382 | let (p, n) = split_parent_name(path)?; | ||
| 383 | create(p, &n, FileType::Regular).map(|_| ()) | ||
| 384 | } | ||
| 385 | |||
| 386 | /// Resolve a path, creating a regular file if it doesn't exist yet. | ||
| 387 | pub fn ensure_file(path: &str) -> Result<usize, ()> { | ||
| 388 | if let Some(h) = resolve(path) { | ||
| 389 | if kind(h) == FileType::Dir { | ||
| 390 | Err(()) | ||
| 391 | } else { | ||
| 392 | Ok(h) | ||
| 393 | } | ||
| 394 | } else { | ||
| 395 | touch(path)?; | ||
| 396 | resolve(path).ok_or(()) | ||
| 397 | } | ||
| 398 | } | ||
| 399 | |||
| 400 | pub fn remove_path(path: &str) -> Result<(), ()> { | ||
| 401 | let h = resolve(path).ok_or(())?; | ||
| 402 | remove(h) | ||
| 403 | } | ||
| 404 | |||
| 405 | // --- character devices ---------------------------------------------------- | ||
| 406 | |||
| 407 | fn version_text() -> String { | ||
| 408 | alloc::format!( | ||
| 409 | "rsnix v{} — TempleOS-inspired toy kernel\nUEFI x86_64, identity paging, free-list heap, bitmap pages\n", | ||
| 410 | env!("CARGO_PKG_VERSION") | ||
| 411 | ) | ||
| 412 | } | ||
| 413 | |||
| 414 | fn meminfo_text() -> String { | ||
| 415 | let h = memory::heap_stats(); | ||
| 416 | let p = memory::page_stats(); | ||
| 417 | let (map, key) = memory::get_map(); | ||
| 418 | let mut usable_kb = 0u64; | ||
| 419 | for d in map { | ||
| 420 | if d.kind == 7 { | ||
| 421 | usable_kb += d.number_of_pages * 4; | ||
| 422 | } | ||
| 423 | } | ||
| 424 | alloc::format!( | ||
| 425 | "heap: {} KB capacity, {} KB free in {} block(s)\npages: {} free / {} used, {} MB span\nmem: {} entries (key 0x{:x}), {} MB usable\n", | ||
| 426 | h.heap_size >> 10, | ||
| 427 | h.free_bytes >> 10, | ||
| 428 | h.free_blocks, | ||
| 429 | p.free_pages, | ||
| 430 | p.used_pages, | ||
| 431 | p.span_bytes >> 20, | ||
| 432 | map.len(), | ||
| 433 | key, | ||
| 434 | usable_kb >> 10, | ||
| 435 | ) | ||
| 436 | } | ||
| 437 | |||
| 438 | fn pci_listing() -> String { | ||
| 439 | let mut out = String::new(); | ||
| 440 | let devices = pci::probe(); | ||
| 441 | out += &alloc::format!("{} device(s)\n", devices.len()); | ||
| 442 | for d in &devices { | ||
| 443 | out += &alloc::format!( | ||
| 444 | "{:02x}:{:02x}.{} {:<16} {:04x}:{:04x}\n", | ||
| 445 | d.bus, | ||
| 446 | d.device, | ||
| 447 | d.func, | ||
| 448 | pci::class_name(d.class, d.subclass), | ||
| 449 | d.vendor, | ||
| 450 | d.device_id | ||
| 451 | ); | ||
| 452 | } | ||
| 453 | out | ||
| 454 | } | ||
| 455 | |||
| 456 | fn dev_text(dev: CharDevice, _pos: u64) -> String { | ||
| 457 | match dev { | ||
| 458 | CharDevice::Mem => String::new(), // raw bytes, special-cased in read() | ||
| 459 | CharDevice::Rtc => { | ||
| 460 | let t = rtc::now(); | ||
| 461 | alloc::format!( | ||
| 462 | "{}-{:02}-{:02} {:02}:{:02}:{:02}\n", | ||
| 463 | t.year, | ||
| 464 | t.month, | ||
| 465 | t.day, | ||
| 466 | t.hour, | ||
| 467 | t.minute, | ||
| 468 | t.second | ||
| 469 | ) | ||
| 470 | } | ||
| 471 | CharDevice::Pci => pci_listing(), | ||
| 472 | CharDevice::Version => version_text(), | ||
| 473 | CharDevice::Meminfo => meminfo_text(), | ||
| 474 | CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), | ||
| 475 | CharDevice::Zero => String::new(), | ||
| 476 | } | ||
| 477 | } | ||
| 478 | |||
| 479 | // --- self-test ------------------------------------------------------------ | ||
| 480 | |||
| 481 | pub fn self_test() -> bool { | ||
| 482 | let mut buf = [0u8; 8]; | ||
| 483 | let h = match ensure_file("/tmp/selftest") { | ||
| 484 | Ok(h) => h, | ||
| 485 | Err(_) => return false, | ||
| 486 | }; | ||
| 487 | if write(h, b"hello", 0) != Ok(5) { | ||
| 488 | return false; | ||
| 489 | } | ||
| 490 | if read(h, &mut buf, 0) != Ok(5) || &buf[..5] != b"hello" { | ||
| 491 | return false; | ||
| 492 | } | ||
| 493 | if remove(h).is_err() || resolve("/tmp/selftest").is_some() { | ||
| 494 | return false; | ||
| 495 | } | ||
| 496 | // device write goes nowhere: echo into /dev/null | ||
| 497 | if let Ok(nul) = ensure_file("/dev/null") { | ||
| 498 | let _ = write(nul, b"discard me", 0); | ||
| 499 | } | ||
| 500 | true | ||
| 501 | } | ||
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 @@ | |||
| 3 | extern crate alloc; | 3 | extern crate alloc; |
| 4 | 4 | ||
| 5 | pub mod font; | 5 | pub mod font; |
| 6 | pub mod fs; | ||
| 6 | pub mod gop; | 7 | pub mod gop; |
| 7 | pub mod interrupts; | 8 | pub mod interrupts; |
| 8 | pub mod memory; | 9 | 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 @@ | |||
| 2 | 2 | ||
| 3 | extern crate alloc; | 3 | extern crate alloc; |
| 4 | 4 | ||
| 5 | use alloc::string::ToString; | ||
| 6 | |||
| 5 | /// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. | 7 | /// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. |
| 6 | use rsnix::ps2::{self, KeyEvent}; | 8 | use rsnix::ps2::{self, KeyEvent}; |
| 7 | use rsnix::{gop_print, gop_println, serial_print, serial_println}; | 9 | use rsnix::{gop_print, gop_println, serial_print, serial_println}; |
| @@ -52,6 +54,12 @@ pub fn run() -> ! { | |||
| 52 | "stress" => cmd_stress(args), | 54 | "stress" => cmd_stress(args), |
| 53 | "date" => cmd_date(), | 55 | "date" => cmd_date(), |
| 54 | "pci" => cmd_pci(), | 56 | "pci" => cmd_pci(), |
| 57 | "ls" => cmd_ls(args), | ||
| 58 | "cat" => cmd_cat(args), | ||
| 59 | "hexdump" => cmd_hexdump(args), | ||
| 60 | "mkdir" => cmd_mkdir(args), | ||
| 61 | "touch" => cmd_touch(args), | ||
| 62 | "rm" => cmd_rm(args), | ||
| 55 | _ => { | 63 | _ => { |
| 56 | gop_println!("unknown command: {}", cmd); | 64 | gop_println!("unknown command: {}", cmd); |
| 57 | serial_println!("unknown command: {}", cmd); | 65 | serial_println!("unknown command: {}", cmd); |
| @@ -128,15 +136,220 @@ fn cmd_help() { | |||
| 128 | gop_println!(" stress — stress-test heap/page allocators"); | 136 | gop_println!(" stress — stress-test heap/page allocators"); |
| 129 | gop_println!(" date — show current date/time (RTC)"); | 137 | gop_println!(" date — show current date/time (RTC)"); |
| 130 | gop_println!(" pci — list PCI devices"); | 138 | gop_println!(" pci — list PCI devices"); |
| 139 | gop_println!(" ls — list a directory"); | ||
| 140 | gop_println!(" cat — print files (e.g. cat /dev/rtc)"); | ||
| 141 | gop_println!(" hexdump — hex-dump a file (e.g. hexdump /dev/mem)"); | ||
| 142 | gop_println!(" mkdir — create a directory"); | ||
| 143 | gop_println!(" touch — create an empty file"); | ||
| 144 | gop_println!(" rm — remove a file or empty dir"); | ||
| 145 | gop_println!(" echo > /dev/null, > file, >> file"); | ||
| 131 | gop_println!(" halt — halt the system (hlt loop)"); | 146 | gop_println!(" halt — halt the system (hlt loop)"); |
| 132 | gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); | 147 | gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); |
| 133 | gop_println!(" exit — alias for poweroff"); | 148 | gop_println!(" exit — alias for poweroff"); |
| 134 | serial_println!("available commands:"); | 149 | serial_println!("available commands:"); |
| 135 | serial_println!(" help, echo, clear, hello, info, mem, mmap, stress, date, pci, halt, poweroff, exit"); | 150 | serial_println!( |
| 151 | " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, halt, poweroff, exit" | ||
| 152 | ); | ||
| 136 | } | 153 | } |
| 137 | 154 | ||
| 138 | fn cmd_echo(args: &str) { | 155 | fn cmd_echo(args: &str) { |
| 156 | // Redirection: `text > path` (overwrite) or `text >> path` (append). | ||
| 157 | if let Some(idx) = args.find('>') { | ||
| 158 | let append = args[idx..].starts_with(">>"); | ||
| 159 | let text = &args[..idx]; | ||
| 160 | let path = args[idx + 1 + append as usize..].trim(); | ||
| 161 | let path = path.split_whitespace().next().unwrap_or(""); | ||
| 162 | if !path.is_empty() { | ||
| 163 | let content = alloc::format!("{}\n", text.trim_end()); | ||
| 164 | let result = rsnix::fs::ensure_file(path).and_then(|h| { | ||
| 165 | if append { | ||
| 166 | rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)) | ||
| 167 | .map(|_| ()) | ||
| 168 | } else { | ||
| 169 | let _ = rsnix::fs::set_len(h, 0); | ||
| 170 | rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ()) | ||
| 171 | } | ||
| 172 | }); | ||
| 173 | match result { | ||
| 174 | Ok(()) => {} | ||
| 175 | Err(()) => { | ||
| 176 | gop_println!("echo: cannot write '{}'", path); | ||
| 177 | serial_println!("echo: cannot write '{}'", path); | ||
| 178 | } | ||
| 179 | } | ||
| 180 | return; | ||
| 181 | } | ||
| 182 | } | ||
| 139 | gop_println!("{}", args); | 183 | gop_println!("{}", args); |
| 184 | serial_println!("{}", args); | ||
| 185 | } | ||
| 186 | |||
| 187 | fn cmd_ls(args: &str) { | ||
| 188 | let path = args.split_whitespace().next().unwrap_or("/"); | ||
| 189 | let mut out = alloc::string::String::new(); | ||
| 190 | match rsnix::fs::resolve(path) { | ||
| 191 | None => { | ||
| 192 | out.push_str(&alloc::format!("ls: {}: no such file or directory\n", path)); | ||
| 193 | } | ||
| 194 | Some(h) => { | ||
| 195 | let list = if rsnix::fs::kind(h) == rsnix::fs::FileType::Dir { | ||
| 196 | rsnix::fs::children(h) | ||
| 197 | } else { | ||
| 198 | alloc::vec![( | ||
| 199 | path.rsplit('/').next().unwrap_or(path).to_string(), | ||
| 200 | rsnix::fs::kind(h), | ||
| 201 | rsnix::fs::size(h) | ||
| 202 | )] | ||
| 203 | }; | ||
| 204 | for (name, kind, size) in list { | ||
| 205 | match kind { | ||
| 206 | rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)), | ||
| 207 | rsnix::fs::FileType::Char => { | ||
| 208 | out.push_str(&alloc::format!("{}@\n", name)) | ||
| 209 | } | ||
| 210 | rsnix::fs::FileType::Regular => { | ||
| 211 | out.push_str(&alloc::format!("{} {} B\n", name, size)) | ||
| 212 | } | ||
| 213 | } | ||
| 214 | } | ||
| 215 | } | ||
| 216 | } | ||
| 217 | gop_print!("{}", out); | ||
| 218 | serial_print!("{}", out); | ||
| 219 | } | ||
| 220 | |||
| 221 | fn cmd_cat(args: &str) { | ||
| 222 | let paths: alloc::vec::Vec<&str> = args.split_whitespace().collect(); | ||
| 223 | if paths.is_empty() { | ||
| 224 | gop_println!("usage: cat <path> [path...]"); | ||
| 225 | serial_println!("usage: cat <path> [path...]"); | ||
| 226 | return; | ||
| 227 | } | ||
| 228 | let mut out = alloc::string::String::new(); | ||
| 229 | let mut buf = [0u8; 512]; | ||
| 230 | for p in paths { | ||
| 231 | match rsnix::fs::resolve(p) { | ||
| 232 | None => out.push_str(&alloc::format!("cat: {}: no such file\n", p)), | ||
| 233 | Some(h) => { | ||
| 234 | let mut pos = 0u64; | ||
| 235 | loop { | ||
| 236 | match rsnix::fs::read(h, &mut buf, pos) { | ||
| 237 | Ok(0) | Err(_) => break, | ||
| 238 | Ok(n) => { | ||
| 239 | out.push_str(core::str::from_utf8(&buf[..n]).unwrap_or("?")); | ||
| 240 | pos += n as u64; | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
| 244 | } | ||
| 245 | } | ||
| 246 | } | ||
| 247 | gop_print!("{}", out); | ||
| 248 | serial_print!("{}", out); | ||
| 249 | } | ||
| 250 | |||
| 251 | fn cmd_hexdump(args: &str) { | ||
| 252 | let path = args.split_whitespace().next().unwrap_or(""); | ||
| 253 | if path.is_empty() { | ||
| 254 | gop_println!("usage: hexdump <path>"); | ||
| 255 | serial_println!("usage: hexdump <path>"); | ||
| 256 | return; | ||
| 257 | } | ||
| 258 | let h = match rsnix::fs::resolve(path) { | ||
| 259 | Some(h) => h, | ||
| 260 | None => { | ||
| 261 | gop_println!("hexdump: {}: no such file", path); | ||
| 262 | serial_println!("hexdump: {}: no such file", path); | ||
| 263 | return; | ||
| 264 | } | ||
| 265 | }; | ||
| 266 | let mut out = alloc::string::String::new(); | ||
| 267 | let mut buf = [0u8; 512]; | ||
| 268 | let mut pos = 0u64; | ||
| 269 | use core::fmt::Write as _; | ||
| 270 | 'outer: loop { | ||
| 271 | let n = match rsnix::fs::read(h, &mut buf, pos) { | ||
| 272 | Ok(0) | Err(_) => break, | ||
| 273 | Ok(n) => n, | ||
| 274 | }; | ||
| 275 | for i in (0..n).step_by(16) { | ||
| 276 | let _ = core::write!(out, "{:08x} ", pos + i as u64); | ||
| 277 | for j in 0..16 { | ||
| 278 | if i + j < n { | ||
| 279 | let _ = core::write!(out, "{:02x} ", buf[i + j]); | ||
| 280 | } else { | ||
| 281 | out.push_str(" "); | ||
| 282 | } | ||
| 283 | } | ||
| 284 | out.push_str(" |"); | ||
| 285 | for j in 0..16 { | ||
| 286 | if i + j >= n { | ||
| 287 | break; | ||
| 288 | } | ||
| 289 | let b = buf[i + j]; | ||
| 290 | out.push(if b.is_ascii_graphic() || b == b' ' { | ||
| 291 | b as char | ||
| 292 | } else { | ||
| 293 | '.' | ||
| 294 | }); | ||
| 295 | } | ||
| 296 | out.push_str("|\n"); | ||
| 297 | if i + 16 >= n { | ||
| 298 | pos += n as u64; | ||
| 299 | continue 'outer; | ||
| 300 | } | ||
| 301 | } | ||
| 302 | } | ||
| 303 | gop_print!("{}", out); | ||
| 304 | serial_print!("{}", out); | ||
| 305 | } | ||
| 306 | |||
| 307 | fn cmd_mkdir(args: &str) { | ||
| 308 | let path = args.split_whitespace().next().unwrap_or(""); | ||
| 309 | if path.is_empty() { | ||
| 310 | gop_println!("usage: mkdir <path>"); | ||
| 311 | serial_println!("usage: mkdir <path>"); | ||
| 312 | return; | ||
| 313 | } | ||
| 314 | match rsnix::fs::mkdir(path) { | ||
| 315 | Ok(()) => {} | ||
| 316 | Err(()) => { | ||
| 317 | gop_println!("mkdir: cannot create directory '{}'", path); | ||
| 318 | serial_println!("mkdir: cannot create directory '{}'", path); | ||
| 319 | } | ||
| 320 | } | ||
| 321 | } | ||
| 322 | |||
| 323 | fn cmd_touch(args: &str) { | ||
| 324 | let path = args.split_whitespace().next().unwrap_or(""); | ||
| 325 | if path.is_empty() { | ||
| 326 | gop_println!("usage: touch <path>"); | ||
| 327 | serial_println!("usage: touch <path>"); | ||
| 328 | return; | ||
| 329 | } | ||
| 330 | match rsnix::fs::touch(path) { | ||
| 331 | Ok(()) => {} | ||
| 332 | Err(()) => { | ||
| 333 | gop_println!("touch: cannot create '{}'", path); | ||
| 334 | serial_println!("touch: cannot create '{}'", path); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | } | ||
| 338 | |||
| 339 | fn cmd_rm(args: &str) { | ||
| 340 | let path = args.split_whitespace().next().unwrap_or(""); | ||
| 341 | if path.is_empty() { | ||
| 342 | gop_println!("usage: rm <path>"); | ||
| 343 | serial_println!("usage: rm <path>"); | ||
| 344 | return; | ||
| 345 | } | ||
| 346 | match rsnix::fs::remove_path(path) { | ||
| 347 | Ok(()) => {} | ||
| 348 | Err(()) => { | ||
| 349 | gop_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); | ||
| 350 | serial_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); | ||
| 351 | } | ||
| 352 | } | ||
| 140 | } | 353 | } |
| 141 | 354 | ||
| 142 | fn cmd_clear() { | 355 | fn cmd_clear() { |
| @@ -372,7 +585,12 @@ fn cmd_date() { | |||
| 372 | let t = rsnix::rtc::now(); | 585 | let t = rsnix::rtc::now(); |
| 373 | let line = alloc::format!( | 586 | let line = alloc::format!( |
| 374 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", | 587 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", |
| 375 | t.year, t.month, t.day, t.hour, t.minute, t.second | 588 | t.year, |
| 589 | t.month, | ||
| 590 | t.day, | ||
| 591 | t.hour, | ||
| 592 | t.minute, | ||
| 593 | t.second | ||
| 376 | ); | 594 | ); |
| 377 | gop_println!("{}", line); | 595 | gop_println!("{}", line); |
| 378 | serial_println!("{}", line); | 596 | serial_println!("{}", line); |
