RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorGrok Bot <grokbot@localhost>2026-10-31 19:42:42 -0700
commit8950fae6ee56f66ef0918e92a70b2a4a6d9d493f
parent15035eada55eda065a0ceb6f556a1eb576fa2867
treebrowse files at this commit
rbsh: C: is the share (/mnt/share) when mounted, disks take B:, D:, ...; getty prefers /bin/rbsh; host tests (make rbsh-test)
MMakefile+8 -1
Arust/rbsh/host-tests/parse_test.rs+53 -0
Mrust/rbsh/src/main.rs+4 -2
Mrust/rbsh/src/parse.rs+30 -14
Mrust/sys/src/bin/getty.rs+8 -3

5 files changed, 103 insertions, 20 deletions

@@ -22,7 +22,7 @@ ALL_BIN    := $(BIN_ORDER) $(FOUNDRY_C) $(FOUNDRY_PORTS) PORTS_OUT  := $(OUT)/ports  .DEFAULT_GOAL := image-.PHONY: help image rust fixtures cprogs ports staging kernel kernel-src run test test-reference compare k1-check musl clean distclean+.PHONY: help image rust fixtures cprogs ports staging kernel kernel-src run test test-reference compare k1-check rbsh-test musl clean distclean  help: 	@echo "Foundry OS (userland for the Rustbelt kernel $(KERNEL_VERSION) @ $(KERNEL_REV))"@@ -38,6 +38,7 @@ help: 	@echo "  make test           headless boot + the shell self-test + expected-line checks" 	@echo "  make test-reference  the same headless test, but booting the kernel's own disk.img (baseline)" 	@echo "  make compare        compare out/$(IMAGE_NAME) with the kernel's own disk.img"+	@echo "  make rbsh-test      host unit tests for rbsh's parser and drive letters (no QEMU)" 	@echo "  make k1-check       build the kernel with RUSTBELT_DISK0=our image (K1) and check it boots ours" 	@echo "  make musl           fetch (cache/) + build musl $(MUSL_VERSION) into out/sysroot" 	@echo "  make clean          rm -rf out   (distclean also removes cache/)"@@ -107,6 +108,12 @@ test-reference: kernel compare: image kernel 	FOUNDRY_BIN="$(FOUNDRY_C) $(FOUNDRY_PORTS)" FOUNDRY_FILES="$(FOUNDRY_FILES)" scripts/compare-image.sh $(IMAGE) $(KTARGET)/disk.img $(OUT) +# rbsh's parser is pure Rust, so it is also tested on the host.+rbsh-test:+	mkdir -p $(OUT)/host-tests+	rustc --edition 2024 --test rust/rbsh/host-tests/parse_test.rs -o $(OUT)/host-tests/parse_test+	$(OUT)/host-tests/parse_test+ # K1 (kernel 0.21.0+): the kernel's own build takes our image as disk 0. # Uses its own target dir, so out/kernel-target (the reference) is untouched. k1-check: image kernel-src
@@ -0,0 +1,53 @@+//! Host unit tests for rbsh's parser (rbsh/src/parse.rs is pure: no+//! syscalls). Run with `make rbsh-test` (plain `rustc --test`, no Cargo+//! target needed).+extern crate alloc;++#[path = "../src/parse.rs"]+mod parse;++use parse::*;++fn letters(table: &str) -> Vec<String> {+    assign_drives(table).iter().map(|d| format!("{}:{}", d.letter, d.path)).collect()+}++#[test]+fn contract_v1_drives() {+    // Ramfs root, our FAT32 disk and the kernel's three test disks.+    let t = "/ ramfs\n/tmp ramfs\n/mnt/disk0 fat32\n/mnt/disk1 ext2\n/mnt/disk2 exfat\n/mnt/disk3 btrfs\n";+    assert_eq!(letters(t), ["A:/", "B:/mnt/disk0", "C:/mnt/disk1", "D:/mnt/disk2", "E:/mnt/disk3"]);+}++#[test]+fn contract_v2_drives() {+    // ext2 root, FAT32 data disk, exFAT share (mounted last by the kernel).+    let t = "/ ext2\n/tmp ramfs\n/dev devfs\n/proc procfs\n/mnt/disk0 fat32\n/mnt/share exfat\n";+    assert_eq!(letters(t), ["A:/", "B:/mnt/disk0", "C:/mnt/share"]);+    let d = assign_drives(t);+    assert_eq!(d[0].fs, "ext2");+    assert_eq!(d[2].fs, "exfat");+}++#[test]+fn share_skips_c_for_more_disks() {+    let t = "/ ext2\n/mnt/disk0 fat32\n/mnt/disk1 ext2\n/mnt/disk2 btrfs\n/mnt/share exfat\n";+    assert_eq!(letters(t), ["A:/", "B:/mnt/disk0", "C:/mnt/share", "D:/mnt/disk1", "E:/mnt/disk2"]);+}++#[test]+fn share_without_disks_is_still_c() {+    assert_eq!(letters("/ ext2\n/mnt/share exfat\n"), ["A:/", "C:/mnt/share"]);+}++#[test]+fn translate_and_display_with_share() {+    let d = assign_drives("/ ext2\n/mnt/disk0 fat32\n/mnt/share exfat\n");+    assert_eq!(translate("C:/notes.txt", &d).as_deref(), Some("/mnt/share/notes.txt"));+    assert_eq!(translate("c:", &d).as_deref(), Some("/mnt/share"));+    assert_eq!(translate("B:bin", &d).as_deref(), Some("/mnt/disk0/bin"));+    assert_eq!(translate("A:/etc/motd", &d).as_deref(), Some("/etc/motd"));+    assert_eq!(display("/mnt/share/x", &d), "C:/x");+    assert_eq!(display("/mnt/disk0", &d), "B:/");+    assert_eq!(display("/etc", &d), "A:/etc");+}
@@ -1,4 +1,6 @@-//! sh -- the Rustbelt shell, a normal user program (/bin/sh on the disk).+//! rbsh -- the Foundry OS shell with DOS commands and drive letters, a+//! normal user program: /bin/rbsh on the root partition (boot contract v2;+//! /bin/sh is dash there), or /bin/sh on the FAT32 disk under contract v1. //! //!   sh               interactive: prompt, line editing, history //!   sh script.sh     run the commands in a file (echoing each one)@@ -59,7 +61,7 @@ Rustbelt shell commands   start prog args           run it in the background (or: prog args &)   a | b | c                 pipes;  > file  >> file  < file  redirection   Ctrl+C                    interrupt the running program (SIGINT); Ctrl+Z stops it-  A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt+  A: is /, B: the data disk, C: the share, D: ... more disks (see `drives`): cd B:, type C:/README.TXT ";  /// A background (or stopped) job: one pipeline, one process group.
@@ -161,25 +161,41 @@ 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).-/// 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 A: (A:/tmp).+///+/// * A: is always the root, `/`.+/// * C: is the share, `/mnt/share` (boot contract v2's exFAT partition),+///   whenever it is mounted.+/// * The disks (the other mounts under /mnt: /mnt/disk0, /mnt/disk1, ...)+///   take the free letters in mount order: B:, then D:, E:, ... (or B:, C:,+///   D:, ... when there is no share, as under contract v1).+///+/// Other mounts such as /tmp are reachable through A: (A:/tmp). pub fn assign_drives(mount_table: &str) -> Vec<Drive> {-    let mut drives = Vec::new();-    let root_fs = mount_table+    const SHARE: &str = "/mnt/share";+    let mounts: Vec<(&str, &str)> = mount_table         .lines()-        .find_map(|l| l.split_once(' ').filter(|(p, _)| *p == "/").map(|(_, fs)| fs))-        .unwrap_or("ramfs");-    drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs.trim()) });+        .filter_map(|l| l.split_once(' '))+        .map(|(p, fs)| (p, fs.trim()))+        .collect();+    let root_fs = mounts.iter().find(|(p, _)| *p == "/").map(|(_, fs)| *fs).unwrap_or("ramfs");+    let mut drives = Vec::new();+    drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs) });+    let share = mounts.iter().find(|(p, _)| *p == SHARE);+    if let Some((path, fs)) = share {+        drives.push(Drive { letter: 'C', path: String::from(*path), fs: String::from(*fs) });+    }     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' {-                drives.push(Drive { letter: letter as char, path: String::from(path), fs: String::from(fs.trim()) });-                letter += 1;-            }+    for (path, fs) in mounts.iter().filter(|(p, _)| p.starts_with("/mnt/") && *p != SHARE) {+        if share.is_some() && letter == b'C' {+            letter += 1; // C: is the share's+        }+        if letter > b'Z' {+            break;         }+        drives.push(Drive { letter: letter as char, path: String::from(*path), fs: String::from(*fs) });+        letter += 1;     }+    drives.sort_by_key(|d| d.letter); // `drives` lists them A:, B:, C:, ...     drives } 
@@ -26,7 +26,10 @@ mod ulib;  use ulib::*; -const SHELL: &str = "/mnt/disk0/bin/sh";+/// The shell to start, first one that exists: rbsh on the root partition+/// (boot contract v2), else the FAT32 disk's /bin/sh (contract v1, where+/// that is rbsh).+const SHELLS: [(&str, &str); 2] = [("/bin/rbsh", "rbsh"), ("/mnt/disk0/bin/sh", "sh")];  /// The child's side (steps 2-5). Only returns on failure. fn become_shell(dev: &str) -> i64 {@@ -44,8 +47,10 @@ fn become_shell(dev: &str) -> i64 {     }     let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n";     write(1, greeting.as_bytes());-    let err = execve(SHELL, &["sh"], &environ());-    eprintln!("getty: {}: {}", SHELL, strerror(err));+    let mut st = Stat::default();+    let (path, argv0) = SHELLS.iter().copied().find(|(p, _)| stat(p, &mut st) == 0).unwrap_or(SHELLS[1]);+    let err = execve(path, &[argv0], &environ());+    eprintln!("getty: {}: {}", path, strerror(err));     127 }