RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorTyler Fowler <tyler@localhost>2026-10-30 01:52:32 -0700
commitf856a3aa4c884f7c0d06d17dfccaecef3b2dd9ec
parent642316995676e40b82513422d30d6645005ca7fd
treebrowse files at this commit
Stage 8: ext2 driver (0.8.0)

Hand-written ext2 (src/fs/ext2.rs, no crate) plugged into the VFS:
superblock, group descriptors, bitmaps, inodes with direct/single/
double/triple indirect blocks, directories with file types, fast and
slow symlinks; 1 KiB and 4 KiB blocks, revision 0 and 1. Writes:
create, write/append, truncate, mkdir, unlink, rmdir with bitmap
allocation and free counts in the superblock and group descriptors.

- VFS: symlink following (ELOOP after 8), lookup_nofollow, readlink;
  Stat gains mode; new readlink syscall (89); mount_disks probes ext2
  (magic 0xEF53) before FAT and mounts unpartitioned disks
- build.rs: mke2fs -d images from disk2/ (big.txt, sparse.bin,
  symlinks): ext2.img (raw, vdb -> /mnt/disk1 = C:), plus 4K-block
  and revision-0 images for tests
- ls -l shows permissions and symlink targets
- tests/ext2.rs (11 tests); scripts/ext2-check.sh runs Rustbelt on
  writable copies, then host e2fsck -fn and debugfs
- self-test and boot.png use drive C:; README ext2 section
Mls.rs+46 -8
Mulib.rs+11 -0

2 files changed, 57 insertions, 8 deletions

