diff --git a/Makefile b/Makefile index 678681d5..4bcd0779 100644 --- a/Makefile +++ b/Makefile @@ -517,6 +517,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_attest_record \ $(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_dirs_toolong_run \ $(BIN)/test_stat_run $(BIN)/test_time_run \ $(BIN)/test_intdiv_signed \ $(BIN)/test_strings_run \ @@ -2683,6 +2684,16 @@ $(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 $@ $< +# 975_dirs_toolong_run (F14 #69): an over-long composed path must abort +# loudly, not silently mkdir a truncated wrong directory. Builds+runs a +# dirs.config fixture on BOTH driver twins under a short and a ~260B HOME. +$(BIN)/test_dirs_toolong_run: test/wcc/975_dirs_toolong_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(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 $@ $< diff --git a/lib/dirs/dirs.ww b/lib/dirs/dirs.ww index 6cba53ff..3f7f8c20 100644 --- a/lib/dirs/dirs.ww +++ b/lib/dirs/dirs.ww @@ -90,15 +90,24 @@ fn puts(off: i32, s: str) i32 = { // Embedded '/' in `sub` (e.g. ".local/share") is fine — [[os.mkdirs]] // handles intermediate dirs. fn build(base: str, sub: str, prog: str) void = { + // ref/hare/dirs/xdg.ha routes through path::set/push, whose too_long + // error the `!` turns into a loud abort. ww's fixed 256B pathbuf + // (dirs.ww:58) is a documented simplification, but the overflow must + // be LOUD, not a silently-wrong directory that then gets mkdir'd: + // reject up front when the composition (+ trailing NUL) won't fit. + // Shape (a); routing dirs through lib/path is the fidelity follow-up. + let need: i32 = base.len + 1 + prog.len; + if (sub.len > 0) { need += 1 + sub.len; }; + if (need >= 256) { rtabort("dirs: path too long"); }; + let off: i32 = 0; off = puts(off, base); if (sub.len > 0) { - if (off < 255) { pathbuf[off] = SEP; off += 1; }; + pathbuf[off] = SEP; off += 1; off = puts(off, sub); }; - if (off < 255) { pathbuf[off] = SEP; off += 1; }; + pathbuf[off] = SEP; off += 1; off = puts(off, prog); - if (off > 255) { off = 255; }; pathbuf[off] = 0u8; pathlen = off; }; diff --git a/test/wcc/975_dirs_toolong_run.c b/test/wcc/975_dirs_toolong_run.c new file mode 100644 index 00000000..c650c5e6 --- /dev/null +++ b/test/wcc/975_dirs_toolong_run.c @@ -0,0 +1,140 @@ +/* + * 975_dirs_toolong_run — F14 #69: dirs silently truncated an over-long + * composed path and then mkdir'd the WRONG directory, returning it rc=0. + * The fix (dirs.ww build()) precomputes the composition length and + * rt_aborts loudly when it won't fit the 256B pathbuf — mirroring Hare's + * path::push too_long → `!` abort (ref/hare/dirs/xdg.ha). Both stages + * share the lib, so this is a stdlib behavior fix, not a stage divergence; + * the test still runs both driver twins for parity. + * + * One fixture program (dirs.config("prog"), print len) built per stage, + * then run under two HOME states: + * - a short HOME → exit 0 (normal path intact, no regression); + * - a ~260-byte HOME → non-zero exit (loud abort) AND no stray + * directory created on disk. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static const char *PROG = + "package main;\n" + "import dirs;\n" + "import fmt;\n" + "export fn main() int = {\n" + " let d = dirs.config(\"prog\");\n" + " fmt.println(d.len: i64);\n" + " return 0;\n" + "};\n"; + +/* run_stage — build PROG with `driver`, then exercise the two HOME states. + * Returns 0 on success, non-zero on any mismatch. */ +static int +run_stage(const char *name, const char *driver) +{ + char src[64], tmpdir[64], cmd[2048]; + snprintf(src, sizeof src, "/tmp/dtl_%d.ww", getpid()); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dtl_%d_d", getpid()); + + FILE *f = fopen(src, "wb"); + if (!f) return 1; + fputs(PROG, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "dirs_toolong[%s]: build failed\n", name); + unlink(src); rmdir(tmpdir); + return 1; + } + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/dtl_%d", tmpdir, getpid()); + + int fail = 0; + + /* short HOME — the normal path, must still exit 0. */ + snprintf(cmd, sizeof cmd, + "env -u XDG_CONFIG_HOME HOME=/tmp/dtl_%d_home %s >/dev/null 2>&1", + getpid(), outbin); + if (runwait(cmd) != 0) { + fprintf(stderr, "dirs_toolong[%s]: short HOME did not exit 0\n", + name); + fail = 1; + } + + /* ~260-byte HOME — must abort loudly (non-zero) and create no dir. */ + char longhome[512]; + int p = snprintf(longhome, sizeof longhome, "/tmp/dtl_%d_long_", getpid()); + for (int i = 0; i < 260 && p < (int)sizeof longhome - 1; i++) + longhome[p++] = 'a'; + longhome[p] = '\0'; + + snprintf(cmd, sizeof cmd, + "env -u XDG_CONFIG_HOME HOME=%s %s >/dev/null 2>&1", + longhome, outbin); + if (runwait(cmd) == 0) { + fprintf(stderr, "dirs_toolong[%s]: long HOME exited 0, " + "expected a loud abort\n", name); + fail = 1; + } + /* the truncated wrong directory must NOT have been created. */ + snprintf(cmd, sizeof cmd, "test -e %s", longhome); + if (runwait(cmd) == 0) { + fprintf(stderr, "dirs_toolong[%s]: stray directory created\n", + name); + fail = 1; + snprintf(cmd, sizeof cmd, "rm -rf %s", longhome); + (void)system(cmd); + } + + unlink(src); unlink(outbin); rmdir(tmpdir); + snprintf(cmd, sizeof cmd, "rm -rf /tmp/dtl_%d_home", getpid()); + (void)system(cmd); + return fail; +} + +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 cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int fail = 0; + fail += run_stage("cstage", cdrv); + if (access(wdrv, X_OK) == 0) + fail += run_stage("wwstage", wdrv); + else + fprintf(stderr, "dirs_toolong: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "dirs_toolong: %d stage(s) failed\n", fail); + return 1; + } + printf("dirs_toolong: ok\n"); + return 0; +}