RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 01:32:12 -0700
commitd3dbcfaf7fae0c452e63f201fbc709209f8810ed
parent320b50a20c463485b5abd4f9d71b5395b4c27581
treebrowse files at this commit
Shell: drive letters start at A: (root) instead of C:

/ is A:, mounted disks get B:, C:, ... in mount order (the FAT disk is B:).
C: for the system drive is a floppy-era leftover; Rustbelt has no floppies.
Updated shparse, sh help text, selftest.sh, disk readme, tests, scripts,
README and boot.png.
MREADME.md+69 -64
Mboot.png+0 -0
Mdisk/readme.txt+1 -1
Mdisk/scripts/selftest.sh+11 -11
Mscripts/run-headless.sh+4 -4
Mscripts/screenshot.sh+4 -4
Mtests/shell.rs+24 -24
Muser/sh.rs+3 -3
Muser/shparse.rs+12 -12

9 files changed, 128 insertions, 123 deletions

@@ -5,7 +5,7 @@ the 2nd edition of Philipp Oppermann's [Writing an OS in Rust](https://os.phil-opp.com/) and keeps going past the end of it. It boots in QEMU straight into **its own shell**: a user-mode program that has a Unix feel with DOS touches. It offers `ls`, `cat`, pipes and-redirection, plus `dir`, `type`, `ver`, and drive letters like `C:` and `D:`.+redirection, plus `dir`, `type`, `ver`, and drive letters like `A:` and `B:`.  Under the shell: - **VGA text and serial output.** The kernel prints to the VGA text screen and@@ -79,8 +79,8 @@ cargo run                           # build + boot in a QEMU window; serial outp ```  **Using it:** `cargo run` opens a QEMU window. The kernel prints a few boot-lines, then you're at the shell prompt `C:/ $`. Click into the window and type,-for example `help`, `cd D:`, `ls` or `cat readme.txt | wc`. See+lines, then you're at the shell prompt `A:/ $`. Click into the window and type,+for example `help`, `cd B:`, `ls` or `cat readme.txt | wc`. See [The shell](#the-shell-stage-7) for everything it can do. Everything on the screen is also mirrored to your terminal over serial. Typing `exit` ends the shell, and the kernel starts a fresh one.@@ -136,29 +136,29 @@ Rustbelt 0.7.0 -- a hobby x86_64 kernel in Rust [vfs] mounted ramfs at / [vfs] mounted ramfs at /tmp [vfs] mounted fat32 at /mnt/disk0-C:/ $ ver+A:/ $ ver Rustbelt 0.7.0 (stage 7: shell) x86_64-C:/ $ drives-C:  /            ramfs-D:  /mnt/disk0   fat32-C:/ $ pwd+A:/ $ drives+A:  /            ramfs+B:  /mnt/disk0   fat32+A:/ $ pwd /-C:/ $ cd D:-D:/ $ ls+A:/ $ cd B:+B:/ $ ls [process] started pid 2 (ls), entry 0x8000402c00 bin/        notes/      readme.txt  scripts/ [process] pid 2 (ls) exited with code 0-D:/ $ ls -l notes+B:/ $ ls -l notes [process] started pid 3 (ls), entry 0x8000402c00        125  todo.txt [process] pid 3 (ls) exited with code 0-D:/ $ type D:/readme.txt+B:/ $ type B:/readme.txt [process] started pid 4 (cat), entry 0x8000400260 Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT partition, in a FAT32 file system -- read through the kernel's VFS.-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`. [process] pid 4 (cat) exited with code 0-D:/ $ cat d:notes/todo.txt | grep -i EXT | wc -l+B:/ $ cat b:notes/todo.txt | grep -i EXT | wc -l [process] started pid 5 (cat), entry 0x8000400260 [process] started pid 6 (grep), entry 0x8000400610 [process] pid 5 (cat) exited with code 0@@ -166,46 +166,46 @@ D:/ $ cat d:notes/todo.txt | grep -i EXT | wc -l [process] pid 6 (grep) exited with code 0 1 [process] pid 7 (wc) exited with code 0-D:/ $ dir bin | wc -l+B:/ $ dir bin | wc -l [process] started pid 8 (ls), entry 0x8000402c00 [process] started pid 9 (wc), entry 0x8000400260 [process] pid 8 (ls) exited with code 0 15 [process] pid 9 (wc) exited with code 0-D:/ $ echo first line > C:/tmp/out.txt-D:/ $ echo second line >> /tmp/out.txt-D:/ $ cat < /tmp/out.txt+B:/ $ echo first line > A:/tmp/out.txt+B:/ $ echo second line >> /tmp/out.txt+B:/ $ cat < /tmp/out.txt [process] started pid 10 (cat), entry 0x8000400260 first line second line [process] pid 10 (cat) exited with code 0-D:/ $ md /tmp/work+B:/ $ md /tmp/work [process] started pid 11 (mkdir), entry 0x8000400100 [process] pid 11 (mkdir) exited with code 0-D:/ $ copy D:/notes/todo.txt /tmp/work+B:/ $ copy B:/notes/todo.txt /tmp/work [process] started pid 12 (cp), entry 0x8000400670 [process] pid 12 (cp) exited with code 0-D:/ $ touch /tmp/work/empty.txt+B:/ $ touch /tmp/work/empty.txt [process] started pid 13 (touch), entry 0x8000400100 [process] pid 13 (touch) exited with code 0-D:/ $ ls -l C:/tmp/work+B:/ $ ls -l A:/tmp/work [process] started pid 14 (ls), entry 0x8000402c00          0  empty.txt        125  todo.txt [process] pid 14 (ls) exited with code 0-D:/ $ del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work+B:/ $ del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work [process] started pid 15 (rm), entry 0x8000400100 [process] pid 15 (rm) exited with code 0-D:/ $ grep "D:" readme.txt+B:/ $ grep "B:" readme.txt [process] started pid 16 (grep), entry 0x8000400610-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`. [process] pid 16 (grep) exited with code 0-D:/ $ alias ll="ls -l"-D:/ $ ll C:/tmp+B:/ $ alias ll="ls -l"+B:/ $ ll A:/tmp [process] started pid 17 (ls), entry 0x8000402c00         23  out.txt [process] pid 17 (ls) exited with code 0-D:/ $ run --trace cat D:/notes/todo.txt+B:/ $ run --trace cat B:/notes/todo.txt [process] started pid 18 (cat), entry 0x8000400260 [trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3 [trace 18] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125@@ -216,29 +216,29 @@ Next: more file systems -- ext2, exFAT, btrfs. [trace 18] close(3) = 0 [trace 18] exit(0) [process] pid 18 (cat) exited with code 0-D:/ $ start sleep 1+B:/ $ start sleep 1 [process] started pid 19 (sleep), entry 0x8000400000 [pid 19] sleep started in the background-D:/ $ jobs+B:/ $ jobs [pid 19] running  start sleep 1-D:/ $ wait+B:/ $ wait [process] pid 19 (sleep) exited with code 0 [done] pid 19 (start sleep 1), exit code 0-D:/ $ run evil+B:/ $ run evil [process] started pid 20 (evil), entry 0x80004000f0 [evil] pid 20: reading kernel memory at 0x200000... [kernel] user page fault: address Ok(VirtAddr(0x200000)), error PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE) [kernel] page fault in user mode at rip 0x800040017e [process] pid 20 (evil) killed: page fault [exit code 139]-D:/ $ nosuchprogram+B:/ $ nosuchprogram sh: nosuchprogram: command not found (try `help`)-D:/ $ cd Q:+B:/ $ cd Q: cd: there is no drive Q: (see `drives`)-D:/ $ cd C:/tmp-C:/tmp $ pwd+B:/ $ cd A:/tmp+A:/tmp $ pwd /tmp-C:/tmp $ exit 0+A:/tmp $ exit 0 [process] pid 1 (sh) exited with code 0 [ok] shell self-test finished: Exited(0) [memory] 10 frames in use@@ -285,7 +285,7 @@ heap_allocation::simple_allocation...               [ok] | `tests/should_panic.rs` | a failing `assert_eq!` reaches the panic handler (`harness = false`) | | `tests/stack_overflow.rs` | stack overflow lands in the double-fault handler on the IST stack (`harness = false`) | | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) |-| `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `D:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged |+| `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `B:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged | | `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter | | `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts, sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` | | `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk |@@ -896,19 +896,19 @@ When it exits, the thread starts a new one, the way `init` respawns a login shell.  ```-Rustbelt shell. Type `help` for commands. Drives: C: = /, D: = /mnt/disk0-C:/ $ cd d:-D:/ $ ls+Rustbelt shell. Type `help` for commands. Drives: A: = /, B: = /mnt/disk0+A:/ $ cd b:+B:/ $ ls bin/        notes/      readme.txt  scripts/-D:/ $ type readme.txt | grep -i rustbelt-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.-D:/ $ ls bin | wc -l+B:/ $ type readme.txt | grep -i rustbelt+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`.+B:/ $ ls bin | wc -l 15-D:/ $ echo first > C:/tmp/a.txt-D:/ $ echo second >> C:/tmp/a.txt-D:/ $ wc < C:/tmp/a.txt+B:/ $ echo first > A:/tmp/a.txt+B:/ $ echo second >> A:/tmp/a.txt+B:/ $ wc < A:/tmp/a.txt       2       2      13-D:/ $ run --trace cat notes/todo.txt+B:/ $ run --trace cat notes/todo.txt [trace 8] open("notes/todo.txt", 0o0) = 3 [trace 8] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125 Done: the shell (cd, ls, cat, run, start, pipes, redirection, drive letters).@@ -917,26 +917,26 @@ Next: more file systems -- ext2, exFAT, btrfs. [trace 8] read(3, "", 512) = 0 [trace 8] close(3) = 0 [trace 8] exit(0)-D:/ $ start sleep 5+B:/ $ start sleep 5 [pid 9] sleep started in the background-D:/ $ jobs+B:/ $ jobs [pid 9] running  start sleep 5-D:/ $ run evil+B:/ $ run evil [evil] pid 10: reading kernel memory at 0x200000... [process] pid 10 (evil) killed: page fault [exit code 139]-D:/ $ ver+B:/ $ ver Rustbelt 0.7.0 (stage 7: shell) x86_64-D:/ $                      <- Enter on an empty line, a few seconds later+B:/ $                      <- Enter on an empty line, a few seconds later [done] pid 9 (start sleep 5), exit code 0-D:/ $+B:/ $ ```  ### Commands  | Command | What it does | |---|---|-| `cd [dir]` | change directory (`cd` alone = `C:/`); `cd D:` goes to the root of drive D |+| `cd [dir]` | change directory (`cd` alone = `A:/`); `cd B:` goes to the root of drive B | | `pwd` | print the current directory as a Unix path (the prompt shows the drive form) | | `echo text...` | print the arguments | | `ver` | kernel name and version, from the `uname` syscall |@@ -949,9 +949,9 @@ D:/ $ | `exit [code]` | leave the shell (with the last command's exit code, or `code`) | | `run [--trace] prog args` | run a program in the foreground; `--trace` prints every syscall it makes | | `start prog args` | run it in the background and print its pid (same as `prog args &`) |-| `prog args` | a bare name is looked up in each drive's `bin` directory, in order: `C:/bin`, `D:/bin`... |+| `prog args` | a bare name is looked up in each drive's `bin` directory, in order: `A:/bin`, `B:/bin`... | -Programs in `D:/bin` (`user/*.rs`, each 30-150 lines):+Programs in `B:/bin` (`user/*.rs`, each 30-150 lines):  | Program | What it does | |---|---|@@ -1001,20 +1001,25 @@ to the pipe. Pipes have no size limit here, so that can't block.  Drive letters are only shell syntax. The kernel keeps one Unix tree: -- **Mapping**: `C:` is `/`. `D:`, `E:`... go to the mounts under `/mnt`,+- **Why start at A:?** In DOS and Windows the system drive is `C:` only for+  historical reasons. `A:` and `B:` were reserved for floppy drives, so the+  first hard disk got `C:`, and Windows still keeps that habit. Rustbelt has+  no floppies, so its letters simply start at the front of the alphabet.+  (The first version of the shell used `C:` for `/`.)+- **Mapping**: `A:` is `/`. `B:`, `C:`... go to the mounts under `/mnt`,   in the order they were mounted. The shell reads them from the `mounts`   syscall before every prompt, so a newly mounted disk would get a letter-  right away. Other mounts, such as `/tmp`, stay under C: (`C:/tmp`).+  right away. Other mounts, such as `/tmp`, stay under A: (`A:/tmp`). - **Translation**: every unquoted word that starts with a known letter is   translated before any syscall sees it (`shparse::translate`):-  - `D:/notes/todo.txt` becomes `/mnt/disk0/notes/todo.txt`.-  - `d:` becomes `/mnt/disk0` (any case).-  - `D:notes` becomes `/mnt/disk0/notes`, since drives don't have their own+  - `B:/notes/todo.txt` becomes `/mnt/disk0/notes/todo.txt`.+  - `b:` becomes `/mnt/disk0` (any case).+  - `B:notes` becomes `/mnt/disk0/notes`, since drives don't have their own     current directory. -  Quote a word to keep it literal: `grep "D:" readme.txt`.+  Quote a word to keep it literal: `grep "B:" readme.txt`. - **Prompt**: the prompt goes the other way (`shparse::display`). The longest-  matching drive path wins, so `/mnt/disk0/bin` shows as `D:/bin`.+  matching drive path wins, so `/mnt/disk0/bin` shows as `B:/bin`. - **Unknown letters**: these are left alone, so `cd Q:` says   `there is no drive Q:`. 

Binary file changed.

@@ -1,3 +1,3 @@ Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT partition, in a FAT32 file system -- read through the kernel's VFS.-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`.
@@ -1,32 +1,32 @@ # Rustbelt shell self-test: `cargo run --features headless` runs this with-# `sh /mnt/disk0/scripts/selftest.sh` (D:/scripts/selftest.sh) and scripts/run-headless.sh checks the output.+# `sh /mnt/disk0/scripts/selftest.sh` (B:/scripts/selftest.sh) and scripts/run-headless.sh checks the output. ver drives pwd-cd D:+cd B: ls ls -l notes-type D:/readme.txt-cat d:notes/todo.txt | grep -i EXT | wc -l+type B:/readme.txt+cat b:notes/todo.txt | grep -i EXT | wc -l dir bin | wc -l-echo first line > C:/tmp/out.txt+echo first line > A:/tmp/out.txt echo second line >> /tmp/out.txt cat < /tmp/out.txt md /tmp/work-copy D:/notes/todo.txt /tmp/work+copy B:/notes/todo.txt /tmp/work touch /tmp/work/empty.txt-ls -l C:/tmp/work+ls -l A:/tmp/work del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work-grep "D:" readme.txt+grep "B:" readme.txt alias ll="ls -l"-ll C:/tmp-run --trace cat D:/notes/todo.txt+ll A:/tmp+run --trace cat B:/notes/todo.txt start sleep 1 jobs wait run evil nosuchprogram cd Q:-cd C:/tmp+cd A:/tmp pwd exit 0
@@ -32,10 +32,10 @@ set -e if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     expect=(         "Rustbelt 0.7.0 (stage 7: shell) x86_64"      # ver-        "D:  /mnt/disk0   fat32"                       # drives-        "D:/ $ ls"                                     # cd D: changed the prompt+        "B:  /mnt/disk0   fat32"                       # drives+        "B:/ $ ls"                                     # cd B: changed the prompt         "readme.txt  scripts/"                         # ls in columns-        "In the Rustbelt shell this disk is drive D:"  # type D:/readme.txt+        "In the Rustbelt shell this disk is drive B:"  # type B:/readme.txt         "second line"                                  # >> and <         "       125  todo.txt"                         # cp + ls -l         '[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3'   # run --trace@@ -44,7 +44,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "[exit code 139]"                              # run evil         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"-        "C:/tmp $ pwd"+        "A:/tmp $ pwd"     )     missing=0     for line in "${expect[@]}"; do
@@ -25,14 +25,14 @@ if [ -z "$SESSION" ]; then     # and fixes it.     SESSION=$(mktemp /tmp/session.XXXXXX)     cat > "$SESSION" <<'TXT'-cd d:+cd b: lx{BS}s-cat D:/notes/todo.txt+cat B:/notes/todo.txt type readme.txt | grep -i rustbelt run --trace cat notes/todo.txt TXT-    set -- "D:/ $ ls" "bin/        notes/" "Next: more file systems" \-        "In the Rustbelt shell this disk is drive D:" \+    set -- "B:/ $ ls" "bin/        notes/" "Next: more file systems" \+        "In the Rustbelt shell this disk is drive B:" \         '] open("notes/todo.txt", 0o0) = 3' "] exit(0)" fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX)
@@ -94,21 +94,21 @@ const MOUNTS: &str = "/ ramfs\n/tmp ramfs\n/mnt/disk0 fat32\n/mnt/disk1 fat32\n" fn drive_letters_are_assigned_in_mount_order() {     let drives = shparse::assign_drives(MOUNTS);     let summary: Vec<(char, &str, &str)> = drives.iter().map(|d| (d.letter, d.path.as_str(), d.fs.as_str())).collect();-    // /tmp gets no letter: it's C:/tmp.-    assert_eq!(summary, [('C', "/", "ramfs"), ('D', "/mnt/disk0", "fat32"), ('E', "/mnt/disk1", "fat32")]);+    // /tmp gets no letter: it's A:/tmp.+    assert_eq!(summary, [('A', "/", "ramfs"), ('B', "/mnt/disk0", "fat32"), ('C', "/mnt/disk1", "fat32")]); }  #[test_case] fn drive_letters_translate_to_paths() {     let drives = shparse::assign_drives(MOUNTS);     let t = |s| shparse::translate(s, &drives);-    assert_eq!(t("D:").as_deref(), Some("/mnt/disk0"));-    assert_eq!(t("D:/").as_deref(), Some("/mnt/disk0"));-    assert_eq!(t("d:/notes/todo.txt").as_deref(), Some("/mnt/disk0/notes/todo.txt")); // any case-    assert_eq!(t("D:notes").as_deref(), Some("/mnt/disk0/notes"));-    assert_eq!(t("e:/x").as_deref(), Some("/mnt/disk1/x"));-    assert_eq!(t("C:").as_deref(), Some("/"));-    assert_eq!(t("C:/tmp/a.txt").as_deref(), Some("/tmp/a.txt"));+    assert_eq!(t("B:").as_deref(), Some("/mnt/disk0"));+    assert_eq!(t("B:/").as_deref(), Some("/mnt/disk0"));+    assert_eq!(t("b:/notes/todo.txt").as_deref(), Some("/mnt/disk0/notes/todo.txt")); // any case+    assert_eq!(t("B:notes").as_deref(), Some("/mnt/disk0/notes"));+    assert_eq!(t("c:/x").as_deref(), Some("/mnt/disk1/x"));+    assert_eq!(t("A:").as_deref(), Some("/"));+    assert_eq!(t("A:/tmp/a.txt").as_deref(), Some("/tmp/a.txt"));     assert_eq!(t("Z:/x"), None); // no such drive     assert_eq!(t("readme.txt"), None);     assert_eq!(t("/mnt/disk0"), None);@@ -119,13 +119,13 @@ fn drive_letters_translate_to_paths() { fn paths_display_with_drive_letters() {     let drives = shparse::assign_drives(MOUNTS);     let d = |s| shparse::display(s, &drives);-    assert_eq!(d("/"), "C:/");-    assert_eq!(d("/tmp"), "C:/tmp");-    assert_eq!(d("/mnt/disk0"), "D:/");-    assert_eq!(d("/mnt/disk0/bin"), "D:/bin");-    assert_eq!(d("/mnt/disk1/a/b"), "E:/a/b");-    assert_eq!(d("/mnt/disk01"), "C:/mnt/disk01"); // not inside /mnt/disk0-    assert_eq!(d("/mnt"), "C:/mnt");+    assert_eq!(d("/"), "A:/");+    assert_eq!(d("/tmp"), "A:/tmp");+    assert_eq!(d("/mnt/disk0"), "B:/");+    assert_eq!(d("/mnt/disk0/bin"), "B:/bin");+    assert_eq!(d("/mnt/disk1/a/b"), "C:/a/b");+    assert_eq!(d("/mnt/disk01"), "A:/mnt/disk01"); // not inside /mnt/disk0+    assert_eq!(d("/mnt"), "A:/mnt"); }  // --- the real shell --------------------------------------------------------@@ -154,13 +154,13 @@ fn run_shell_script(script: &str) -> (String, ExitStatus) { #[test_case] fn scripted_shell_session() {     let (out, status) = run_shell_script(-        "cd D:\n\+        "cd B:\n\          pwd\n\          ls notes\n\          echo piped text | grep piped\n\-         type d:notes/todo.txt | wc -l\n\-         echo saved > C:/tmp/s.txt\n\-         echo more >> C:/tmp/s.txt\n\+         type b:notes/todo.txt | wc -l\n\+         echo saved > A:/tmp/s.txt\n\+         echo more >> A:/tmp/s.txt\n\          cat < /tmp/s.txt\n\          ver\n\          nosuchprogram\n\@@ -169,14 +169,14 @@ fn scripted_shell_session() {     );     // Each command is echoed after its prompt; the prompt shows the drive.     for expected in [-        "C:/ $ cd D:\n",-        "D:/ $ pwd\n/mnt/disk0\n",-        "D:/ $ ls notes\ntodo.txt\n", // into a pipe: one name per line+        "A:/ $ cd B:\n",+        "B:/ $ pwd\n/mnt/disk0\n",+        "B:/ $ ls notes\ntodo.txt\n", // into a pipe: one name per line         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",         "Rustbelt 0.7.0",-        "D:/ $ exit 3\n",+        "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);     }
@@ -41,7 +41,7 @@ Rustbelt shell commands   run [--trace] prog args   run a program (--trace prints its syscalls)   start prog args           run it in the background (or: prog args &)   a | b | c                 pipes;  > file  >> file  < file  redirection-  C: is /, D: E: ... are the disks (see `drives`): cd D:, cat D:/readme.txt+  A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt ";  /// A background job started with `start` or `&`.@@ -137,7 +137,7 @@ impl Shell {         String::from_utf8_lossy(&buf[..n]).into_owned()     } -    /// `C:/ $ `, `D:/notes $ `+    /// `A:/ $ `, `B:/notes $ `     fn prompt(&self) -> String {         format!("{} $ ", shparse::display(&self.cwd(), &self.drives))     }@@ -381,7 +381,7 @@ impl Shell {     }      /// Find a program: names with a `/` are paths; others are looked up in-    /// the `bin` directory of every drive (our PATH): C:/bin, D:/bin, ...+    /// the `bin` directory of every drive (our PATH): A:/bin, B:/bin, ...     fn find_program(&self, name: &str) -> Option<String> {         if name.contains('/') {             return Some(name.to_string());
@@ -1,5 +1,5 @@ //! The shell's parser: command line -> tokens -> pipeline, and the drive-//! letter mapping (`D:/notes` <-> `/mnt/disk0/notes`).+//! letter mapping (`B:/notes` <-> `/mnt/disk0/notes`). //! //! Pure functions with no syscalls, so the same file is compiled into the //! shell (user/sh.rs) *and* into the kernel's test suite (tests/shell.rs),@@ -20,7 +20,7 @@ use alloc::{format, string::String, vec::Vec};  /// One word of a command. `quoted` words are never drive-translated, so-/// `grep "C:" file` searches for the text C:.+/// `grep "A:" file` searches for the text A:. #[derive(Debug, Clone, PartialEq)] pub struct Word {     pub text: String,@@ -161,17 +161,17 @@ pub struct Drive {  /// Assign letters from the kernel's mount table (the text from the /// `mounts` syscall: one "path fstype" per line, in mount order).-/// C: is always the root; D:, E:, ... go to the mounts under /mnt (the+/// A: is always the root; B:, C:, ... go to the mounts under /mnt (the /// disks), in the order they were mounted. Other mounts such as /tmp are-/// reachable through C: (C:/tmp).+/// reachable through A: (A:/tmp). pub fn assign_drives(mount_table: &str) -> Vec<Drive> {     let mut drives = Vec::new();     let root_fs = mount_table         .lines()         .find_map(|l| l.split_once(' ').filter(|(p, _)| *p == "/").map(|(_, fs)| fs))         .unwrap_or("ramfs");-    drives.push(Drive { letter: 'C', path: String::from("/"), fs: String::from(root_fs.trim()) });-    let mut letter = b'D';+    drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs.trim()) });+    let mut letter = b'B';     for line in mount_table.lines() {         if let Some((path, fs)) = line.split_once(' ') {             if path.starts_with("/mnt/") && letter <= b'Z' {@@ -183,7 +183,7 @@ pub fn assign_drives(mount_table: &str) -> Vec<Drive> {     drives } -/// Does `word` start with a drive letter ("D:", "d:/x", "E:notes")?+/// Does `word` start with a drive letter ("B:", "b:/x", "C:notes")? pub fn drive_letter(word: &str) -> Option<char> {     let b = word.as_bytes();     if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' {@@ -193,9 +193,9 @@ pub fn drive_letter(word: &str) -> Option<char> {     } } -/// `D:/notes/todo.txt` -> `/mnt/disk0/notes/todo.txt`. Letters are case-/// insensitive; `D:` and `D:/` are the drive's root, and `D:notes` means-/// `D:/notes` (drives have no current directory of their own). `None` if+/// `B:/notes/todo.txt` -> `/mnt/disk0/notes/todo.txt`. Letters are case+/// insensitive; `B:` and `B:/` are the drive's root, and `B:notes` means+/// `B:/notes` (drives have no current directory of their own). `None` if /// `word` doesn't start with a known drive letter. pub fn translate(word: &str, drives: &[Drive]) -> Option<String> {     let letter = drive_letter(word)?;@@ -210,8 +210,8 @@ pub fn translate(word: &str, drives: &[Drive]) -> Option<String> {     }) } -/// The other direction, for the prompt: `/mnt/disk0/bin` -> `D:/bin`,-/// `/tmp` -> `C:/tmp`. Picks the drive with the longest matching path.+/// The other direction, for the prompt: `/mnt/disk0/bin` -> `B:/bin`,+/// `/tmp` -> `A:/tmp`. Picks the drive with the longest matching path. pub fn display(path: &str, drives: &[Drive]) -> String {     let mut best: Option<(&Drive, &str)> = None;     for d in drives {