From 93b671808c09ae758ebc4bbf54d12b81b329cd9b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 21 Jun 2026 23:41:18 +0900 Subject: [PATCH] os: getenvs() []str + single-walker getenv (Hare env surface) Mirror Hare's os env surface: getenvs() builds an owned []str of NAME=VALUE entries from the rt_envp table (platform_environ.ha:41), and getenv iterates it (environ.ha:32) so there is exactly one env walker. strings.dup is unusable here -- lib/strings imports os, so os importing strings would cycle; the owned copy is inlined (dup.ha:7). test_getenvs_entries pins the []str shape non-vacuously. --- lib/os/os.ww | 109 ++++++++++++++++++++++++++++++++++------------- lib/os/ostest.ww | 28 ++++++++++++ 2 files changed, 108 insertions(+), 29 deletions(-) diff --git a/lib/os/os.ww b/lib/os/os.ww index 09060bd2..dc560b7a 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -437,47 +437,98 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { @symbol("rt_argc") fn rtargc() i64; @symbol("rt_argv") fn rtargv() **u8; +// envpbuilt / envpcache — build-once cache for [[getenvs]], the single +// env walker. drew ruling (task #17, shared with [[args]]): an explicit +// `built` sentinel, NOT len==0 overloading — an empty environment is a +// legitimate len-0 state, so a len check would re-walk every call. +let envpbuilt: bool = false; +let envpcache: []str; + +// getenvs — the environment as an owned `[]str` of "NAME=VALUE" entries. +// Mirrors ref/hare/os/+linux/platform_environ.ha:41: lazy-build a +// file-private cache by walking the NUL-pointer-terminated rt_envp table, +// duping each C string into an owned str. Second call returns the cache. +// [[getenv]] borrows into this slice, so there is exactly one env walker. +// +// DIVERGENCE (Hare-fidelity): Hare's getenvs uses strings::dup, but ww's +// strings imports os (os.alloc is ww's allocator — a divergence from +// Hare's rt::malloc-direct strings), so os importing strings would be an +// import cycle. The owned copy is inlined here — same alloc + byte-copy +// as ref/hare/strings/dup.ha:7. +export fn getenvs() []str = { + if (envpbuilt) { return envpcache; }; + let tab: **u8 = rtenvp(); + let count: i32 = 0; + for (tab[count] != nil: *u8) { count += 1; }; + // alloc([], 0) is rt_malloc(0) = an mmap of 0 bytes (-EINVAL); the + // empty environment carries the {nil,0,0} shape directly (mirrors + // lib/strings dupall's empty bypass). + let r: []str; + r.ptr = nil: *str; + r.len = 0; + r.cap = 0; + if (count != 0) { + let acc: []str = alloc([], count: u64)!; + let i: i32 = 0; + for (i < count) { + let entry: *u8 = tab[i]; + let n: i32 = 0; + for (entry[n] != 0u8) { n += 1; }; + let buf: []u8 = alloc([], n: u64)!; + buf.len = n; + let j: i32 = 0; + for (j < n) { buf[j] = entry[j]; j += 1; }; + let s: str; + s.ptr = buf.ptr; + s.len = n; + append(acc, s); + i += 1; + }; + r = acc; + }; + envpcache = r; + envpbuilt = true; + return r; +}; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes -// live in the kernel-supplied envp table at process entry. A future -// `setenv` (separate task) that grows the table behind the scenes -// would invalidate prior views; v1 has no setenv, so callers can -// hold the view indefinitely. +// live in the owned []str built by [[getenvs]] (borrow-source change, +// task #28: was the raw rt_envp table; now the single duped walker). A +// future `setenv` that grows the table behind the scenes would +// invalidate prior views; v1 has no setenv, so callers can hold the +// view indefinitely. // -// Mirrors Hare's os::tryenv shape (returns void rather than panicking -// on missing). Hare also ships os::getenv (`(str | void)`) and -// os::mustenv (panic-on-missing); ww collapses to the single -// `(str | void)` form for now — consumers wanting "must" semantics -// abort at the call site. +// Mirrors Hare's os::getenv (ref/hare/os/environ.ha:32): iterate +// [[getenvs]], "name=" prefix-match each entry, return the value tail. +// Hare also ships os::tryenv (default-on-missing) and os::mustenv +// (panic-on-missing); ww collapses to the single `(str | void)` form for +// now — consumers wanting "must" semantics abort at the call site. // -// Algorithm: walk the NUL-pointer-terminated `environ` table doing a -// "name=" prefix match against each entry, byte-wise. NUL inside -// `name` would never match a real env var (env var names cannot -// contain '\0'), so we don't filter — POSIX puts that responsibility -// on the caller. +// NUL inside `name` would never match a real env var (env var names +// cannot contain '\0'), so we don't filter — POSIX puts that +// responsibility on the caller. export fn getenv(name: str) (str | void) = { - let envp: **u8 = rtenvp(); + let env: []str = getenvs(); let i: i32 = 0; - for (true) { - let entry: *u8 = envp[i]; - if (entry == nil: *u8) { return; }; - let j: i32 = 0; + for (i < env.len) { + let ent: str = env[i]; let matched: bool = true; + let j: i32 = 0; for (j < name.len) { - if (entry[j] == 0u8) { matched = false; break; }; - if (entry[j] != name[j]) { matched = false; break; }; + if (j >= ent.len) { matched = false; break; }; + if (ent[j] != name[j]) { matched = false; break; }; j += 1; }; if (matched) { - if (entry[name.len] == '=') { - let val: *u8 = entry + ((name.len + 1): u64); - let n: i32 = 0; - for (val[n] != 0u8) { n += 1; }; - let r: str; - r.ptr = val; - r.len = n; - return r; + if (name.len < ent.len) { + if (ent[name.len] == '=') { + let val: str; + val.ptr = ent.ptr + ((name.len + 1): u64); + val.len = ent.len - (name.len + 1); + return val; + }; }; }; i += 1; diff --git a/lib/os/ostest.ww b/lib/os/ostest.ww index 39e61525..134e3689 100644 --- a/lib/os/ostest.ww +++ b/lib/os/ostest.ww @@ -91,6 +91,34 @@ fn streq(a: str, b: str) bool = { }; }; +// ---- getenvs: direct non-vacuous coverage of the []str shape -------- +// +// getenvs() is public API (task #28) and the single env walker getenv +// routes through. getenv's cases exercise the walk transitively, but +// none asserts the []str RETURN shape — its count or "NAME=VALUE" entry +// text. This row pins both: the slice must be non-empty, and every +// expected entry must appear verbatim (name, '=', value duped intact). +// Fails if getenvs miscounts (len 0 → the inner search finds nothing) +// or dups the wrong bytes (off-by-one truncation → no streq match). +// The "WW_TEST_EMPTY=" row also pins the empty-value entry text. + +@test fn test_getenvs_entries() void = { + let env: []str = os.getenvs(); + assert(!(env.len == 0)); + let want: [2]str = ["WW_TEST_GETENV=hello-world", "WW_TEST_EMPTY="]; + let w: i32 = 0; + for (w < 2) { + let found: bool = false; + let i: i32 = 0; + for (i < env.len) { + if (streq(env[i], want[w])) { found = true; }; + i += 1; + }; + assert(found); + w += 1; + }; +}; + // ---- alloc/free: mmap-backed runtime allocator ---------------------- // // Direct round-trip. Write-then-read-back proves the returned page is