// temp — process-local temporary files and directories. // // Mirrors Hare's temp:: at ref/hare/temp/+linux.ha; drops underscores // per plan 9 style and the layers Hare wires on top of raw os: there // is no fs::fs filesystem handle, no io::file abstraction, no defer // hook for auto-cleanup. Callers MUST [[os.close]] the returned fd // and [[os.remove]] / [[os.rmdir]] the returned path themselves; the // absence of defer is a deliberate ergonomic divergence drew-devault // was explicit about — do not invent auto-cleanup wrappers. // // Subset divergence from Hare's +linux.ha: // // * No TMPDIR env probe. lib/os doesn't expose getenv yet, so // [[file]] and [[dir]] are pinned to "/tmp". Graduate to a // `gettmpdir` that mirrors Hare's os::tryenv lookup when // lib/os.getenv lands. // * No O_TMPFILE fast path in [[file]]. We always go through // [[named]] and discard the returned path, matching Hare's // +freebsd.ha branch (and the +linux.ha fallback). The on-disk // entry leaks until the caller of [[named]] removes it; [[file]] // callers cannot remove it because they no longer hold the path. // Lift to a real O_TMPFILE branch when lib/os gains a TMPFILE // flag. // * [[named]] writes its (fd, path) result through out-pointers // instead of returning Hare's `(io::file, str) | fs::error` // 2-tuple. ww's cgen drops 32B return-by-value (task #5) and // mis-sizes the (i32, str) tuple (task #11), so we mirror // memio's / getopt's out-pointer shape until both close. // * Hare's `fs: *fs::fs` parameter is dropped — lib/os hands out // raw fds, there is no virtual-filesystem indirection. // * `iomode: mode` is our own subset of Hare's io::mode — only // WRITE and RDWR, the two values Hare's temp asserts on. // * `perm: i32` is the raw Linux mode_t bitset rather than // Hare's fs::mode enum; matches what [[os.open]] takes. // * No PRNG seeding from crypto::random. Hare reads /dev/urandom // (via crypto::random::buffer); ww doesn't ship crypto::random // yet, so we seed an inline SplitMix64 from [[os.getpid]] and // rely on O_EXCL retry for collision avoidance. Adopt a real // entropy source when crypto::random is ported. // * The path stored in pathbuf is NUL-terminated for direct // handoff to syscalls; the returned str view excludes the NUL. // Hare's path::buffer carries no terminator. // // GRADUATE-IN-ONE-GO WARNING (lib/CLAUDE.md policy). Once tasks #5 // and #11 close and lib/os exposes a real io::file abstraction plus // getenv, [[named]] graduates to Hare's value-returning // `(io::file, str) | fs::error` shape with a `fs: *fs::fs` first // arg, [[file]] grows an O_TMPFILE fast path, and [[gettmpdir]] // consults $TMPDIR. The old out-pointer surface disappears in the // same commit — callers MUST NOT bake it into themselves. // // Caller cleanup template (no defer yet): // // let fd: i32; let p: str; // match (temp.named(&fd, &p, "/tmp", temp.mode.RDWR, 384i32)) { // case void => { /* use fd */ os.close(fd); os.remove(p.ptr); }; // case let e: os.oserror => { /* error */ }; // }; // // let d: str = temp.dir(); // /* populate d ... */ // os.rmdir(d.ptr); package temp; import os; // mode — temp's io flavour. Hare exposes io::mode {READ, WRITE, // RDWR}; temp asserts iomode must be WRITE or RDWR, so we ship just // those two. Numeric values match the corresponding [[os.flag]] // bits so the OR with CREATE/EXCL composes correctly. export type mode = enum i32 { WRITE = 1, // os.flag.WRONLY RDWR = 2, // os.flag.RDWR }; // pathbuf — shared scratch path for [[named]] and [[dir]]. Sized // for any caller-supplied directory <= 233 bytes plus our 22-byte // "/temp.<16-hex-digits>\0" suffix; [[named]] returns ENAMETOOLONG // past that. Hare uses a path::buffer of PATH_MAX bytes; this // collapses to a single static slot because lib/path doesn't yet // ship a path::buffer type. NUL byte at `pathbuf[pathlen]` lets the // bytes be handed straight to [[os.open]] / [[os.mkdir]] / // [[os.remove]] / [[os.rmdir]]. let pathbuf: [256]u8; let pathlen: i32 = 0; // rng* — SplitMix64 state. Lazily seeded on first call from // [[os.getpid]]; collisions are caught by the O_EXCL retry loops in // [[named]] and [[dir]]. Inline rather than `use math.random;` to // keep temp's import surface minimal and avoid pulling random's // module-level scope into temp's namespace. let rngstate: u64 = 0u64; let rnginit: i32 = 0; fn seedrng() void = { if (rnginit == 0) { rngstate = (os.getpid(): u64) ^ 0x9E3779B97F4A7C15u64; rnginit = 1; }; }; fn nextrand() u64 = { seedrng(); rngstate = rngstate + 0x9E3779B97F4A7C15u64; let a: u64 = rngstate; a = (a ^ (a >> 30u64)) * 0xBF58476D1CE4E5B9u64; a = (a ^ (a >> 27u64)) * 0x94D049BB133111EBu64; return a ^ (a >> 31u64); }; // gettmpdir — Hare's get_tmpdir(). Hardcoded /tmp until lib/os // exposes getenv; documented in the file header. fn gettmpdir() str = { return "/tmp"; }; // Caps writes against pathbuf's capacity so a long caller-supplied // dir can't run off the end. fn puts(off: i32, s: str) i32 = { let i: i32 = 0; for (i < s.len) { if (off + i >= 255) { break; }; pathbuf[off + i] = s[i]; i += 1; }; return off + i; }; fn puthex(off: i32, v: u64) i32 = { let hex: str = "0123456789abcdef"; let i: i32 = 0; for (i < 16) { let shift: u64 = ((15 - i): u64) * 4u64; let nib: i32 = ((v >> shift) & 0xFu64): i32; if (off + i < 255) { pathbuf[off + i] = hex[nib]; }; i += 1; }; return off + 16; }; // makenamed — `/temp.\0` into pathbuf. Returns the path // length (excluding NUL). fn makenamed(dir: str) i32 = { let off: i32 = 0; off = puts(off, dir); pathbuf[off] = '/'; off += 1; off = puts(off, "temp."); off = puthex(off, nextrand()); pathbuf[off] = 0u8; return off; }; // makedir — `/\0` into pathbuf. Returns path length // (excluding NUL). fn makedir() i32 = { let off: i32 = 0; off = puts(off, gettmpdir()); pathbuf[off] = 47u8; off += 1; off = puthex(off, nextrand()); pathbuf[off] = 0u8; return off; }; @symbol("rt_abort") fn rtabort(msg: str) void; // named — create a fresh `/temp.` with O_EXCL. On success // `*outfd` receives the fd and `*outpath` receives a borrowed view // into a module-level path buffer; the buffer is overwritten on // every subsequent [[named]] or [[dir]] call. Caller MUST close // *outfd and [[os.remove]] *outpath when done. // // Returns -ENAMETOOLONG (-36) as `os.oserror` if `dir.len > 233`: // pathbuf is 256B, the suffix "/temp.<16-hex>\0" needs 22 bytes, // leaving 234 for `dir` (0..233 inclusive). Without this guard // [[makenamed]]'s final NUL-store would write past pathbuf's end. // // Mirrors Hare's temp::named (minus the fs::fs first arg and the // 2-tuple return — see file header). export fn named(outfd: *i32, outpath: *str, dir: str, iomode: mode, perm: i32) (void | os.oserror) = { if (dir.len > 233) { return -36i64: os.oserror; }; let flags: os.flag = os.flag.CREATE | os.flag.EXCL; if (iomode == mode.RDWR) { flags = flags | os.flag.RDWR; } else { flags = flags | os.flag.WRONLY; }; for (true) { pathlen = makenamed(dir); 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; p.ptr = &pathbuf[0]; p.len = pathlen; *outpath = p; return; }; let e: i64 = fd: i64; if (e != -17i64) { return e: os.oserror; }; // -EEXIST → retry }; return; }; // file — create a temporary file under [[gettmpdir]] and return the // fd. The on-disk path is discarded; without O_TMPFILE the entry // persists until removed externally. Matches Hare's +freebsd.ha // branch (and the +linux.ha fallback path). Caller MUST close. // // Mirrors Hare's temp::file. export fn file(iomode: mode, perm: i32) (i32 | os.oserror) = { let fd: i32 = 0; let p: str; match (named(&fd, &p, gettmpdir(), iomode, perm)) { case void => return fd; case let e: os.oserror => return e; }; }; // dir — create a fresh temporary directory under [[gettmpdir]] with // mode 0o700. Returns a borrowed view of the static path buffer; // the buffer is overwritten on every subsequent [[named]] or // [[dir]] call. Caller MUST [[os.rmdir]] the path when done. // Aborts on syscall failure other than EEXIST, mirroring Hare's // `abort("Could not create temp directory")`. // // Mirrors Hare's temp::dir. export fn dir() str = { for (true) { pathlen = makedir(); 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]; p.len = pathlen; return p; }; if (r != -17) { rtabort("temp.dir: mkdir failed"); }; // EEXIST → retry }; let p: str; p.ptr = &pathbuf[0]; p.len = pathlen; return p; };