@@ -2,7 +2,9 @@ //! //! On the console the names are printed in columns; into a pipe or file //! one per line (like the real `ls`, so `ls | wc -l` counts files).-//! -l adds sizes, DOS style: `<DIR>` or the size in bytes.+//! -l adds sizes, DOS style: `<DIR>` or the size in bytes. On file systems+//! with Unix permissions (ext2) it also shows them, like `-rw-r--r--`, and+//! where symlinks point: `hello-link -> hello.txt`. #![no_std] #![no_main] @@ -18,7 +20,10 @@ use ulib::*; struct Entry {     name: String,     dir: bool,+    link: bool,     size: u64,+    mode: u64,           // 0 = no permissions on this file system+    target: String,      // symlinks (with -l): where they point }  fn main() -> i64 {@@ -46,7 +51,8 @@ fn main() -> i64 {             continue;         }         if st.kind != KIND_DIR {-            print_entries(&[Entry { name: String::from(*path), dir: false, size: st.size }], long, columns);+            let entry = Entry { name: String::from(*path), dir: false, link: false, size: st.size, mode: st.mode, target: String::new() };+            print_entries(&[entry], long, columns);             continue;         }         if paths.len() > 1 {@@ -79,7 +85,8 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {         }         for_each_dirent(&buf, n as usize, |name, d_type| {             if name != "." && name != ".." {-                entries.push(Entry { name: String::from(name), dir: d_type == 4, size: 0 });+                let (dir, link) = (d_type == 4, d_type == 10); // DT_DIR, DT_LNK+                entries.push(Entry { name: String::from(name), dir, link, size: 0, mode: 0, target: String::new() });             }         });     }@@ -87,9 +94,17 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {     entries.sort_by(|a, b| a.name.cmp(&b.name));     if long {         let mut st = Stat::default();-        for e in entries.iter_mut().filter(|e| !e.dir) {-            if stat(&format!("{}/{}", path, e.name), &mut st) == 0 {+        for e in entries.iter_mut() {+            let full = format!("{}/{}", path, e.name);+            if e.link {+                let mut buf = [0u8; 256];+                let n = readlink(&full, &mut buf);+                if n >= 0 {+                    e.target = String::from(core::str::from_utf8(&buf[..n as usize]).unwrap_or("?"));+                }+            } else if stat(&full, &mut st) == 0 {                 e.size = st.size;+                e.mode = st.mode;             }         }     }@@ -99,15 +114,23 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> { fn print_entries(entries: &[Entry], long: bool, columns: bool) {     if long {         for e in entries {+            // `drwxr-xr-x ` in front if the file system has permissions.+            let perms = if e.mode != 0 || e.link { format!("{} ", mode_string(e)) } else { String::new() };             if e.dir {-                println!("     <DIR>  {}/", e.name);+                println!("{}     <DIR>  {}/", perms, e.name);+            } else if e.link {+                println!("{}            {} -> {}", perms, e.name, e.target);             } else {-                println!("{:>10}  {}", e.size, e.name);+                println!("{}{:>10}  {}", perms, e.size, e.name);             }         }         return;     }-    let shown = |e: &Entry| if e.dir { format!("{}/", e.name) } else { e.name.clone() };+    let shown = |e: &Entry| match (e.dir, e.link) {+        (true, _) => format!("{}/", e.name),+        (_, true) => format!("{}@", e.name), // like `ls -F`+        _ => e.name.clone(),+    };     if !columns {         entries.iter().for_each(|e| println!("{}", shown(e)));         return;@@ -124,3 +147,18 @@ fn print_entries(entries: &[Entry], long: bool, columns: bool) {         }     } }++/// `-rw-r--r--` style: type, then rwx for owner, group and others.+fn mode_string(e: &Entry) -> String {+    if e.link {+        return String::from("lrwxrwxrwx"); // symlink permissions are never used+    }+    let mut s = String::from(if e.dir { "d" } else { "-" });+    for shift in [6, 3, 0] {+        let bits = (e.mode >> shift) & 7;+        s.push(if bits & 4 != 0 { 'r' } else { '-' });+        s.push(if bits & 2 != 0 { 'w' } else { '-' });+        s.push(if bits & 1 != 0 { 'x' } else { '-' });+    }+    s+}
@@ -37,6 +37,7 @@ pub const SYS_CHDIR: u64 = 80; pub const SYS_MKDIR: u64 = 83; pub const SYS_RMDIR: u64 = 84; pub const SYS_UNLINK: u64 = 87;+pub const SYS_READLINK: u64 = 89; pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500; pub const SYS_MOUNTS: u64 = 501;@@ -69,11 +70,13 @@ pub const ENOTEMPTY: i64 = 39; pub struct Stat {     pub kind: u64, // KIND_*     pub size: u64,+    pub mode: u64, // permission bits like 0o644; 0 = the file system has none } pub const KIND_FILE: u64 = 1; pub const KIND_DIR: u64 = 2; pub const KIND_CONSOLE: u64 = 3; pub const KIND_PIPE: u64 = 4;+pub const KIND_SYMLINK: u64 = 5;  /// Same layout as the kernel's `syscall::SpawnOptions`. #[repr(C)]@@ -209,6 +212,10 @@ pub fn mkdir(path: &str) -> i64 { pub fn rmdir(path: &str) -> i64 {     syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0) }+/// The target of a symlink, into `buf`; returns its length or -errno.+pub fn readlink(path: &str, buf: &mut [u8]) -> i64 {+    syscall5(SYS_READLINK, path.as_ptr() as u64, path.len() as u64, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+} pub fn unlink(path: &str) -> i64 {     syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0) }@@ -324,13 +331,17 @@ pub fn strerror(errno: i64) -> &'static str {         22 => "Invalid argument",         24 => "Too many open files",         25 => "Not a terminal",+        27 => "File too large",         28 => "No space left on device",         29 => "Illegal seek",+        30 => "Read-only file system",         32 => "Broken pipe",         34 => "Result too large",         36 => "File name too long",         38 => "Function not implemented",         39 => "Directory not empty",+        40 => "Too many levels of symbolic links",+        95 => "Operation not supported",         _ => "Unknown error",     } }