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 /shell/src | |
| 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
Diffstat (limited to 'shell/src')
| -rw-r--r-- | shell/src/lib.rs | 222 |
1 files changed, 220 insertions, 2 deletions
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); |
