diff --git a/lib/dirs/dirs.ww b/lib/dirs/dirs.ww index 47f1d43d..ee17c779 100644 --- a/lib/dirs/dirs.ww +++ b/lib/dirs/dirs.ww @@ -114,7 +114,9 @@ fn view() str = { // behaviour Hare's lookup uses (`fmt::fatalf` on the HOME branch). // Centralised so both branches in [[lookup]] share the error path. fn ensure() void = { - match (os.mkdirs(&pathbuf[0], MODE_0755)) { + let pv: str; + pv.ptr = &pathbuf[0]; pv.len = pathlen; + match (os.mkdirs(pv, MODE_0755)) { case void => {}; case let _e: os.oserror => rtabort("dirs: mkdirs failed"); }; diff --git a/lib/os/os.ww b/lib/os/os.ww index 1aa65eb9..747ec8bd 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -87,6 +87,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -125,15 +155,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -193,26 +225,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -220,29 +263,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -250,8 +292,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -269,9 +311,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -544,27 +591,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -585,7 +636,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -598,9 +651,11 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; diff --git a/lib/os/stattest.ww b/lib/os/stattest.ww index 5ccf29b4..086811b7 100644 --- a/lib/os/stattest.ww +++ b/lib/os/stattest.ww @@ -56,7 +56,7 @@ fn istype(m: os.mode, t: os.mode) bool = { | os.stat_mask.SIZE | os.stat_mask.INODE | os.stat_mask.ATIME | os.stat_mask.MTIME | os.stat_mask.CTIME) as u32; - match (os.stat(&fi, p.ptr)) { + match (os.stat(&fi, p)) { case let e: os.oserror => fail(); case void => { if ((fi.mask as u32) != wantmask) { fail(); }; @@ -87,7 +87,7 @@ fn istype(m: os.mode, t: os.mode) bool = { @test fn test_stat_subdir() void = { let p = envpath("WW_TEST_STAT_SUBDIR"); let fi: os.filestat; - match (os.stat(&fi, p.ptr)) { + match (os.stat(&fi, p)) { case let e: os.oserror => fail(); case void => { if (!istype(fi.mode, os.mode.DIR)) { fail(); }; @@ -100,7 +100,7 @@ fn istype(m: os.mode, t: os.mode) bool = { @test fn test_stat_noent() void = { let p = envpath("WW_TEST_STAT_NOENT"); let fi: os.filestat; - match (os.stat(&fi, p.ptr)) { + match (os.stat(&fi, p)) { case void => fail(); case let e: os.oserror => { // ENOENT = 2 → raw errno is -2. @@ -117,7 +117,7 @@ fn istype(m: os.mode, t: os.mode) bool = { @test fn test_stat_symlink_follow() void = { let p = envpath("WW_TEST_STAT_SYMLINK"); let fi: os.filestat; - match (os.stat(&fi, p.ptr)) { + match (os.stat(&fi, p)) { case let e: os.oserror => fail(); case void => { if (!istype(fi.mode, os.mode.REG)) { fail(); }; @@ -129,7 +129,7 @@ fn istype(m: os.mode, t: os.mode) bool = { @test fn test_lstat_symlink_nofollow() void = { let p = envpath("WW_TEST_STAT_SYMLINK"); let fi: os.filestat; - match (os.lstat(&fi, p.ptr)) { + match (os.lstat(&fi, p)) { case let e: os.oserror => fail(); case void => { if (!istype(fi.mode, os.mode.LINK)) { fail(); }; @@ -141,7 +141,7 @@ fn istype(m: os.mode, t: os.mode) bool = { @test fn test_fstat_regfile() void = { let p = envpath("WW_TEST_STAT_REGFILE"); - let fd: i32 = os.open(p.ptr, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(p, os.flag.RDONLY, 0i32); if (fd < 0) { fail(); }; let fi: os.filestat; match (os.fstat(&fi, fd)) { @@ -158,17 +158,53 @@ fn istype(m: os.mode, t: os.mode) bool = { @test fn test_exists_regfile() void = { let p = envpath("WW_TEST_STAT_REGFILE"); - if (!os.exists(p.ptr)) { fail(); }; + if (!os.exists(p)) { fail(); }; }; @test fn test_exists_subdir() void = { let p = envpath("WW_TEST_STAT_SUBDIR"); - if (!os.exists(p.ptr)) { fail(); }; + if (!os.exists(p)) { fail(); }; }; @test fn test_exists_noent() void = { let p = envpath("WW_TEST_STAT_NOENT"); - if (os.exists(p.ptr)) { fail(); }; + if (os.exists(p)) { fail(); }; +}; + +// ---- ENAMETOOLONG: kpath rejects paths >= PATH_MAX ------------------- +// +// kpath copies into a single [PATH_MAX]u8 buffer and reserves one byte +// for the NUL terminator (`p.len + 1 >= PATH_MAX` → reject). The +// rejection surfaces as `oserror = -36` (ENAMETOOLONG) on (... | +// oserror)-returning wrappers, and as `false` on [[os.exists]] +// (Hare's os::exists doc: "true if a node exists at the given path, +// or false if not."). + +let bigbuf: [4200]u8; + +fn makebig(n: i32) str = { + let i: i32 = 0; + for (i < n) { bigbuf[i] = 97u8; i += 1; }; // 'a' + let r: str; + r.ptr = &bigbuf[0]; + r.len = n; + return r; +}; + +@test fn test_stat_toolong() void = { + let bp = makebig(os.PATH_MAX); + let fi: os.filestat; + match (os.stat(&fi, bp)) { + case void => fail(); + case let e: os.oserror => { + if ((e: i64) != -36i64) { fail(); }; // ENAMETOOLONG + }; + }; +}; + +@test fn test_exists_toolong() void = { + let bp = makebig(os.PATH_MAX); + if (os.exists(bp)) { fail(); }; }; export fn main() i32 = { @@ -181,5 +217,7 @@ export fn main() i32 = { signalled = 7; test_exists_regfile(); signalled = 8; test_exists_subdir(); signalled = 9; test_exists_noent(); + signalled = 10; test_stat_toolong(); + signalled = 11; test_exists_toolong(); return 0; }; diff --git a/lib/temp/temp.ww b/lib/temp/temp.ww index 7a1a3064..31bf1d64 100644 --- a/lib/temp/temp.ww +++ b/lib/temp/temp.ww @@ -186,7 +186,9 @@ export fn named(outfd: *i32, outpath: *str, }; for (true) { pathlen = makenamed(dir); - let fd: i32 = os.open(&pathbuf[0], flags, perm); + let bs: str; + bs.ptr = &pathbuf[0]; bs.len = pathlen; + let fd: i32 = os.open(bs, flags, perm); if (fd >= 0) { *outfd = fd; let p: str; @@ -227,7 +229,9 @@ export fn file(iomode: mode, perm: i32) (i32 | os.oserror) = { export fn dir() str = { for (true) { pathlen = makedir(); - let r: i32 = os.mkdir(&pathbuf[0], 448i32); // 0o700 + let bs: str; + bs.ptr = &pathbuf[0]; bs.len = pathlen; + let r: i32 = os.mkdir(bs, 448i32); // 0o700 if (r >= 0) { let p: str; p.ptr = &pathbuf[0]; diff --git a/lib/temp/temptest.ww b/lib/temp/temptest.ww index d1720a47..cbd262e3 100644 --- a/lib/temp/temptest.ww +++ b/lib/temp/temptest.ww @@ -94,13 +94,13 @@ fn streq(a: str, b: str) bool = { }; // File exists pre-cleanup. - if (os.access(p.ptr, 0i32) != 0) { fail(); }; + if (os.access(p, 0i32) != 0) { fail(); }; if (os.close(fd) != 0) { fail(); }; - if (os.remove(p.ptr) != 0) { fail(); }; + if (os.remove(p) != 0) { fail(); }; // Cleanup landed. - if (os.access(p.ptr, 0i32) == 0) { fail(); }; + if (os.access(p, 0i32) == 0) { fail(); }; i += 1; }; @@ -156,10 +156,10 @@ fn strslice(p: str, lo: i32, hi: i32) str = { if (fd1 == fd2) { fail(); }; if (os.close(fd2) != 0) { fail(); }; - if (os.remove(p2.ptr) != 0) { fail(); }; + if (os.remove(p2) != 0) { fail(); }; if (os.close(fd1) != 0) { fail(); }; - if (os.remove(&snap[0]) != 0) { fail(); }; + if (os.remove(psnap) != 0) { fail(); }; }; // NOTE: temp.file() has no test of its own. The function is a thin @@ -187,17 +187,20 @@ fn strslice(p: str, lo: i32, hi: i32) str = { if (d.ptr[d.len] != 0u8) { fail(); }; // Dir exists. - if (os.access(d.ptr, 0i32) != 0) { fail(); }; + if (os.access(d, 0i32) != 0) { fail(); }; // Snapshot the dir path into a local NUL-terminated buffer: - // we'll need it after os.open() (the child create) leaves - // the buffer alone, but it's good hygiene given future - // helpers might share pathbuf. + // the os.* path entrypoints now copy through lib/os.pathbuf + // (kpath), so d's view into temp.pathbuf is safe; the + // snapshot still buys robustness against future helpers + // that might share temp's buffer. let dsnap: [128]u8; let dlen: i32 = d.len; let s: i32 = 0; for (s < d.len) { dsnap[s] = d[s]; s += 1; }; dsnap[d.len] = 0u8; + let dview: str; + dview.ptr = &dsnap[0]; dview.len = dlen; if (childn[i] > 0) { // Build "/x\0" in a local buffer. @@ -208,23 +211,25 @@ fn strslice(p: str, lo: i32, hi: i32) str = { cbuf[off] = 47u8; off += 1; // '/' cbuf[off] = 120u8; off += 1; // 'x' cbuf[off] = 0u8; + let cview: str; + cview.ptr = &cbuf[0]; cview.len = off; let cflags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL; - let fd: i32 = os.open(&cbuf[0], cflags, 384i32); + let fd: i32 = os.open(cview, cflags, 384i32); if (fd < 0) { fail(); }; let payload: [3]u8; payload[0] = 88u8; payload[1] = 89u8; payload[2] = 90u8; // "XYZ" let wr: i64 = os.write(fd, &payload[0], 3u64); if (wr != 3i64) { fail(); }; if (os.close(fd) != 0) { fail(); }; - if (os.remove(&cbuf[0]) != 0) { fail(); }; + if (os.remove(cview) != 0) { fail(); }; }; - // Rmdir uses dsnap (more robust if the static buffer were + // Rmdir uses dview (more robust if the static buffer were // touched between dir() and here). - if (os.rmdir(&dsnap[0]) != 0) { fail(); }; + if (os.rmdir(dview) != 0) { fail(); }; // Cleanup landed. - if (os.access(&dsnap[0], 0i32) == 0) { fail(); }; + if (os.access(dview, 0i32) == 0) { fail(); }; i += 1; }; @@ -249,8 +254,8 @@ fn strslice(p: str, lo: i32, hi: i32) str = { if (streq(strslice(d2, 0, d2.len), dsnap)) { fail(); }; // Cleanup both, using snap for d1's old contents. - if (os.rmdir(d2.ptr) != 0) { fail(); }; - if (os.rmdir(&snap[0]) != 0) { fail(); }; + if (os.rmdir(d2) != 0) { fail(); }; + if (os.rmdir(dsnap) != 0) { fail(); }; }; export fn main() i32 = { diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index 31b76d95..2e7432a6 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -88,6 +88,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -126,15 +156,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -194,26 +226,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -221,29 +264,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -251,8 +293,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -270,9 +312,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -545,27 +592,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -586,7 +637,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -599,10 +652,12 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; @@ -2896,9 +2951,18 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. Bridges argv/arena +// callers to lib/os entrypoints (str post-task-#23). +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + // Slurp the whole file into a fresh buffer. fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -2974,7 +3038,7 @@ export fn main(argc: i32, argv: **u8) i32 = { if (encode(&s) != 0) { return 1; }; // Open output for write. - let fd: i32 = os.open(out, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + let fd: i32 = os.open(pathstr(out), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (fd < 0) { os.write(2, "w6a: cannot open output\n".ptr, 23u64); return 1; diff --git a/selfhost/cmd/w6a/main.ww b/selfhost/cmd/w6a/main.ww index 069c4ba8..216af99e 100644 --- a/selfhost/cmd/w6a/main.ww +++ b/selfhost/cmd/w6a/main.ww @@ -30,9 +30,18 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. Bridges argv/arena +// callers to lib/os entrypoints (str post-task-#23). +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + // Slurp the whole file into a fresh buffer. fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -108,7 +117,7 @@ export fn main(argc: i32, argv: **u8) i32 = { if (encode(&s) != 0) { return 1; }; // Open output for write. - let fd: i32 = os.open(out, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + let fd: i32 = os.open(pathstr(out), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (fd < 0) { os.write(2, "w6a: cannot open output\n".ptr, 23u64); return 1; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1d9259aa..3dacad07 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -88,6 +88,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -126,15 +156,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -194,26 +226,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -221,29 +264,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -251,8 +293,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -270,9 +312,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -545,27 +592,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -586,7 +637,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -599,10 +652,12 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; @@ -19129,8 +19184,18 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. lib/os entrypoints +// take str post-task-#23; this bridges call sites that still hold +// C-string paths (argv entries, arena-allocated buffers). +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -19196,7 +19261,7 @@ export fn main(argc: i32, argv: **u8) i32 = { // cgen.ww writes directly to fd 1; dup2 lets us reuse it without // threading a file descriptor through the emit helpers. if (out != nil) { - let ofd: i32 = os.open(out, + let ofd: i32 = os.open(pathstr(out), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (ofd < 0) { os.write(2, "w6c: cannot open output\n".ptr, 23u64); diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index c2b83a2a..97b97e8c 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -39,8 +39,18 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. lib/os entrypoints +// take str post-task-#23; this bridges call sites that still hold +// C-string paths (argv entries, arena-allocated buffers). +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -106,7 +116,7 @@ export fn main(argc: i32, argv: **u8) i32 = { // cgen.ww writes directly to fd 1; dup2 lets us reuse it without // threading a file descriptor through the emit helpers. if (out != nil) { - let ofd: i32 = os.open(out, + let ofd: i32 = os.open(pathstr(out), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (ofd < 0) { os.write(2, "w6c: cannot open output\n".ptr, 23u64); diff --git a/selfhost/cmd/w6l/dyn.ww b/selfhost/cmd/w6l/dyn.ww index 17c7926b..cd5fa932 100644 --- a/selfhost/cmd/w6l/dyn.ww +++ b/selfhost/cmd/w6l/dyn.ww @@ -124,7 +124,7 @@ fn dbasename(p: *u8) *u8 = { // ---- file slurp -------------------------------------------------------- fn slurpso(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index ddb65297..40947784 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -88,6 +88,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -126,15 +156,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -194,26 +226,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -221,29 +264,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -251,8 +293,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -270,9 +312,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -545,27 +592,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -586,7 +637,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -599,10 +652,12 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; @@ -906,7 +961,7 @@ def RELA_ADDEND: u64 = 16u64; // ---- file slurp -------------------------------------------------------- fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -983,6 +1038,16 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. Bridges argv/arena +// callers to lib/os entrypoints (str post-task-#23). Shared with +// main.ww and dyn.ww via the w6l bundle. +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + fn cstreq(p: *u8, lit: str) bool = { let n: u64 = lit.len: u64; let i: u64 = 0u64; @@ -1556,7 +1621,7 @@ fn dbasename(p: *u8) *u8 = { // ---- file slurp -------------------------------------------------------- fn slurpso(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -3010,7 +3075,7 @@ fn buildpathv(dst: *u8, dir: *u8, name: *u8, v: u64) u64 = { // islinkable: read first 8 bytes; require !\n or \x7fELF. fn islinkable(path: *u8) bool = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return false; }; let mp: *u8 = os.alloc(8u64): *u8; let n: i64 = os.read(fd, mp, 8u64); @@ -3079,7 +3144,7 @@ fn resolvelib(a: *arena, name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = { // Read first 20 bytes; return 1 for ET_DYN .so, 0 for ar/.o. fn isso(path: *u8) i32 = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return 0; }; let mp: *u8 = os.alloc(20u64): *u8; let n: i64 = os.read(fd, mp, 20u64); @@ -3226,7 +3291,7 @@ export fn main(argc: i32, argv: **u8) i32 = { }; let flags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC; - let fd: i32 = os.open(outpath, flags, 493i32); // 0o755 + let fd: i32 = os.open(pathstr(outpath), flags, 493i32); // 0o755 if (fd < 0) { os.write(2, "w6l: cannot open output\n".ptr, 23u64); return 1; diff --git a/selfhost/cmd/w6l/main.ww b/selfhost/cmd/w6l/main.ww index eacc73ec..85ee43e9 100644 --- a/selfhost/cmd/w6l/main.ww +++ b/selfhost/cmd/w6l/main.ww @@ -99,7 +99,7 @@ fn buildpathv(dst: *u8, dir: *u8, name: *u8, v: u64) u64 = { // islinkable: read first 8 bytes; require !\n or \x7fELF. fn islinkable(path: *u8) bool = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return false; }; let mp: *u8 = os.alloc(8u64): *u8; let n: i64 = os.read(fd, mp, 8u64); @@ -168,7 +168,7 @@ fn resolvelib(a: *arena, name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = { // Read first 20 bytes; return 1 for ET_DYN .so, 0 for ar/.o. fn isso(path: *u8) i32 = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return 0; }; let mp: *u8 = os.alloc(20u64): *u8; let n: i64 = os.read(fd, mp, 20u64); @@ -315,7 +315,7 @@ export fn main(argc: i32, argv: **u8) i32 = { }; let flags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC; - let fd: i32 = os.open(outpath, flags, 493i32); // 0o755 + let fd: i32 = os.open(pathstr(outpath), flags, 493i32); // 0o755 if (fd < 0) { os.write(2, "w6l: cannot open output\n".ptr, 23u64); return 1; diff --git a/selfhost/cmd/w6l/obj.ww b/selfhost/cmd/w6l/obj.ww index cfabdef4..edc0995e 100644 --- a/selfhost/cmd/w6l/obj.ww +++ b/selfhost/cmd/w6l/obj.ww @@ -76,7 +76,7 @@ def RELA_ADDEND: u64 = 16u64; // ---- file slurp -------------------------------------------------------- fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -153,6 +153,16 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. Bridges argv/arena +// callers to lib/os entrypoints (str post-task-#23). Shared with +// main.ww and dyn.ww via the w6l bundle. +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + fn cstreq(p: *u8, lit: str) bool = { let n: u64 = lit.len: u64; let i: u64 = 0u64; diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 303a3b6e..ad02468e 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -88,6 +88,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -126,15 +156,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -194,26 +226,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -221,29 +264,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -251,8 +293,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -270,9 +312,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -545,27 +592,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -586,7 +637,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -599,10 +652,12 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; @@ -744,6 +799,16 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. Bridges the +// driver's argv/arena *u8 paths to lib/os entrypoints (str +// post-task-#23). +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + fn cstreq(a: *u8, b: *u8) bool = { let i: u64 = 0u64; for (a[i] == b[i]) { @@ -873,7 +938,7 @@ fn procrun(path: *u8, argv: **u8) i32 = { return -1; }; if (pid == 0) { - os.execve(path, argv, nil: **u8); + os.execve(pathstr(path), argv, nil: **u8); os.write(2, "ww: execve failed\n".ptr, 18u64); os.exit(127); }; @@ -951,7 +1016,7 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = { buf[off] = 119u8; off += 1u64; // 'w' buf[off] = 119u8; off += 1u64; // 'w' buf[off] = 0u8; - if (os.access(buf, 0i32) == 0) { return buf; }; + if (os.access(pathstr(buf), 0i32) == 0) { return buf; }; // candidate 2: //.ww let buf2: *u8 = amalloc(a, PATH_MAX): *u8; @@ -971,7 +1036,7 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = { buf2[off] = 119u8; off += 1u64; buf2[off] = 119u8; off += 1u64; buf2[off] = 0u8; - if (os.access(buf2, 0i32) == 0) { return buf2; }; + if (os.access(pathstr(buf2), 0i32) == 0) { return buf2; }; return nil; }; @@ -998,7 +1063,7 @@ fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = { // ---- file slurp ------------------------------------------------------- fn slurp(pathcs: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathcs, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -1283,7 +1348,7 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = { }; // Step 1: expand `use`s into the combined file. - let cf: i32 = os.open(combined, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (cf < 0) { os.write(2, "ww: cannot open combined\n".ptr, 25u64); return 1; @@ -1461,7 +1526,7 @@ fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = { // (1) literal .ww file that exists if (cstrendswithlit(name, ".ww")) { - if (os.access(name, 0i32) == 0) { + if (os.access(pathstr(name), 0i32) == 0) { return arenadupcstr(a, name, nlen); }; }; @@ -1479,7 +1544,7 @@ fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = { dot[0] = 46u8; dot[1] = 0u8; let probe: *u8 = builddirmodulepath(a, dot, 1u64, cwd + bo, blen); - if (os.access(probe, 0i32) == 0) { return probe; }; + if (os.access(pathstr(probe), 0i32) == 0) { return probe; }; return nil; }; }; @@ -1488,7 +1553,7 @@ fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = { let bo: u64 = basenameoff(name, nlen); let probe: *u8 = builddirmodulepath(a, name, nlen, name + bo, nlen - bo); - if (os.access(probe, 0i32) == 0) { return probe; }; + if (os.access(pathstr(probe), 0i32) == 0) { return probe; }; // (4) search path lookup let search: *u8 = buildsearchpath(a, selfdir, incs); @@ -1779,7 +1844,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.libs = libs; lf.nlibs = nlibs; if (buildone(selfdir, resolved, tmp, incs, &lf) != 0) { - os.remove(tmp); + os.remove(pathstr(tmp)); return 1; }; @@ -1796,7 +1861,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; execargv[nextra + 1] = nil; let rc: i32 = procrun(tmp, execargv); - os.remove(tmp); + os.remove(pathstr(tmp)); return rc; }; @@ -1811,19 +1876,19 @@ fn runsingletest(selfdir: *u8, src: *u8) i32 = { let tmp: *u8 = os.alloc(PATH_MAX): *u8; makeruntmp(tmp); if (buildone(selfdir, src, tmp, "\0".ptr, nil) != 0) { - os.remove(tmp); + os.remove(pathstr(tmp)); return 1; }; let execargv: **u8 = os.alloc(16u64): **u8; execargv[0] = tmp; execargv[1] = nil; let rc: i32 = procrun(tmp, execargv); - os.remove(tmp); + os.remove(pathstr(tmp)); return rc; }; fn rundirtests(selfdir: *u8, dir: *u8) i32 = { - let fd: i32 = os.open(dir, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(dir), os.flag.RDONLY, 0i32); if (fd < 0) { os.write(2, "ww test: cannot open directory\n".ptr, 31u64); return 1; @@ -1883,7 +1948,7 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = { os.write(2, "\n".ptr, 1u64); }; }; - os.remove(tmp); + os.remove(pathstr(tmp)); }; off += reclen; }; @@ -1907,7 +1972,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // single-file mode: literal *.ww that exists if (cstrendswithlit(target, ".ww")) { - if (os.access(target, 0i32) == 0) { + if (os.access(pathstr(target), 0i32) == 0) { return runsingletest(selfdir, target); }; }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 06b25df3..88c6d9d1 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -27,6 +27,16 @@ fn cstrlen(p: *u8) u64 = { return n; }; +// pathstr — view a NUL-terminated *u8 as a str. Bridges the +// driver's argv/arena *u8 paths to lib/os entrypoints (str +// post-task-#23). +fn pathstr(p: *u8) str = { + let r: str; + r.ptr = p; + r.len = cstrlen(p): i32; + return r; +}; + fn cstreq(a: *u8, b: *u8) bool = { let i: u64 = 0u64; for (a[i] == b[i]) { @@ -156,7 +166,7 @@ fn procrun(path: *u8, argv: **u8) i32 = { return -1; }; if (pid == 0) { - os.execve(path, argv, nil: **u8); + os.execve(pathstr(path), argv, nil: **u8); os.write(2, "ww: execve failed\n".ptr, 18u64); os.exit(127); }; @@ -234,7 +244,7 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = { buf[off] = 119u8; off += 1u64; // 'w' buf[off] = 119u8; off += 1u64; // 'w' buf[off] = 0u8; - if (os.access(buf, 0i32) == 0) { return buf; }; + if (os.access(pathstr(buf), 0i32) == 0) { return buf; }; // candidate 2: //.ww let buf2: *u8 = amalloc(a, PATH_MAX): *u8; @@ -254,7 +264,7 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = { buf2[off] = 119u8; off += 1u64; buf2[off] = 119u8; off += 1u64; buf2[off] = 0u8; - if (os.access(buf2, 0i32) == 0) { return buf2; }; + if (os.access(pathstr(buf2), 0i32) == 0) { return buf2; }; return nil; }; @@ -281,7 +291,7 @@ fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = { // ---- file slurp ------------------------------------------------------- fn slurp(pathcs: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathcs, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32); if (fd < 0) { return nil, 0u64; }; let szr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = 0i64; @@ -566,7 +576,7 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = { }; // Step 1: expand `use`s into the combined file. - let cf: i32 = os.open(combined, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (cf < 0) { os.write(2, "ww: cannot open combined\n".ptr, 25u64); return 1; @@ -744,7 +754,7 @@ fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = { // (1) literal .ww file that exists if (cstrendswithlit(name, ".ww")) { - if (os.access(name, 0i32) == 0) { + if (os.access(pathstr(name), 0i32) == 0) { return arenadupcstr(a, name, nlen); }; }; @@ -762,7 +772,7 @@ fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = { dot[0] = 46u8; dot[1] = 0u8; let probe: *u8 = builddirmodulepath(a, dot, 1u64, cwd + bo, blen); - if (os.access(probe, 0i32) == 0) { return probe; }; + if (os.access(pathstr(probe), 0i32) == 0) { return probe; }; return nil; }; }; @@ -771,7 +781,7 @@ fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = { let bo: u64 = basenameoff(name, nlen); let probe: *u8 = builddirmodulepath(a, name, nlen, name + bo, nlen - bo); - if (os.access(probe, 0i32) == 0) { return probe; }; + if (os.access(pathstr(probe), 0i32) == 0) { return probe; }; // (4) search path lookup let search: *u8 = buildsearchpath(a, selfdir, incs); @@ -1062,7 +1072,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.libs = libs; lf.nlibs = nlibs; if (buildone(selfdir, resolved, tmp, incs, &lf) != 0) { - os.remove(tmp); + os.remove(pathstr(tmp)); return 1; }; @@ -1079,7 +1089,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; execargv[nextra + 1] = nil; let rc: i32 = procrun(tmp, execargv); - os.remove(tmp); + os.remove(pathstr(tmp)); return rc; }; @@ -1094,19 +1104,19 @@ fn runsingletest(selfdir: *u8, src: *u8) i32 = { let tmp: *u8 = os.alloc(PATH_MAX): *u8; makeruntmp(tmp); if (buildone(selfdir, src, tmp, "\0".ptr, nil) != 0) { - os.remove(tmp); + os.remove(pathstr(tmp)); return 1; }; let execargv: **u8 = os.alloc(16u64): **u8; execargv[0] = tmp; execargv[1] = nil; let rc: i32 = procrun(tmp, execargv); - os.remove(tmp); + os.remove(pathstr(tmp)); return rc; }; fn rundirtests(selfdir: *u8, dir: *u8) i32 = { - let fd: i32 = os.open(dir, os.flag.RDONLY, 0i32); + let fd: i32 = os.open(pathstr(dir), os.flag.RDONLY, 0i32); if (fd < 0) { os.write(2, "ww test: cannot open directory\n".ptr, 31u64); return 1; @@ -1166,7 +1176,7 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = { os.write(2, "\n".ptr, 1u64); }; }; - os.remove(tmp); + os.remove(pathstr(tmp)); }; off += reclen; }; @@ -1190,7 +1200,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // single-file mode: literal *.ww that exists if (cstrendswithlit(target, ".ww")) { - if (os.access(target, 0i32) == 0) { + if (os.access(pathstr(target), 0i32) == 0) { return runsingletest(selfdir, target); }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7aa24514..3bf48df5 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -88,6 +88,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -126,15 +156,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -194,26 +226,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -221,29 +264,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -251,8 +293,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -270,9 +312,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -545,27 +592,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -586,7 +637,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -599,10 +652,12 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; @@ -19164,7 +19219,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; - let fdorerr: (i32 | os.oserror) = os.tryopen(path, os.flag.RDONLY, 0i32); + let fdorerr: (i32 | os.oserror) = os.tryopen(argstr(path), os.flag.RDONLY, 0i32); let fd: i32 = -1; match (fdorerr) { case let v: i32 => fd = v; diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww index 0fcb205d..38332da1 100644 --- a/selfhost/cmd/wwdump/main.ww +++ b/selfhost/cmd/wwdump/main.ww @@ -74,7 +74,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; - let fdorerr: (i32 | os.oserror) = os.tryopen(path, os.flag.RDONLY, 0i32); + let fdorerr: (i32 | os.oserror) = os.tryopen(argstr(path), os.flag.RDONLY, 0i32); let fd: i32 = -1; match (fdorerr) { case let v: i32 => fd = v; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 73416fe7..424a51b0 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -88,6 +88,36 @@ export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; +// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ +// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | +// []u8 | *const u8)`; ww's lib/os public surface narrows to `str` +// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). +// Internally, [[kpath]] copies the `str` bytes into a single +// module-level [[pathbuf]] scratch slot and NUL-terminates so the +// raw Linux syscalls (which require C strings) see a valid +// terminator. Same precedent as Hare's static `pathbuf`. +// +// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. +// rewrites it. Same caveat as strconv's `*tos` family (overwritten +// on next call). Caller must NOT hold a kpath-returned pointer +// across another lib/os path call. Graduates when ww grows a +// thread story. +// +// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates +// 1-word-payload tagged returns to 24B (cstage emits 16B). +// Task #9; revert at task #10 when fixed. Repro at +// .ai/probe_tagged_return_pointer_payload.ww. +export def PATH_MAX: i32 = 4096; +let pathbuf: [4096]u8; + +fn kpath(p: str) *u8 = { + if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG + let i: i32 = 0; + for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; + pathbuf[p.len] = 0u8; + return &pathbuf[0]; +}; + // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. @@ -126,15 +156,17 @@ export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { return r; }; -// open — Linux open(2). Path must be NUL-terminated; callers using ww -// `str` must ensure the bytes are followed by a 0 byte (literals are, -// arena-copied paths usually are by construction). Returns -errno on -// failure, fd otherwise. Higher-level callers prefer `tryopen`. -export fn open(path: *u8, flags: flag, mode: i32) i32 = { - return syscall3(nr.OPEN, path: i64, (flags as i32): i64, mode: i64): i32; +// open — Linux open(2). Returns -errno on failure, fd otherwise. +// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open +// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn open(path: str, flags: flag, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG + return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; -export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = { +export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; @@ -194,26 +226,37 @@ export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). -export fn access(path: *u8, mode: i32) i32 = { - return syscall2(nr.ACCESS, path: i64, mode: i64): i32; +// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). +// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. +export fn access(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; -// remove — unlink(2). Hare name; the underlying syscall is unlink(2). -export fn remove(path: *u8) i32 = { - return syscall1(nr.UNLINK, path: i64): i32; +// remove — unlink(2). Mirrors Hare's os::remove +// (ref/hare/os/os.ha:12). +export fn remove(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.UNLINK, p: i64): i32; }; -// mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix -// permission bitset (e.g. 0o700). Returns 0 on success, negative -// errno otherwise. Hare name (os::mkdir). -export fn mkdir(path: *u8, mode: i32) i32 = { - return syscall2(nr.MKDIR, path: i64, mode: i64): i32; +// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). +// Returns 0 on success, negative errno otherwise. Mirrors Hare's +// os::mkdir (ref/hare/os/os.ha:50). +export fn mkdir(path: str, mode: i32) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; -// rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on -// success, negative errno otherwise. Hare name (os::rmdir). -export fn rmdir(path: *u8) i32 = { - return syscall1(nr.RMDIR, path: i64): i32; +// rmdir — rmdir(2). Mirrors Hare's os::rmdir +// (ref/hare/os/os.ha:58). +export fn rmdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.RMDIR, p: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent @@ -221,29 +264,28 @@ export fn rmdir(path: *u8) i32 = { // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // -// `path` must be NUL-terminated AND its bytes must be writable — -// mkdirs temporarily replaces '/' separators with NUL while -// invoking [[mkdir]] on each prefix, then restores them. Pointing -// `path` at a string literal will segfault. Callers hold the bytes -// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same -// precedent as [[temp.named]]'s pathbuf. -// -// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). -export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { - // Find the path length (excluding trailing NUL). - let n: i32 = 0; - for (path[n] != 0u8) { n += 1; }; +// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place +// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly +// instead of recursing through [[mkdir]] — re-entering kpath would +// clobber the buffer mid-walk (single static slot, see kpath's +// non-reentrancy note above). +export fn mkdirs(path: str, mode: i32) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; + let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, - // mkdir it, restore the slash, continue. Skip index 0 so a - // leading '/' on absolute paths doesn't trigger an empty mkdir. + // raw MKDIR syscall on pathbuf, restore the slash, continue. + // Skip index 0 so a leading '/' on absolute paths doesn't + // trigger an empty mkdir. let i: i32 = 1; for (i < n) { - if (path[i] == 47u8) { // '/' - path[i] = 0u8; - let r: i32 = mkdir(path, mode); - path[i] = 47u8; + if (pathbuf[i] == 47u8) { // '/' + pathbuf[i] = 0u8; + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; + pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -251,8 +293,8 @@ export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { i += 1; }; - // mkdir the full path. - let r: i32 = mkdir(path, mode); + let r: i32 = syscall2(nr.MKDIR, + (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; @@ -270,9 +312,14 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; -// execve(2): on success, does not return. -export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { - return syscall3(nr.EXECVE, path: i64, argv: i64, envp: i64): i32; +// execve(2): on success, does not return. Mirrors Hare's +// os::exec::exec path arg (str). argv/envp stay `**u8` — the +// kernel takes a NUL-pointer-terminated table of NUL-terminated +// C strings, a different shape from a path. +export fn execve(path: str, argv: **u8, envp: **u8) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in @@ -545,27 +592,31 @@ fn fillfilestat(out: *filestat, k: *kstat) void = { }; // stat — fill *out with metadata for `path`. Follows symlinks. -// `path` must be NUL-terminated (lib/os convention; see task #23 -// for a planned `path: str` migration). +// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows +// PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. -export fn stat(out: *filestat, path: *u8) (void | oserror) = { +export fn stat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). -export fn lstat(out: *filestat, path: *u8) (void | oserror) = { +export fn lstat(out: *filestat, path: str) (void | oserror) = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, + AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); @@ -586,7 +637,9 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are -// followed; a dangling symlink is `false`. +// followed; a dangling symlink is `false`. ENAMETOOLONG is +// swallowed as `false` — Hare's os::exists doc says "true if a +// node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same @@ -599,10 +652,12 @@ export fn fstat(out: *filestat, fd: i32) (void | oserror) = { // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. -export fn exists(path: *u8) bool = { +export fn exists(path: str) bool = { + let cp: *u8 = kpath(path); + if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, - AT_FDCWD: i64, path: i64, (&k): i64, 0i64); + AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; }; @@ -1513,7 +1568,7 @@ export fn main() i32 = { // that step. RDONLY is 0; passing the literal keeps the call // site standalone-compilable to byte-identical asm on both // compilers. - let fd: i32 = os.open(path.ptr, 0, 0i32); + let fd: i32 = os.open(path, 0, 0i32); if (fd < 0) { return 21; }; let rbuf: [128]u8; // Use raw os.read here (single syscall, plain i64) instead of diff --git a/selfhost/test/smoke.ww b/selfhost/test/smoke.ww index 52880975..3fc53a37 100644 --- a/selfhost/test/smoke.ww +++ b/selfhost/test/smoke.ww @@ -154,7 +154,7 @@ export fn main() i32 = { // that step. RDONLY is 0; passing the literal keeps the call // site standalone-compilable to byte-identical asm on both // compilers. - let fd: i32 = os.open(path.ptr, 0, 0i32); + let fd: i32 = os.open(path, 0, 0i32); if (fd < 0) { return 21; }; let rbuf: [128]u8; // Use raw os.read here (single syscall, plain i64) instead of