foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 11:08:48 -0700 |
|---|---|---|
| commit | e8a1598981b9d4c220ff475716dda5dd7df785f3 | |
| parent | 618ed3f823d916066aae2a189d31e57ee4314adb | |
| tree | browse files at this commit | |
WIP 15.1: Linux C-string path syscalls, struct stat, *at() calls, rename, pathtest
| M | cp.rs | +2 -2 |
| M | demo.rs | +4 -4 |
| M | fetch.rs | +1 -1 |
| M | hello.rs | +1 -1 |
| M | httpd.rs | +1 -1 |
| M | ls.rs | +5 -5 |
| A | pathtest.rs | +177 -0 |
| M | sh.rs | +3 -4 |
| M | touch.rs | +1 -1 |
| M | ulib.rs | +127 -29 |
10 files changed, 322 insertions, 48 deletions
@@ -19,7 +19,7 @@ fn main() -> i64 { }; let mut st = Stat::default(); let mut dst = String::from(dst);- if stat(&dst, &mut st) == 0 && st.kind == KIND_DIR {+ if stat(&dst, &mut st) == 0 && st.is_dir() { let name = src.rsplit('/').next().unwrap_or(src); dst = format!("{}/{}", dst.trim_end_matches('/'), name); }@@ -28,7 +28,7 @@ fn main() -> i64 { eprintln!("cp: {}: {}", src, strerror(from)); return 1; }- let to = open(&dst, O_CREAT | O_TRUNC);+ let to = open(&dst, O_WRONLY | O_CREAT | O_TRUNC); if to < 0 { eprintln!("cp: {}: {}", dst, strerror(to)); close(from);
@@ -46,9 +46,9 @@ fn main() -> i64 { // --- stat / fstat / lseek ------------------------------------------------ let mut st = Stat::default();- check(stat("readme.txt", &mut st) == 0 && st.kind == KIND_FILE && st.size == n as u64, "stat readme.txt");- check(fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE, "fstat stdout = console");- check(stat("bin", &mut st) == 0 && st.kind == KIND_DIR, "stat bin is a directory");+ check(stat("readme.txt", &mut st) == 0 && st.is_file() && st.size == n as i64, "stat readme.txt");+ check(fstat(1, &mut st) == 0 && st.is_char_device() && isatty(1), "fstat stdout = a terminal");+ check(stat("bin", &mut st) == 0 && st.is_dir(), "stat bin is a directory"); check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET"); let mut word = [0u8; 4]; check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");@@ -87,7 +87,7 @@ fn main() -> i64 { check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST"); let mut file_buf = [0u8; 64]; let file = join(&mut file_buf, dir, "/f.txt");- let fd = open(file, O_CREAT);+ let fd = open(file, O_WRONLY | O_CREAT); check(write(fd, b"bytes") == 5, "write new file"); close(fd); check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");
@@ -97,7 +97,7 @@ fn main() -> i64 { // ...then the body, to stdout or the file, until the server closes. let out = match out_path {- Some(p) => ulib::open(p, ulib::O_CREAT | ulib::O_TRUNC),+ Some(p) => ulib::open(p, ulib::O_WRONLY | ulib::O_CREAT | ulib::O_TRUNC), None => 1, }; if out < 0 {
@@ -32,7 +32,7 @@ fn main() -> i64 { check(&mut failures, close(fd) == 0, "close"); // Create a file on the second mount (/tmp), then read it back.- let fd = open("/tmp/note.txt", O_CREAT);+ let fd = open("/tmp/note.txt", O_WRONLY | O_CREAT); check(&mut failures, write(fd, b"written from ring 3") == 19, "write /tmp/note.txt"); close(fd); let fd = open("/tmp/note.txt", 0);
@@ -109,7 +109,7 @@ fn respond(conn: i64, status: &'static str, body: &[u8]) -> (&'static str, usize fn send_file(conn: i64, path: &str) -> (&'static str, usize) { let mut st = ulib::Stat::default();- if ulib::stat(path, &mut st) < 0 || st.kind != ulib::KIND_FILE {+ if ulib::stat(path, &mut st) < 0 || !st.is_file() { return respond(conn, "404 Not Found", b"not found\n"); } let fd = ulib::open(path, 0);
@@ -41,7 +41,7 @@ fn main() -> i64 { paths.push("."); } let mut st = Stat::default();- let columns = !long && fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE;+ let columns = !long && isatty(1); let mut status = 0; for (i, path) in paths.iter().enumerate() {@@ -51,8 +51,8 @@ fn main() -> i64 { status = 1; continue; }- if st.kind != KIND_DIR {- let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.kind == KIND_DEVICE, size: st.size, mode: st.mode, target: String::new() };+ if !st.is_dir() {+ let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.is_char_device(), size: st.size as u64, mode: st.perm() as u64, target: String::new() }; print_entries(&[entry], long, columns); continue; }@@ -104,8 +104,8 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> { 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;+ e.size = st.size as u64;+ e.mode = st.perm() as u64; } } }
@@ -0,0 +1,177 @@+//! pathtest -- Linux's path syscalls: C strings, `struct stat`, *at() (stage 15.1).+//!+//! Since 15.1 every path argument is what C passes: a pointer to a+//! NUL-terminated string (before, our own programs passed pointer ++//! length, which no libc can). The kernel copies it in carefully, so:+//! - a NULL, kernel-space, or unmapped pointer is EFAULT (not a crash),+//! also when the string runs off the end of a mapped page;+//! - more than PATH_MAX (4096) bytes without a NUL is ENAMETOOLONG;+//! - "" is ENOENT.+//! Then: stat/fstat/lstat/newfstatat fill in Linux's 144-byte `struct+//! stat`, and the *at() calls take paths relative to a directory fd.+//! Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::string::String;+use alloc::vec;+use ulib::*;++/// A kernel address: user code may never pass one of these.+const KERNEL_PTR: u64 = 0xffff_8000_0000_0000;++fn sys(nr: u64, a: u64, b: u64, c: u64) -> i64 {+ syscall3(nr, a, b, c)+}++fn main() -> i64 {+ let mut c = Checks::new("pathtest");+ let mut st = Stat::default();+ let stp = &mut st as *mut Stat as u64;++ // --- bad pointers: EFAULT, from every path call ---------------------------+ for &(bad, what) in &[(0u64, "NULL"), (KERNEL_PTR, "a kernel address"), (0xf0_0000_0000, "an unmapped user address")] {+ let calls = [+ ("open", sys(SYS_OPEN, bad, 0, 0)),+ ("openat", syscall5(SYS_OPENAT, AT_FDCWD as u64, bad, 0, 0, 0)),+ ("stat", sys(SYS_STAT, bad, stp, 0)),+ ("lstat", sys(SYS_LSTAT, bad, stp, 0)),+ ("newfstatat", syscall5(SYS_NEWFSTATAT, AT_FDCWD as u64, bad, stp, 0, 0)),+ ("access", sys(SYS_ACCESS, bad, 0, 0)),+ ("mkdir", sys(SYS_MKDIR, bad, 0o755, 0)),+ ("rmdir", sys(SYS_RMDIR, bad, 0, 0)),+ ("unlink", sys(SYS_UNLINK, bad, 0, 0)),+ ("rename", sys(SYS_RENAME, bad, bad, 0)),+ ("chdir", sys(SYS_CHDIR, bad, 0, 0)),+ ("readlink", sys(SYS_READLINK, bad, stp, 64)),+ ("execve", sys(SYS_EXECVE, bad, 0, 0)),+ ];+ let failed: alloc::vec::Vec<&str> = calls.iter().filter(|(_, r)| *r != -EFAULT).map(|(n, _)| *n).collect();+ if !failed.is_empty() {+ println!("pathtest: not EFAULT for {}: {:?}", what, failed);+ }+ c.check(failed.is_empty(), &alloc::format!("13 path calls with {} as the path: EFAULT", what));+ }+ // A path that runs off the end of a mapped page into an unmapped one:+ // the kernel must notice at the page boundary.+ let two = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+ munmap(two + 4096, 4096);+ let tail = two + 4096 - 8;+ unsafe { core::ptr::copy_nonoverlapping(b"/tmp/abc".as_ptr(), tail as *mut u8, 8) }; // no NUL+ c.check(sys(SYS_STAT, tail, stp, 0) == -EFAULT, "a path running into an unmapped page: EFAULT");+ unsafe { *((two + 4095) as *mut u8) = 0 }; // now it ends just in time+ c.check(sys(SYS_STAT, tail, stp, 0) == -ENOENT, "the same path ending right at the page end: ENOENT");+ munmap(two, 4096);+ // Good path, bad `struct stat` pointer.+ c.check(sys(SYS_STAT, c"/".as_ptr() as u64, KERNEL_PTR, 0) == -EFAULT, "stat into a kernel address: EFAULT");+ c.check(sys(SYS_FSTAT, 0, 0, 0) == -EFAULT, "fstat into NULL: EFAULT");++ // --- lengths ------------------------------------------------------------------+ let mut long = vec![b'a'; 5000]; // no NUL in the first 4096 bytes+ long.push(0);+ c.check(sys(SYS_STAT, long.as_ptr() as u64, stp, 0) == -ENAMETOOLONG, "no NUL within PATH_MAX: ENAMETOOLONG");+ // 4095 bytes + NUL is allowed (short components, so it's just missing).+ let mut max = String::new();+ while max.len() < 4095 {+ max.push_str("/d");+ }+ max.truncate(4095);+ c.check(stat(&max, &mut st) == -ENOENT, "a 4095-byte path is accepted (ENOENT)");+ c.check(sys(SYS_STAT, c"".as_ptr() as u64, stp, 0) == -ENOENT, "an empty path: ENOENT");++ // --- Linux's struct stat ----------------------------------------------------+ c.check(core::mem::size_of::<Stat>() == 144, "struct stat is 144 bytes");+ c.check(stat("/mnt/disk0/bin", &mut st) == 0 && st.is_dir() && st.nlink == 2, "a directory: S_IFDIR, 2 links");+ let r = stat("/mnt/disk0/bin/pathtest", &mut st);+ let ino = st.ino;+ c.check(+ r == 0 && st.is_file() && st.size > 0 && st.blksize == 4096 && st.blocks == (st.size + 511) / 512 && st.ino != 0,+ "a file: S_IFREG, size, blksize 4096, 512-byte blocks, an inode number",+ );+ c.check(st.uid == 0 && st.gid == 0 && st.perm() != 0, "owned by root, with permission bits");+ let fd = open("/mnt/disk0/bin/pathtest", O_RDONLY);+ let mut st2 = Stat::default();+ c.check(fstat(fd, &mut st2) == 0 && st2.ino == ino && st2.size == st.size, "fstat on it: same inode and size");+ let mut st3 = Stat::default();+ c.check(fstatat(fd, "", &mut st3, AT_EMPTY_PATH) == 0 && st3.ino == ino, "newfstatat(fd, \"\", AT_EMPTY_PATH) = fstat");+ c.check(fstatat(fd, "x", &mut st3, 0) == -ENOTDIR, "a file as dirfd: ENOTDIR");+ close(fd);+ c.check(fstat(0, &mut st) == 0 && st.is_char_device() && isatty(0), "stdin: a character device and a tty");+ let (r, w) = pipe().unwrap();+ c.check(fstat(r, &mut st) == 0 && st.file_type() == S_IFIFO, "a pipe: S_IFIFO");+ close(r);+ close(w);++ // Symlinks: stat follows, lstat doesn't (on the ext2 disk, if it's there).+ let link = (0..4).map(|i| alloc::format!("/mnt/disk{}/hello-link", i)).find(|p| lstat(p, &mut st) == 0);+ if let Some(link) = link {+ c.check(lstat(&link, &mut st) == 0 && st.is_symlink() && st.size == 9, "lstat on a symlink: S_IFLNK, size = target length");+ c.check(stat(&link, &mut st) == 0 && st.is_file(), "stat on it: the file it points to");+ let mut buf = [0u8; 64];+ let n = readlink(&link, &mut buf);+ c.check(n == 9 && &buf[..9] == b"hello.txt", "readlink: the target, no NUL");+ let dir = &link[..link.rfind('/').unwrap()];+ let dfd = open(dir, O_RDONLY | O_DIRECTORY);+ let n = syscall5(SYS_READLINKAT, dfd as u64, c"hello-link".as_ptr() as u64, buf.as_mut_ptr() as u64, 64, 0);+ c.check(n == 9, "readlinkat(dirfd, \"hello-link\")");+ c.check(fstatat(dfd, "hello-link", &mut st, AT_SYMLINK_NOFOLLOW) == 0 && st.is_symlink(), "newfstatat AT_SYMLINK_NOFOLLOW = lstat");+ close(dfd);+ } else {+ println!("pathtest: no ext2 disk with hello-link: skipped symlink checks");+ }+ c.check(readlink("/mnt/disk0/bin", &mut [0u8; 8]) == -EINVAL, "readlink on a non-link: EINVAL");++ // --- open flags ----------------------------------------------------------------+ c.check(open("/mnt/disk0/bin/pathtest", O_RDONLY | O_DIRECTORY) == -ENOTDIR, "O_DIRECTORY on a file: ENOTDIR");+ c.check(open("/tmp", O_WRONLY) == -EISDIR, "a directory opened for writing: EISDIR");+ let fd = open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL);+ c.check(fd >= 0 && open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL) == -EEXIST, "O_CREAT|O_EXCL: creates, then EEXIST");+ close(fd);+ c.check(access("/tmp/pt-excl") == 0 && access("/tmp/nope") == -ENOENT, "access: 0 / ENOENT");++ // --- *at() relative to a directory fd ------------------------------------------+ c.check(mkdir("/tmp/ptdir") == 0, "mkdir /tmp/ptdir");+ let dfd = open("/tmp/ptdir", O_RDONLY | O_DIRECTORY);+ c.check(dfd >= 0, "open a directory with O_DIRECTORY");+ let fd = openat(dfd, "inner.txt", O_WRONLY | O_CREAT);+ c.check(fd >= 0 && write(fd, b"12345") == 5, "openat(dirfd, \"inner.txt\", O_CREAT) + write");+ close(fd);+ c.check(stat("/tmp/ptdir/inner.txt", &mut st) == 0 && st.size == 5, "...it's in that directory");+ c.check(openat(AT_FDCWD, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "openat(AT_FDCWD, absolute)");+ c.check(openat(99, "inner.txt", O_RDONLY) == -EBADF, "openat with a bad dirfd: EBADF");+ c.check(openat(99, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "...unless the path is absolute");+ c.check(syscall5(SYS_MKDIRAT, dfd as u64, c"sub".as_ptr() as u64, 0o755, 0, 0) == 0, "mkdirat(dirfd, \"sub\")");+ c.check(sys(SYS_FACCESSAT, dfd as u64, c"sub".as_ptr() as u64, 0) == 0, "faccessat(dirfd, \"sub\")");+ c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, 0, 0, 0) == -EISDIR, "unlinkat on a directory without AT_REMOVEDIR: EISDIR");+ c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, AT_REMOVEDIR, 0, 0) == 0, "unlinkat(AT_REMOVEDIR)");+ let r = syscall5(SYS_RENAMEAT, dfd as u64, c"inner.txt".as_ptr() as u64, AT_FDCWD as u64, c"/tmp/renamed.txt".as_ptr() as u64, 0);+ c.check(r == 0 && access("/tmp/ptdir/inner.txt") == -ENOENT, "renameat(dirfd, \"inner.txt\", AT_FDCWD, \"/tmp/renamed.txt\")");+ // fchdir: cd to the directory we have open.+ c.check(sys(SYS_FCHDIR, dfd as u64, 0, 0) == 0 && stat("../renamed.txt", &mut st) == 0 && st.size == 5, "fchdir(dirfd), then a relative path");+ chdir("/");+ close(dfd);++ // --- rename -----------------------------------------------------------------------+ let fd = open("/tmp/pt-new", O_WRONLY | O_CREAT);+ write(fd, b"new contents");+ close(fd);+ c.check(rename("/tmp/pt-new", "/tmp/renamed.txt") == 0, "rename a file over another");+ c.check(stat("/tmp/renamed.txt", &mut st) == 0 && st.size == 12 && access("/tmp/pt-new") == -ENOENT, "...it replaced it");+ c.check(rename("/tmp/renamed.txt", "/mnt/disk0/x.txt") == -EXDEV, "rename across file systems: EXDEV");+ c.check(rename("/tmp/ptdir", "/tmp/ptdir2") == 0 && stat("/tmp/ptdir2", &mut st) == 0 && st.is_dir(), "rename an empty directory");+ c.check(rename("/tmp/ptdir2", "/tmp/ptdir2/in") == -EINVAL, "rename a directory into itself: EINVAL");+ c.check(rename("/tmp/nope", "/tmp/x") == -ENOENT, "rename a missing file: ENOENT");+ unlink("/tmp/renamed.txt");+ unlink("/tmp/pt-excl");+ rmdir("/tmp/ptdir2");++ // --- small things musl asks at startup ------------------------------------------+ c.check(sys(SYS_GETUID, 0, 0, 0) == 0, "getuid: 0 (root)");+ c.done()+}
@@ -117,8 +117,7 @@ fn main() -> i64 { } None => 0, };- let mut st = Stat::default();- let interactive = fd == 0 && fstat(0, &mut st) == 0 && st.kind == KIND_CONSOLE;+ let interactive = fd == 0 && isatty(0); let mut input = LineReader { fd, buf: Vec::new(), eof: false }; sh.refresh_drives();@@ -386,7 +385,7 @@ impl Shell { prev_pipe = r; } if let Some((file, append)) = &stage.stdout {- let flags = O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC };+ let flags = O_WRONLY | O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC }; stdout = open(file, flags); if stdout < 0 { eprintln!("sh: {}: {}", file, strerror(stdout));@@ -512,7 +511,7 @@ impl Shell { return Some(name.to_string()); } let mut st = Stat::default();- self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.kind == KIND_FILE)+ self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.is_file()) } /// Start one program: fork, then in the child set up its process
@@ -13,7 +13,7 @@ fn main() -> i64 { let mut status = 0; for i in 1..argc() { let path = arg(i).unwrap_or("");- let fd = open(path, O_CREAT); // no O_TRUNC: keep the contents+ let fd = open(path, O_WRONLY | O_CREAT); // no O_TRUNC: keep the contents if fd < 0 { eprintln!("touch: {}: {}", path, strerror(fd)); status = 1;
@@ -63,6 +63,25 @@ pub const SYS_UNLINK: u64 = 87; pub const SYS_READLINK: u64 = 89; pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500;+// Stage 15.1: more of Linux's path calls (all take C strings).+pub const SYS_LSTAT: u64 = 6;+pub const SYS_ACCESS: u64 = 21;+pub const SYS_RENAME: u64 = 82;+pub const SYS_FCHDIR: u64 = 81;+pub const SYS_GETUID: u64 = 102;+pub const SYS_OPENAT: u64 = 257;+pub const SYS_MKDIRAT: u64 = 258;+pub const SYS_NEWFSTATAT: u64 = 262;+pub const SYS_UNLINKAT: u64 = 263;+pub const SYS_RENAMEAT: u64 = 264;+pub const SYS_READLINKAT: u64 = 267;+pub const SYS_FACCESSAT: u64 = 269;+pub const AT_FDCWD: i64 = -100;+pub const AT_SYMLINK_NOFOLLOW: u64 = 0x100;+pub const AT_REMOVEDIR: u64 = 0x200;+pub const AT_EMPTY_PATH: u64 = 0x1000;+/// Longest path the kernel takes, counting the NUL.+pub const PATH_MAX: usize = 4096; pub const SYS_MOUNTS: u64 = 501; pub const SYS_SYSINFO: u64 = 502; pub const SYS_SOCKET: u64 = 41;@@ -78,7 +97,11 @@ pub const SYS_RESOLVE: u64 = 503; pub const SYS_NETINFO: u64 = 504; pub const SYS_GETRANDOM: u64 = 318; +pub const O_RDONLY: u64 = 0;+pub const O_WRONLY: u64 = 1; pub const O_CREAT: u64 = 0o100;+pub const O_EXCL: u64 = 0o200;+pub const O_DIRECTORY: u64 = 0o200000; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; pub const TTY_SET_RAW: u64 = 1;@@ -114,23 +137,63 @@ pub const EEXIST: i64 = 17; pub const ENOTDIR: i64 = 20; pub const EISDIR: i64 = 21; pub const ESPIPE: i64 = 29;+pub const EXDEV: i64 = 18;+pub const EINVAL: i64 = 22;+pub const ENAMETOOLONG: i64 = 36; pub const ENOTEMPTY: i64 = 39;+pub const ELOOP: i64 = 40; pub const ETIMEDOUT: i64 = 110; -/// Same layout as the kernel's `fs::file::Stat`.+/// Linux's `struct stat` on x86_64 (what stat/fstat/lstat fill in since+/// stage 15.1; the same 144 bytes a C program's <sys/stat.h> describes). #[repr(C)] #[derive(Default, Clone, Copy)] pub struct Stat {- pub kind: u64, // KIND_*- pub size: u64,- pub mode: u64, // permission bits like 0o644; 0 = the file system has none+ pub dev: u64,+ pub ino: u64,+ pub nlink: u64,+ pub mode: u32, // file type (S_IF*) | permission bits like 0o644+ pub uid: u32,+ pub gid: u32,+ _pad0: u32,+ pub rdev: u64,+ pub size: i64,+ pub blksize: i64,+ pub blocks: i64,+ pub atime: [i64; 2], // seconds, nanoseconds+ pub mtime: [i64; 2],+ pub ctime: [i64; 2],+ _unused: [i64; 3],+}+pub const S_IFMT: u32 = 0o170000; // the type bits of `mode`+pub const S_IFIFO: u32 = 0o010000;+pub const S_IFCHR: u32 = 0o020000; // terminals, /dev/null, /dev/fb0, ...+pub const S_IFDIR: u32 = 0o040000;+pub const S_IFREG: u32 = 0o100000;+pub const S_IFLNK: u32 = 0o120000;+pub const S_IFSOCK: u32 = 0o140000;++impl Stat {+ pub fn file_type(&self) -> u32 {+ self.mode & S_IFMT+ }+ pub fn is_dir(&self) -> bool {+ self.file_type() == S_IFDIR+ }+ pub fn is_file(&self) -> bool {+ self.file_type() == S_IFREG+ }+ pub fn is_symlink(&self) -> bool {+ self.file_type() == S_IFLNK+ }+ pub fn is_char_device(&self) -> bool {+ self.file_type() == S_IFCHR+ }+ /// Permission bits (0o755 & co.).+ pub fn perm(&self) -> u32 {+ self.mode & 0o7777+ } }-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;-pub const KIND_DEVICE: u64 = 7; // /dev/null, /dev/fb0, ... /// Same layout as the kernel's `syscall::SpawnOptions`. #[repr(C)]@@ -196,18 +259,51 @@ pub fn read(fd: i64, buf: &mut [u8]) -> i64 { pub fn write(fd: i64, buf: &[u8]) -> i64 { syscall3(SYS_WRITE, fd as u64, buf.as_ptr() as u64, buf.len() as u64) }+/// Call `f` with `path` as a C string (NUL-terminated), the way the+/// kernel's path calls want it (Linux's ABI). A Rust &str isn't one: it has+/// no NUL at the end, so we copy it into a buffer and add one. Too long+/// for the kernel: -ENAMETOOLONG; a NUL inside: -EINVAL (C can't say that).+pub fn with_cpath(path: &str, f: impl FnOnce(u64) -> i64) -> i64 {+ if path.len() >= PATH_MAX {+ return -ENAMETOOLONG;+ }+ if path.as_bytes().contains(&0) {+ return -EINVAL;+ }+ let mut c = alloc::vec::Vec::with_capacity(path.len() + 1);+ c.extend_from_slice(path.as_bytes());+ c.push(0);+ f(c.as_ptr() as u64)+}+/// open(path, flags, mode): new files get mode 0o644 (the kernel ignores it). pub fn open(path: &str, flags: u64) -> i64 {- syscall3(SYS_OPEN, path.as_ptr() as u64, path.len() as u64, flags)+ with_cpath(path, |p| syscall3(SYS_OPEN, p, flags, 0o644))+}+/// openat(dirfd, path, flags): a relative path starts at directory `dirfd`.+pub fn openat(dirfd: i64, path: &str, flags: u64) -> i64 {+ with_cpath(path, |p| syscall5(SYS_OPENAT, dirfd as u64, p, flags, 0o644, 0)) } pub fn close(fd: i64) -> i64 { syscall3(SYS_CLOSE, fd as u64, 0, 0) }+/// stat follows symlinks; lstat describes the link itself. pub fn stat(path: &str, st: &mut Stat) -> i64 {- syscall3(SYS_STAT, path.as_ptr() as u64, path.len() as u64, st as *mut Stat as u64)+ with_cpath(path, |p| syscall3(SYS_STAT, p, st as *mut Stat as u64, 0))+}+pub fn lstat(path: &str, st: &mut Stat) -> i64 {+ with_cpath(path, |p| syscall3(SYS_LSTAT, p, st as *mut Stat as u64, 0)) } pub fn fstat(fd: i64, st: &mut Stat) -> i64 { syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0) }+/// newfstatat(dirfd, path, st, flags): what musl's stat() really calls.+pub fn fstatat(dirfd: i64, path: &str, st: &mut Stat, flags: u64) -> i64 {+ with_cpath(path, |p| syscall5(SYS_NEWFSTATAT, dirfd as u64, p, st as *mut Stat as u64, flags, 0))+}+/// Is `fd` a terminal? (Like libc: does it answer the TCGETS ioctl?)+pub fn isatty(fd: i64) -> bool {+ tcgetattr(fd).is_ok()+} pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 { syscall3(SYS_LSEEK, fd as u64, offset as u64, whence) }@@ -326,11 +422,9 @@ impl CStrings { /// execve(path, argv, envp): only returns on failure (-errno). pub fn execve(path: &str, argv: &[&str], envp: &[&str]) -> i64 {- let mut cpath = alloc::vec::Vec::from(path.as_bytes());- cpath.push(0); let mut args = CStrings::new(argv.iter().copied()); let mut env = CStrings::new(envp.iter().copied());- syscall3(SYS_EXECVE, cpath.as_ptr() as u64, args.as_ptr() as u64, env.as_ptr() as u64)+ with_cpath(path, |p| syscall3(SYS_EXECVE, p, args.as_ptr() as u64, env.as_ptr() as u64)) } /// The same environment I got: for passing on to execve.@@ -602,20 +696,29 @@ pub fn getcwd(buf: &mut [u8]) -> i64 { if n > 0 { n - 1 } else { n } } pub fn chdir(path: &str) -> i64 {- syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)+ with_cpath(path, |p| syscall3(SYS_CHDIR, p, 0, 0)) }+/// mkdir(path, mode): 0o755 (the kernel ignores the mode for now). pub fn mkdir(path: &str) -> i64 {- syscall3(SYS_MKDIR, path.as_ptr() as u64, path.len() as u64, 0)+ with_cpath(path, |p| syscall3(SYS_MKDIR, p, 0o755, 0)) } pub fn rmdir(path: &str) -> i64 {- syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0)+ with_cpath(path, |p| syscall3(SYS_RMDIR, p, 0, 0)) }-/// The target of a symlink, into `buf`; returns its length or -errno.+/// The target of a symlink, into `buf` (no NUL added); 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)+ with_cpath(path, |p| syscall3(SYS_READLINK, p, buf.as_mut_ptr() as u64, buf.len() as u64)) } pub fn unlink(path: &str) -> i64 {- syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0)+ with_cpath(path, |p| syscall3(SYS_UNLINK, p, 0, 0))+}+/// rename(old, new): replaces a file at `new`. -EXDEV: copy + delete instead.+pub fn rename(old: &str, new: &str) -> i64 {+ with_cpath(old, |o| with_cpath(new, |n| syscall3(SYS_RENAME, o, n, 0)))+}+/// access(path, mode): 0 if it exists (we don't check permissions yet).+pub fn access(path: &str) -> i64 {+ with_cpath(path, |p| syscall3(SYS_ACCESS, p, 0, 0)) } pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 { syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)@@ -623,14 +726,9 @@ pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 { /// Start program `path` with arguments `argv` (NUL-separated, e.g. /// "child\0hi"). Returns the child's pid (or -errno). pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {- syscall5(- SYS_SPAWN,- path.as_ptr() as u64,- path.len() as u64,- argv.as_ptr() as u64,- argv.len() as u64,- opts as *const SpawnOptions as u64,- )+ with_cpath(path, |p| {+ syscall5(SYS_SPAWN, p, argv.as_ptr() as u64, argv.len() as u64, opts as *const SpawnOptions as u64, 0)+ }) } /// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,