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.
This commit is contained in:
109
lib/os/os.ww
109
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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user