lib/os+test: add stat / lstat / fstat / exists
Hare-shaped filestat introspection. New types: filestat (80B, mirrors fs::filestat ref/hare/fs/types.ha:141), mode (31-member enum mirroring fs::mode ref/hare/fs/types.ha:63), stat_mask (7 bits mirroring fs::stat_mask ref/hare/fs/types.ha:129), timespec (i64+i64, layout-compatible with future lib/time::instant). APIs: stat / lstat / fstat (*filestat, *u8|i32) (void|oserror) over SYS_newfstatat (nr=262). The out-param shape sidesteps the cgreturn 24B ABI cap; commented inline. exists(*u8) bool goes through the syscall directly rather than wrapping stat()? — dodges task #22's 80B-scrutinee match-slot disagreement until that lands. Three latent cgen workarounds in tree, all pointer'd to filed tasks: #22: os.exists sidesteps the (void|oserror) match shape #24: `at` enum bundles AT_FDCWD/SYMLINK_NOFOLLOW/EMPTY_PATH instead of three top-level `def`s (negative-literal def DATA omit) #25: kstat.mode typed as `mode` (enum) rather than u32 to skip the redundant u32→enum cast emit Tests: 976_stat_run, 9 rows — stat/lstat/fstat × regfile/dir/symlink plus exists × {regfile,dir,noent}. Row 1 also pins perm-bit and atime/mtime/ctime!=0 to catch silent kstat→filestat offset miscompiles (kstat fields at 72/88/104). Graduation to lib/fs when it ships is noted inline; signatures stay rename-compatible.
This commit is contained in:
5
Makefile
5
Makefile
@@ -241,6 +241,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \
|
||||
$(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \
|
||||
$(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \
|
||||
$(BIN)/test_stat_run \
|
||||
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
|
||||
$(BIN)/test_base32_run $(BIN)/test_base64_run \
|
||||
$(BIN)/test_adler32_run $(BIN)/test_crc16_run \
|
||||
@@ -498,6 +499,10 @@ $(BIN)/test_dirs_run: test/wcc/975_dirs_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_stat_run: test/wcc/976_stat_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
243
lib/os/os.ww
243
lib/os/os.ww
@@ -61,6 +61,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -365,3 +366,245 @@ export fn getenv(name: str) (str | void) = {
|
||||
};
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
185
lib/os/stattest.ww
Normal file
185
lib/os/stattest.ww
Normal file
@@ -0,0 +1,185 @@
|
||||
// stattest — exercises [[os.stat]] / [[os.lstat]] / [[os.fstat]] /
|
||||
// [[os.exists]] against a scratch tree pre-arranged by
|
||||
// test/wcc/976_stat_run.c. The driver mkdtemp's a tmp dir, creates a
|
||||
// regfile + symlink + subdir, exports paths through env vars, then
|
||||
// exec's `ww run lib/os/stattest.ww`. We read the paths back via
|
||||
// os.getenv and stat the targets.
|
||||
//
|
||||
// Pre-arranged scratch tree (set by 976_stat_run.c):
|
||||
//
|
||||
// WW_TEST_STAT_REGFILE = "<tmp>/regfile" regular file, 11 bytes
|
||||
// WW_TEST_STAT_SYMLINK = "<tmp>/symlink" symlink → ./regfile
|
||||
// WW_TEST_STAT_SUBDIR = "<tmp>/subdir" directory, 0700
|
||||
// WW_TEST_STAT_NOENT = "<tmp>/does-not-exist"
|
||||
//
|
||||
// Test exit code follows the stdlib `_run` convention: a `signalled`
|
||||
// global the main fn bumps before each row, so `WEXITSTATUS = signalled
|
||||
// + 10` tells the harness which row tripped. Same shape as
|
||||
// ostest / temptest / shlextest.
|
||||
|
||||
use os;
|
||||
|
||||
let signalled: i32 = 0;
|
||||
|
||||
fn fail() void = { os.exit(signalled + 10); };
|
||||
|
||||
// envpath — fetch an env var or abort; we want a clean signal if
|
||||
// 976_stat_run.c didn't prime the env state.
|
||||
fn envpath(name: str) str = {
|
||||
match (os.getenv(name)) {
|
||||
case void => { fail(); return "": str; };
|
||||
case let s: str => return s;
|
||||
};
|
||||
};
|
||||
|
||||
// istype — mask the file-type bits (S_IFMT = 0o170000 = 61440)
|
||||
// out of a mode and compare against the requested type bit.
|
||||
fn istype(m: os.mode, t: os.mode) bool = {
|
||||
return ((m as u32) & 61440u32) == (t as u32);
|
||||
};
|
||||
|
||||
// ---- stat: regular file --------------------------------------------
|
||||
//
|
||||
// Pinned bytes are "hello world" (11 bytes). We verify:
|
||||
// - mask is fully set (newfstatat fills everything)
|
||||
// - mode's type bits == REG
|
||||
// - sz == 11
|
||||
// - inode is non-zero (real fs entry, not synthetic)
|
||||
|
||||
@test fn test_stat_regfile() void = {
|
||||
let p = envpath("WW_TEST_STAT_REGFILE");
|
||||
let fi: os.filestat;
|
||||
// newfstatat populates every field, so the mask is the
|
||||
// OR-fold of all 7 Hare stat_mask bits — mirrors what
|
||||
// fillfilestat assigns.
|
||||
let wantmask: u32 = (os.stat_mask.UID | os.stat_mask.GID
|
||||
| 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)) {
|
||||
case let e: os.oserror => fail();
|
||||
case void => {
|
||||
if ((fi.mask as u32) != wantmask) { fail(); };
|
||||
if (!istype(fi.mode, os.mode.REG)) { fail(); };
|
||||
if (fi.sz != 11u64) { fail(); };
|
||||
if (fi.inode == 0u64) { fail(); };
|
||||
// Permission-bit smoke test — 976_stat_run.c open(2)s with
|
||||
// mode 0644 so USER_R survives any reasonable umask. Catches
|
||||
// a struct-field-offset miscompile on `fi.mode` that the
|
||||
// type-bit istype() check could miss if perm bits aliased a
|
||||
// neighbouring u32 (uid/gid).
|
||||
if (((fi.mode as u32) & (os.mode.USER_R as u32)) == 0u32) {
|
||||
fail();
|
||||
};
|
||||
// atime/mtime/ctime — kernel-set at create time, all
|
||||
// post-epoch (>0). Three distinct kstat offsets (72/88/104)
|
||||
// so a fillfilestat field-copy miscompile or a filestat
|
||||
// timespec-offset bug surfaces here, not silently.
|
||||
if (fi.atime.sec <= 0i64) { fail(); };
|
||||
if (fi.mtime.sec <= 0i64) { fail(); };
|
||||
if (fi.ctime.sec <= 0i64) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ---- stat: directory ------------------------------------------------
|
||||
|
||||
@test fn test_stat_subdir() void = {
|
||||
let p = envpath("WW_TEST_STAT_SUBDIR");
|
||||
let fi: os.filestat;
|
||||
match (os.stat(&fi, p.ptr)) {
|
||||
case let e: os.oserror => fail();
|
||||
case void => {
|
||||
if (!istype(fi.mode, os.mode.DIR)) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ---- stat: missing path → oserror ENOENT ---------------------------
|
||||
|
||||
@test fn test_stat_noent() void = {
|
||||
let p = envpath("WW_TEST_STAT_NOENT");
|
||||
let fi: os.filestat;
|
||||
match (os.stat(&fi, p.ptr)) {
|
||||
case void => fail();
|
||||
case let e: os.oserror => {
|
||||
// ENOENT = 2 → raw errno is -2.
|
||||
if ((e: i64) != -2i64) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ---- stat (follow) vs lstat (no-follow) on a symlink ----------------
|
||||
//
|
||||
// stat follows the link → reports the regfile (REG, 11 bytes).
|
||||
// lstat does NOT follow → reports the link itself (LINK).
|
||||
|
||||
@test fn test_stat_symlink_follow() void = {
|
||||
let p = envpath("WW_TEST_STAT_SYMLINK");
|
||||
let fi: os.filestat;
|
||||
match (os.stat(&fi, p.ptr)) {
|
||||
case let e: os.oserror => fail();
|
||||
case void => {
|
||||
if (!istype(fi.mode, os.mode.REG)) { fail(); };
|
||||
if (fi.sz != 11u64) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@test fn test_lstat_symlink_nofollow() void = {
|
||||
let p = envpath("WW_TEST_STAT_SYMLINK");
|
||||
let fi: os.filestat;
|
||||
match (os.lstat(&fi, p.ptr)) {
|
||||
case let e: os.oserror => fail();
|
||||
case void => {
|
||||
if (!istype(fi.mode, os.mode.LINK)) { fail(); };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ---- fstat: open a file and stat by fd ------------------------------
|
||||
|
||||
@test fn test_fstat_regfile() void = {
|
||||
let p = envpath("WW_TEST_STAT_REGFILE");
|
||||
let fd: i32 = os.open(p.ptr, os.flag.RDONLY, 0i32);
|
||||
if (fd < 0) { fail(); };
|
||||
let fi: os.filestat;
|
||||
match (os.fstat(&fi, fd)) {
|
||||
case let e: os.oserror => { os.close(fd); fail(); };
|
||||
case void => {
|
||||
if (!istype(fi.mode, os.mode.REG)) { os.close(fd); fail(); };
|
||||
if (fi.sz != 11u64) { os.close(fd); fail(); };
|
||||
};
|
||||
};
|
||||
os.close(fd);
|
||||
};
|
||||
|
||||
// ---- exists: true on regfile/dir/symlink, false on noent ------------
|
||||
|
||||
@test fn test_exists_regfile() void = {
|
||||
let p = envpath("WW_TEST_STAT_REGFILE");
|
||||
if (!os.exists(p.ptr)) { fail(); };
|
||||
};
|
||||
|
||||
@test fn test_exists_subdir() void = {
|
||||
let p = envpath("WW_TEST_STAT_SUBDIR");
|
||||
if (!os.exists(p.ptr)) { fail(); };
|
||||
};
|
||||
|
||||
@test fn test_exists_noent() void = {
|
||||
let p = envpath("WW_TEST_STAT_NOENT");
|
||||
if (os.exists(p.ptr)) { fail(); };
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
signalled = 1; test_stat_regfile();
|
||||
signalled = 2; test_stat_subdir();
|
||||
signalled = 3; test_stat_noent();
|
||||
signalled = 4; test_stat_symlink_follow();
|
||||
signalled = 5; test_lstat_symlink_nofollow();
|
||||
signalled = 6; test_fstat_regfile();
|
||||
signalled = 7; test_exists_regfile();
|
||||
signalled = 8; test_exists_subdir();
|
||||
signalled = 9; test_exists_noent();
|
||||
return 0;
|
||||
};
|
||||
@@ -62,6 +62,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// MODULE: wcc
|
||||
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
|
||||
//
|
||||
|
||||
@@ -62,6 +62,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// MODULE: wcc
|
||||
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
|
||||
//
|
||||
|
||||
@@ -62,6 +62,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// MODULE: wcc
|
||||
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
|
||||
//
|
||||
|
||||
@@ -62,6 +62,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// MODULE: wcc
|
||||
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
|
||||
//
|
||||
|
||||
@@ -62,6 +62,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// MODULE: wcc
|
||||
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
|
||||
//
|
||||
|
||||
@@ -62,6 +62,7 @@ type nr = enum i64 {
|
||||
UNLINK = 87,
|
||||
GETCWD = 79,
|
||||
GETDENTS64 = 217,
|
||||
NEWFSTATAT = 262,
|
||||
};
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
|
||||
@@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
// ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns
|
||||
// `filestat` by value; ww's cgreturn ABI tops out at 24B today (see
|
||||
// STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] /
|
||||
// [[fstat]] take an out-parameter and return `(void | oserror)`.
|
||||
// Re-evaluate the by-value shape when full sret lands.
|
||||
//
|
||||
// `filestat`, `mode`, and `stat_mask` live in lib/os because ww has
|
||||
// no lib/fs yet; Hare puts them in `fs::`. These types graduate to
|
||||
// lib/fs when that module ships — callers should expect a future
|
||||
// re-export.
|
||||
//
|
||||
// Underlying syscall is SYS_newfstatat (262), which unifies
|
||||
// stat/lstat/fstat through the `dirfd + flags` triple:
|
||||
// stat = newfstatat(AT_FDCWD, path, 0)
|
||||
// lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW)
|
||||
// fstat = newfstatat(fd, "", AT_EMPTY_PATH)
|
||||
// Avoiding SYS_statx — its 256B variable layout would buy btime,
|
||||
// but Hare's filestat doesn't expose btime either, so we stay on
|
||||
// the simpler 144B kernel struct.
|
||||
|
||||
// at — flags for fstatat(2). Numeric values match <fcntl.h>.
|
||||
// Bundled as an enum because top-level `def` of a negative literal
|
||||
// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen —
|
||||
// linker surfaces as "undefined reference". Tracked as task #24;
|
||||
// enum-member resolution sidesteps it cleanly.
|
||||
type at = enum i64 {
|
||||
FDCWD = -100,
|
||||
SYMLINK_NOFOLLOW = 256, // 0x100
|
||||
EMPTY_PATH = 4096, // 0x1000
|
||||
};
|
||||
|
||||
// mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/
|
||||
// types.ha:63). Permission bits are the standard Unix octal subset;
|
||||
// type bits live in the S_IFMT = 0o170000 region. Type-bit test:
|
||||
//
|
||||
// let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask
|
||||
// if (t == os.mode.DIR as u32) { /* directory */ };
|
||||
//
|
||||
// Numeric values are octal in Hare's source; ww has no octal
|
||||
// literals so they're written as decimal with the octal in a
|
||||
// trailing comment.
|
||||
export type mode = enum u32 {
|
||||
// permission bits
|
||||
USER_RWX = 448u32, // 0o700
|
||||
USER_RW = 384u32, // 0o600
|
||||
USER_RX = 320u32, // 0o500
|
||||
USER_R = 256u32, // 0o400
|
||||
USER_W = 128u32, // 0o200
|
||||
USER_X = 64u32, // 0o100
|
||||
GROUP_RWX = 56u32, // 0o070
|
||||
GROUP_RW = 48u32, // 0o060
|
||||
GROUP_RX = 40u32, // 0o050
|
||||
GROUP_R = 32u32, // 0o040
|
||||
GROUP_W = 16u32, // 0o020
|
||||
GROUP_X = 8u32, // 0o010
|
||||
OTHER_RWX = 7u32, // 0o007
|
||||
OTHER_RW = 6u32, // 0o006
|
||||
OTHER_RX = 5u32, // 0o005
|
||||
OTHER_R = 4u32, // 0o004
|
||||
OTHER_W = 2u32, // 0o002
|
||||
OTHER_X = 1u32, // 0o001
|
||||
SETUID = 2048u32, // 0o4000
|
||||
SETGID = 1024u32, // 0o2000
|
||||
STICKY = 512u32, // 0o1000
|
||||
// file-type bits (S_IFMT mask = 0o170000 = 61440)
|
||||
UNKNOWN = 0u32,
|
||||
FIFO = 4096u32, // 0o010000
|
||||
CHR = 8192u32, // 0o020000
|
||||
DIR = 16384u32, // 0o040000
|
||||
BLK = 24576u32, // 0o060000
|
||||
REG = 32768u32, // 0o100000
|
||||
LINK = 40960u32, // 0o120000
|
||||
SOCK = 49152u32, // 0o140000
|
||||
};
|
||||
|
||||
// stat_mask — which filestat fields the call populated. Mirrors
|
||||
// Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills
|
||||
// every field, so [[stat]] / [[lstat]] / [[fstat]] always set all
|
||||
// seven bits OR-folded (see [[fillfilestat]]); per-bit testing is
|
||||
// the documented sparse-backend pattern (cf. Hare's fs::fs network
|
||||
// backends that only populate mtime+size).
|
||||
export type stat_mask = enum u32 {
|
||||
UID = 1u32,
|
||||
GID = 2u32,
|
||||
SIZE = 4u32,
|
||||
INODE = 8u32,
|
||||
ATIME = 16u32,
|
||||
MTIME = 32u32,
|
||||
CTIME = 64u32,
|
||||
};
|
||||
|
||||
// timespec — {sec, nsec} pair, matching Hare's time::instant
|
||||
// (ref/hare/time/types.ha). Local to lib/os; graduates to
|
||||
// lib/time.instant when lib/time and lib/fs ship. Same byte layout
|
||||
// (i64+i64 = 16B) so a future migration is field-rename only.
|
||||
export type timespec = struct {
|
||||
sec: i64,
|
||||
nsec: i64,
|
||||
};
|
||||
|
||||
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
|
||||
// bytes. See module-header note re: graduation to lib/fs.
|
||||
export type filestat = struct {
|
||||
mask: stat_mask, // 0 (4)
|
||||
mode: mode, // 4 (4)
|
||||
uid: u32, // 8 (4)
|
||||
gid: u32, // 12 (4)
|
||||
sz: u64, // 16 (8)
|
||||
inode: u64, // 24 (8)
|
||||
atime: timespec, // 32 (16)
|
||||
mtime: timespec, // 48 (16)
|
||||
ctime: timespec, // 64 (16) — ends at 80
|
||||
};
|
||||
|
||||
// kstat — x86_64 kernel `struct stat` layout. Mirrors
|
||||
// arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed
|
||||
// fields). 144 bytes. Module-internal; SYS_newfstatat writes into
|
||||
// this buffer and the public stat fns then copy the bits into the
|
||||
// Hare-shaped [[filestat]].
|
||||
//
|
||||
// Mode is typed as the public [[mode]] enum (rather than raw u32)
|
||||
// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage
|
||||
// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that
|
||||
// wwstage skips (task #25); the in-tree shape sidesteps it.
|
||||
// Identical byte layout (both 4B at offset 24).
|
||||
type kstat = struct {
|
||||
dev: u64, // 0
|
||||
ino: u64, // 8
|
||||
nlink: u64, // 16
|
||||
mode: mode, // 24
|
||||
uid: u32, // 28
|
||||
gid: u32, // 32
|
||||
pad0: u32, // 36
|
||||
rdev: u64, // 40
|
||||
sz: i64, // 48
|
||||
blksize: i64, // 56
|
||||
blocks: i64, // 64
|
||||
atime_sec: i64, // 72
|
||||
atime_nsec: i64, // 80
|
||||
mtime_sec: i64, // 88
|
||||
mtime_nsec: i64, // 96
|
||||
ctime_sec: i64, // 104
|
||||
ctime_nsec: i64, // 112
|
||||
unused0: i64, // 120
|
||||
unused1: i64, // 128
|
||||
unused2: i64, // 136 — ends at 144
|
||||
};
|
||||
|
||||
// emptypath — single-NUL byte used as the `pathname` arg to
|
||||
// newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL
|
||||
// pointer to a zero-length C string, NOT a null pointer. Bytes are
|
||||
// read-only from the kernel's view; ww has no module-level const so
|
||||
// this is a writable `let`.
|
||||
let emptypath: [1]u8 = [0u8];
|
||||
|
||||
// fillfilestat — copy a 144B kstat into the 80B Hare-shaped
|
||||
// filestat. Internal helper used by all three public entry points.
|
||||
// Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259):
|
||||
// newfstatat populates every field, so the mask is the OR-fold of
|
||||
// all seven Hare stat_mask bits.
|
||||
fn fillfilestat(out: *filestat, k: *kstat) void = {
|
||||
out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE
|
||||
| stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME
|
||||
| stat_mask.CTIME;
|
||||
out.mode = k.mode;
|
||||
out.uid = k.uid;
|
||||
out.gid = k.gid;
|
||||
out.sz = k.sz: u64;
|
||||
out.inode = k.ino;
|
||||
out.atime.sec = k.atime_sec;
|
||||
out.atime.nsec = k.atime_nsec;
|
||||
out.mtime.sec = k.mtime_sec;
|
||||
out.mtime.nsec = k.mtime_nsec;
|
||||
out.ctime.sec = k.ctime_sec;
|
||||
out.ctime.nsec = k.ctime_nsec;
|
||||
};
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: 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) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64,
|
||||
at.SYMLINK_NOFOLLOW: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// fstat — like [[stat]] but addresses the file by fd. Uses
|
||||
// newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd
|
||||
// directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54).
|
||||
export fn fstat(out: *filestat, fd: i32) (void | oserror) = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
fd: i64, (&emptypath[0]): i64, (&k): i64,
|
||||
at.EMPTY_PATH: i64);
|
||||
if (r < 0) { return r: oserror; };
|
||||
fillfilestat(out, &k);
|
||||
};
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Race warning: prefer "open and handle the error" over "exists
|
||||
// then open" in real code (Hare's docstring carries the same
|
||||
// note). The race is unavoidable in this shape.
|
||||
//
|
||||
// Goes through SYS_newfstatat directly rather than match'ing on
|
||||
// [[stat]]'s `(void | oserror)` return. Functionally identical;
|
||||
// the direct shape sidesteps a cstage/wwstage cgen disagreement
|
||||
// on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B
|
||||
// — 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 = {
|
||||
let k: kstat;
|
||||
let r: i64 = syscall4(nr.NEWFSTATAT,
|
||||
at.FDCWD: i64, path: i64, (&k): i64, 0i64);
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// MODULE: strings
|
||||
// strings — operations over the immutable str type ({ *u8, len }).
|
||||
// Mirrors Hare's strings::; `len` and `is-empty` aren't functions
|
||||
|
||||
107
test/wcc/976_stat_run.c
Normal file
107
test/wcc/976_stat_run.c
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 976_stat_run — execute the lib/os stat fixture under the C-side
|
||||
* `ww run` driver and assert exit 0.
|
||||
*
|
||||
* Workflow (mirrors 975_dirs_run's mkdtemp + setenv + rm-rf shape):
|
||||
* 1. mkdtemp /tmp/wwstat-XXXXXX — fresh per run, no parallel-test
|
||||
* contention.
|
||||
* 2. Populate the scratch tree:
|
||||
* <tmp>/regfile regular file, 11 bytes "hello world"
|
||||
* <tmp>/symlink → ./regfile (relative symlink)
|
||||
* <tmp>/subdir directory, mode 0700
|
||||
* 3. setenv WW_TEST_STAT_REGFILE / SYMLINK / SUBDIR / NOENT with
|
||||
* the absolute paths; lib/os/stattest.ww reads them via
|
||||
* os.getenv and exercises stat/lstat/fstat/exists.
|
||||
* 4. exec `ww run lib/os/stattest.ww`.
|
||||
* 5. system("rm -rf <tmp>") regardless of pass/fail.
|
||||
*
|
||||
* Same wrapper shape as 970-975; cleanup-on-failure is rm-rf'd so a
|
||||
* regression never leaves residue under /tmp.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char absbin[1024];
|
||||
if (bin[0] != '/') {
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
|
||||
char tmpl[64];
|
||||
strcpy(tmpl, "/tmp/wwstat-XXXXXX");
|
||||
if (mkdtemp(tmpl) == NULL) {
|
||||
perror("mkdtemp");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char regfile[256], symlink_path[256], subdir[256], noent[256];
|
||||
snprintf(regfile, sizeof regfile, "%s/regfile", tmpl);
|
||||
snprintf(symlink_path, sizeof symlink_path, "%s/symlink", tmpl);
|
||||
snprintf(subdir, sizeof subdir, "%s/subdir", tmpl);
|
||||
snprintf(noent, sizeof noent, "%s/does-not-exist", tmpl);
|
||||
|
||||
/* Populate. */
|
||||
int fd = open(regfile, O_WRONLY | O_CREAT | O_EXCL, 0644);
|
||||
if (fd < 0) { perror("open regfile"); goto cleanup_fail; }
|
||||
if (write(fd, "hello world", 11) != 11) {
|
||||
perror("write regfile"); close(fd); goto cleanup_fail;
|
||||
}
|
||||
close(fd);
|
||||
if (symlink("./regfile", symlink_path) != 0) {
|
||||
perror("symlink"); goto cleanup_fail;
|
||||
}
|
||||
if (mkdir(subdir, 0700) != 0) {
|
||||
perror("mkdir subdir"); goto cleanup_fail;
|
||||
}
|
||||
|
||||
setenv("WW_TEST_STAT_REGFILE", regfile, 1);
|
||||
setenv("WW_TEST_STAT_SYMLINK", symlink_path, 1);
|
||||
setenv("WW_TEST_STAT_SUBDIR", subdir, 1);
|
||||
setenv("WW_TEST_STAT_NOENT", noent, 1);
|
||||
|
||||
const char *src = "lib/os/stattest.ww";
|
||||
char path[1024], cmd[2048];
|
||||
snprintf(path, sizeof path, "%s/%s", cwd, src);
|
||||
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
|
||||
int rc = runwait(cmd);
|
||||
|
||||
char rmcmd[256];
|
||||
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpl);
|
||||
(void)system(rmcmd);
|
||||
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "stat_run FAIL: %s exited %d\n", src, rc);
|
||||
return 1;
|
||||
}
|
||||
printf("stat_run: %s ok\n", src);
|
||||
return 0;
|
||||
|
||||
cleanup_fail: {
|
||||
char rmcmd[256];
|
||||
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpl);
|
||||
(void)system(rmcmd);
|
||||
return 1;
|
||||
} }
|
||||
Reference in New Issue
Block a user