Files
ww/lib/dirs/dirs.ww
Hojun-Cho 19aa66aa5d lib/dirs+lib/os+test: add lib/dirs (XDG paths) + os.mkdirs
Port Hare's lib/dirs (ref/hare/dirs/xdg.ha) — XDG base-directory
lookup with mkdir-on-demand:

  dirs.config(prog) — XDG_CONFIG_HOME/<prog>, fallback $HOME/.config/<prog>
  dirs.cache(prog)  — XDG_CACHE_HOME/<prog>,  fallback $HOME/.cache/<prog>
  dirs.data(prog)   — XDG_DATA_HOME/<prog>,   fallback $HOME/.local/share/<prog>
  dirs.state(prog)  — XDG_STATE_HOME/<prog>,  fallback $HOME/.local/state/<prog>

Static-buffer return (256B pathbuf), overwritten on the next dirs.*
call — same contract as lib/temp. Fallback triggers on unset, empty,
or non-absolute XDG var (matches Hare's path::abs check). HOME-unset
rt_aborts (matches Hare's `as str` panic on missing HOME).

os.mkdirs(path, mode) — recursive mkdir, EEXIST-silenced. Walks the
path replacing each '/' with NUL, mkdir'ing each prefix, restoring
the slash. Path bytes must be writable (documented).

Surface skips with reason notes in dirs.ww header:
  - runtime() — needs stat+getuid; defer until lib/os has them
  - XDG_CONFIG_DIRS / XDG_DATA_DIRS — not in Hare's xdg.ha
  - fmt::fatalf-on-mkdir — wired to rt_abort until {n}-placeholder
    fmt lands

Cohort coverage in lib/dirs/dirstest.ww + test/wcc/975_dirs_run.c
(mkdtemp-rooted env state): XDG-absolute / XDG-non-absolute fallback /
XDG-unset×2.
2026-05-15 17:14:49 +09:00

188 lines
6.7 KiB
Plaintext

// dirs — XDG base directory paths. Port of Hare's lib/dirs
// (ref/hare/dirs/xdg.ha) using the lib/temp-style static `[256]u8`
// pathbuf in place of Hare's `path::buffer` (lib/path doesn't ship
// a buffer type yet).
//
// Surface today (1:1 with Hare's xdg.ha minus runtime()):
//
// dirs.config(prog: str) str — XDG_CONFIG_HOME/<prog>, fallback $HOME/.config/<prog>
// dirs.cache(prog: str) str — XDG_CACHE_HOME/<prog>, fallback $HOME/.cache/<prog>
// dirs.data(prog: str) str — XDG_DATA_HOME/<prog>, fallback $HOME/.local/share/<prog>
// dirs.state(prog: str) str — XDG_STATE_HOME/<prog>, fallback $HOME/.local/state/<prog>
//
// Returns are static-buffer views — borrowed for the lifetime of
// the next dirs call (any of the four above). Callers needing the
// bytes to outlive the next call duplicate via [[strings.dup]].
// Same precedent as Hare's dirs:: (which doc-strings the same
// "overwritten on subsequent calls" contract).
//
// Lookup algorithm (mirrors Hare's xdg.ha:lookup):
//
// 1. If $XDG_<NAME>_HOME is set AND its first byte is '/'
// (Hare's path::abs check), return "<XDG>/<prog>" after
// mkdir-recursive (mode 0o755).
// 2. Otherwise return "$HOME/<default>/<prog>" after mkdir-
// recursive. The non-absolute and unset/empty XDG cases both
// fall through to this branch — matches Hare's `yield`
// after the `path::abs` test.
//
// rt_aborts in two cases (matches Hare's `as str` cast on missing
// HOME, plus its `fs::strerror` fatal on mkdir failure):
//
// - HOME is not set
// - mkdirs(path, 0o755) fails for a non-EEXIST reason
//
// Skipped from v1 (with reasons preserved for the next graduator):
//
// - runtime() — needs lib/os.stat + a getuid primitive for the
// uid/perm/isdir verification Hare's runtime() does. A relaxed
// env-only version would defeat the perm contract that's the
// entire point of XDG_RUNTIME_DIR. Defer (drew/rob aligned).
//
// - XDG_CONFIG_DIRS / XDG_DATA_DIRS — system search paths. Hare's
// xdg.ha doesn't ship them; lib/CLAUDE.md prohibits richer-
// than-Hare surfaces. Defer until upstream Hare adds them.
//
// - lib/fmt's fatalf-style error reporting on mkdir failure. Hare
// uses `fmt::fatalf("Error creating {}: {}", path, ...)`; ww's
// lib/fmt is print-string-only (no {n}-placeholder parser), so
// we hand the bare context "dirs: mkdirs failed" to rt_abort.
// Graduates when the {n}-placeholder parser lands.
use os;
@symbol("rt_abort") fn rtabort(msg: str) void;
// pathbuf — module-level scratch path. Sized for any
// "<HOME>/.local/share/<prog>" composition under reasonable
// HOME and prog lengths; dirs returns a view into pathbuf[0..pathlen].
// NUL byte at pathbuf[pathlen] for direct handoff to [[os.mkdirs]]
// (same precedent as lib/temp).
let pathbuf: [256]u8;
let pathlen: i32 = 0;
// SEP — '/' byte. Same constant as lib/path's SEP.
def SEP: u8 = 47u8;
// MODE_0755 — directory creation mode passed to [[os.mkdirs]].
// Hare uses the literal `0o755` at every call site; ww doesn't ship
// octal literals, so we name the constant once.
def MODE_0755: i32 = 493i32;
// puts — append `s` to pathbuf at offset `off`, capping against the
// buffer's capacity to leave room for the trailing NUL. Returns the
// new offset. Same shape as lib/temp.puts.
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;
};
// build — assemble "<base>/<sub>/<prog>" into pathbuf, NUL-terminate,
// and store the length in [[pathlen]]. If `sub` is empty, the
// "/<sub>" segment is skipped and the result is "<base>/<prog>".
// Embedded '/' in `sub` (e.g. ".local/share") is fine — [[os.mkdirs]]
// handles intermediate dirs.
fn build(base: str, sub: str, prog: str) void = {
let off: i32 = 0;
off = puts(off, base);
if (sub.len > 0) {
if (off < 255) { pathbuf[off] = SEP; off += 1; };
off = puts(off, sub);
};
if (off < 255) { pathbuf[off] = SEP; off += 1; };
off = puts(off, prog);
if (off > 255) { off = 255; };
pathbuf[off] = 0u8;
pathlen = off;
};
// view — return a `str` view into pathbuf[0..pathlen]. Borrowed
// for the lifetime of the next dirs call.
fn view() str = {
let r: str;
r.ptr = &pathbuf[0];
r.len = pathlen;
return r;
};
// ensure — wrap [[os.mkdirs]] with the rt_abort-on-non-EEXIST
// behaviour Hare's lookup uses (`fmt::fatalf` on the HOME branch).
// Centralised so both branches in [[lookup]] share the error path.
fn ensure() void = {
match (os.mkdirs(&pathbuf[0], MODE_0755)) {
case void => {};
case let _e: os.oserror => rtabort("dirs: mkdirs failed");
};
};
// lookup — Hare's lookup() inlined: probe $envvar, fall through to
// $HOME/<dflt> on unset / empty / non-absolute XDG values. Auto-
// mkdirs the result. rt_aborts if HOME is unset (no fallback at
// the bottom of the chain — matches Hare's `as str` cast which
// would also fault on missing HOME).
fn lookup(prog: str, envvar: str, dflt: str) str = {
match (os.getenv(envvar)) {
case let xdg: str => {
if (xdg.len > 0) {
if (xdg[0] == SEP) { // Hare's path::abs(path)
build(xdg, "", prog);
ensure();
return view();
};
};
};
case void => {};
};
let home: str;
match (os.getenv("HOME")) {
case let h: str => { home = h; };
case void => rtabort("dirs: HOME is not set");
};
build(home, dflt, prog);
ensure();
return view();
};
// config — directory suitable for storing config files for `prog`.
// $XDG_CONFIG_HOME/<prog> if set+absolute, else $HOME/.config/<prog>.
// Auto-created with mode 0o755. Returns a borrowed str view into
// the module-level pathbuf; subsequent dirs calls overwrite it.
//
// Mirrors Hare's dirs::config (xdg.ha:47).
export fn config(prog: str) str = {
return lookup(prog, "XDG_CONFIG_HOME", ".config");
};
// cache — directory suitable for cache files for `prog`.
// $XDG_CACHE_HOME/<prog> if set+absolute, else $HOME/.cache/<prog>.
//
// Mirrors Hare's dirs::cache (xdg.ha:53).
export fn cache(prog: str) str = {
return lookup(prog, "XDG_CACHE_HOME", ".cache");
};
// data — directory suitable for persistent data files for `prog`.
// $XDG_DATA_HOME/<prog> if set+absolute, else $HOME/.local/share/<prog>.
// Hare composes the default via path::set(.local, share); we emit
// the composed string directly since lib/path doesn't ship a
// path::buffer type yet.
//
// Mirrors Hare's dirs::data (xdg.ha:59).
export fn data(prog: str) str = {
return lookup(prog, "XDG_DATA_HOME", ".local/share");
};
// state — directory suitable for storing program state files for
// `prog`. $XDG_STATE_HOME/<prog> if set+absolute, else
// $HOME/.local/state/<prog>.
//
// Mirrors Hare's dirs::state (xdg.ha:69).
export fn state(prog: str) str = {
return lookup(prog, "XDG_STATE_HOME", ".local/state");
};