ww source: reject raw NUL characters

This commit is contained in:
2026-08-21 23:03:14 +09:00
parent 9381f8fb8e
commit 4b9968e4e6
10 changed files with 1304 additions and 320 deletions

View File

@@ -130,6 +130,58 @@ fn rewritemidbomfile(path: str, before: str, after: str) void = {
putbomfile(path, before, after, true);
};
fn putnulfile(path: str, before: str, after: str, rewrite: bool) void = {
let flags: os.flag = os.flag.WRONLY;
if (rewrite) { flags |= os.flag.TRUNC; }
else { flags |= os.flag.CREATE | os.flag.EXCL; };
let fd: i32 = os.open(path, flags, 384i32);
assert(fd >= 0);
match (os.writeall(fd, before.ptr, before.len: u64)) {
case let n: i64 => assert(n == before.len: i64);
case let e: os.oserror => abort("write before NUL failed");
};
let nul: [1]u8;
nul[0] = 0u8;
match (os.writeall(fd, nul.ptr, 1u64)) {
case let n: i64 => assert(n == 1i64);
case let e: os.oserror => abort("write NUL failed");
};
match (os.writeall(fd, after.ptr, after.len: u64)) {
case let n: i64 => assert(n == after.len: i64);
case let e: os.oserror => abort("write after NUL failed");
};
assert(os.close(fd) == 0);
};
fn writenulfile(path: str, before: str, after: str) void = {
putnulfile(path, before, after, false);
};
fn rewritenulfile(path: str, before: str, after: str) void = {
putnulfile(path, before, after, true);
};
fn writedoublenulfile(path: str, before: str, after: str) void = {
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
assert(fd >= 0);
match (os.writeall(fd, before.ptr, before.len: u64)) {
case let n: i64 => assert(n == before.len: i64);
case let e: os.oserror => abort("write before NUL pair failed");
};
let pair: [2]u8;
pair[0] = 0u8; pair[1] = 0u8;
match (os.writeall(fd, pair.ptr, 2u64)) {
case let n: i64 => assert(n == 2i64);
case let e: os.oserror => abort("write NUL pair failed");
};
match (os.writeall(fd, after.ptr, after.len: u64)) {
case let n: i64 => assert(n == after.len: i64);
case let e: os.oserror => abort("write after NUL pair failed");
};
assert(os.close(fd) == 0);
};
fn writeexecutable(path: str, content: str) void = {
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32);
@@ -16968,3 +17020,479 @@ fn runtimepath(relative: str) str = {
&& !directoryhasnew(root));
clean(root);
};
// Go 1.26.5's physical-source reader rejects U+0000 before token context can
// turn it into comment or literal payload. Selected roots, dependencies, and
// every test-source role share that rule; target-excluded files do not.
@test fn raw_nul_is_rejected_in_every_selected_source() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
mkdirall(source);
// Direct frontend ownership and exact C/WW recovery parity.
let leading: str = strings.concat(root, "/leading.ww");
let comment: str = strings.concat(root, "/comment.ww");
let stringlit: str = strings.concat(root, "/string.ww");
let runelit: str = strings.concat(root, "/rune.ww");
let between: str = strings.concat(root, "/between.ww");
let escapechar: str = strings.concat(root, "/escape-char.ww");
let escapedigit: str = strings.concat(root, "/escape-digit.ww");
let identifier: str = strings.concat(root, "/identifier.ww");
let lineend: str = strings.concat(root, "/line-end.ww");
let blockend: str = strings.concat(root, "/block-end.ww");
let operator: str = strings.concat(root, "/operator.ww");
let number: str = strings.concat(root, "/number.ww");
let suffix: str = strings.concat(root, "/suffix.ww");
let trailing: str = strings.concat(root, "/trailing.ww");
writenulfile(leading, "",
"package main;\nfn main() void = {};\n");
writenulfile(comment, "package main;\n// NUL: ",
" here\nfn main() void = {};\n");
writenulfile(stringlit,
"package main;\nfn main() void = { let value: str = \"NUL: ",
"\"; };\n");
writenulfile(runelit,
"package main;\nfn main() void = { let value: rune = '",
"A'; };\n");
writenulfile(between, "package main;\n",
"\nfn main() void = {};\n");
writenulfile(escapechar,
"package main;\nfn main() void = { let value: str = \"\\",
"n\"; };\n");
writenulfile(escapedigit,
"package main;\nfn main() void = { let value: str = \"\\x0",
"0\"; };\n");
writenulfile(identifier, "pack",
"age main;\nfn main() void = {};\n");
writenulfile(lineend, "package main;\n// comment",
"\nfn main() void = {};\n");
writenulfile(blockend, "package main;\n/* end *",
"/\nfn main() void = {};\n");
writenulfile(operator,
"package main;\nfn main() void = { let value: bool = true =",
"= true; };\n");
writenulfile(number,
"package main;\nfn main() void = { let value: i32 = 1",
"_0; };\n");
writenulfile(suffix,
"package main;\nfn main() void = { let value: i32 = 1i",
"32; };\n");
writenulfile(trailing,
"package main;\nfn main() void = {};\n", "");
let direct: []str = [leading, comment, stringlit, runelit, between,
escapechar, escapedigit, identifier, lineend, blockend, operator,
number, suffix, trailing];
let positions: []str = [":1:1: error: invalid NUL character\n",
":2:9: error: invalid NUL character\n",
":2:43: error: invalid NUL character\n",
":2:39: error: invalid NUL character\n",
":2:1: error: invalid NUL character\n",
":2:39: error: invalid NUL character\n",
":2:41: error: invalid NUL character\n",
":1:5: error: invalid NUL character\n",
":2:11: error: invalid NUL character\n",
":2:9: error: invalid NUL character\n",
":2:44: error: invalid NUL character\n",
":2:38: error: invalid NUL character\n",
":2:39: error: invalid NUL character\n",
":3:1: error: invalid NUL character\n"];
let compilers: []str = ["w6c", "w6c_ww"];
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let di: i32 = 0;
for (di < direct.len) {
let reference: str = "";
let si: i32 = 0;
for (si < compilers.len) {
let asmout: str = strings.concat(root, "/direct-", tags[si], "-",
boundarypkgname(di), ".s");
let av: []str = [driver(compilers[si]), "-c", "--command-package",
"-o", asmout, direct[di]];
let out: commandout;
runcommand(root, strings.concat("nul-direct-", tags[si], "-",
boundarypkgname(di)), av,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0 && has(out.stderr, positions[di]));
assert(occurrences(out.stderr, "invalid NUL character") == 1);
assert(!os.exists(asmout));
if (si == 0) { reference = strings.dup(out.stderr); }
else { assert(same(reference, out.stderr)); };
si += 1;
};
di += 1;
};
// Decoder recovery is also exact for repeated bytes and the internal
// module-boundary directive used by package-unit composition.
let repeated: str = strings.concat(root, "/repeated.ww");
let directive: str = strings.concat(root, "/directive.ww");
writedoublenulfile(repeated, "pack",
"age main;\nfn main() void = {};\n");
writenulfile(directive, "//ww:module-reset ma",
"in\npackage main;\nfn main() void = {};\n");
let recoverypaths: []str = [repeated, directive];
let recoverycounts: []i32 = [2, 1];
let recoverypositions: []str = [
":1:5: error: invalid NUL character\n",
":1:21: error: invalid NUL character\n"];
let ri: i32 = 0;
for (ri < recoverypaths.len) {
let reference: str = "";
let rsi: i32 = 0;
for (rsi < compilers.len) {
let asmout: str = strings.concat(root, "/recovery-", tags[rsi],
"-", boundarypkgname(ri), ".s");
let av: []str = [driver(compilers[rsi]), "-c",
"--command-package", "-o", asmout, recoverypaths[ri]];
let out: commandout;
runcommand(root, strings.concat("nul-recovery-", tags[rsi], "-",
boundarypkgname(ri)), av,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& has(out.stderr, recoverypositions[ri])
&& occurrences(out.stderr,
"invalid NUL character") == recoverycounts[ri]
&& !os.exists(asmout));
if (ri == 0) {
assert(has(out.stderr,
":1:6: error: invalid NUL character\n"));
};
if (rsi == 0) { reference = strings.dup(out.stderr); }
else { assert(same(reference, out.stderr)); };
rsi += 1;
};
ri += 1;
};
// An escape denotes a literal value; it is not a raw source NUL.
let escaped: str = strings.concat(root, "/escaped.ww");
writefile(escaped, strings.concat(
"package main;\n",
"fn main() i32 = { let value: str = \"\\x00\"; return value.len - 1; };\n"));
let escapedasm: []str = [strings.concat(root, "/escaped-c.s"),
strings.concat(root, "/escaped-ww.s")];
let si: i32 = 0;
for (si < compilers.len) {
let av: []str = [driver(compilers[si]), "-c", "--command-package",
"-o", escapedasm[si], escaped];
let out: commandout;
runcommand(root, strings.concat("nul-escaped-", tags[si]), av,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
si += 1;
};
assert(same(readfile(escapedasm[0]), readfile(escapedasm[1])));
let dep: str = strings.concat(source, "/dep");
let app: str = strings.concat(source, "/app");
let bad: str = strings.concat(source, "/bad");
let good: str = strings.concat(source, "/good");
mkdirall(dep); mkdirall(app); mkdirall(bad); mkdirall(good);
writenulfile(strings.concat(dep, "/dep.ww"),
"package dep;\n// NUL: ",
" dependency\nexport fn value() i32 = { return 42; };\n");
writefile(strings.concat(app, "/main.ww"), strings.concat(
"package main;\nimport dep;\n",
"fn main() i32 = { return dep.value() - 42; };\n"));
writefile(strings.concat(bad, "/a.ww"),
"package main;\nfn helper() i32 = { return 1; };\n");
writenulfile(strings.concat(bad, "/main.ww"),
"package main;\n// NUL: ", strings.concat(
" root\nimport missing;\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(good, "/main.ww"),
"package main;\nfn main() i32 = { return 0; };\n");
writenulfile(strings.concat(good, "/ignored_windows.ww"),
"not package NUL: ", " and not WW\n");
// Invalid root and dependency sources stop before graph actions and public
// output. The wrong-target source remains outside the semantic input set.
let goodbins: []str = [strings.concat(root, "/good-c"),
strings.concat(root, "/good-ww")];
let invaliddiags: []str = ["", ""];
let rootdiags: []str = ["", ""];
si = 0;
for (si < stages.len) {
let depwork: str = strings.concat(root, "/dep-work-", tags[si]);
let depout: str = strings.concat(root, "/dep-output-", tags[si]);
mkdirall(depwork);
let depav: []str = [driver(stages[si]), "build", "-w", depwork,
"-I", source, "-o", depout, "app"];
let out: commandout;
runcommand(root, strings.concat("nul-dependency-", tags[si]), depav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& has(out.stderr, "/dep/dep.ww:2:9: error: invalid NUL character\n"));
assert(occurrences(out.stderr, "invalid NUL character") == 1
&& !has(out.stderr, "w6c failed"));
assert(!os.exists(depout) && directoryisempty(depwork));
invaliddiags[si] = strings.dup(out.stderr);
let badwork: str = strings.concat(root, "/bad-work-", tags[si]);
let badout: str = strings.concat(root, "/bad-output-", tags[si]);
mkdirall(badwork);
let badav: []str = [driver(stages[si]), "build", "-w", badwork,
"-I", source, "-o", badout, "bad"];
runcommand(root, strings.concat("nul-root-", tags[si]), badav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& has(out.stderr, "/bad/main.ww:2:9: error: invalid NUL character\n"));
assert(!has(out.stderr, "cannot find import")
&& occurrences(out.stderr, "invalid NUL character") == 1);
assert(!os.exists(badout) && directoryisempty(badwork));
rootdiags[si] = strings.dup(out.stderr);
let goodwork: str = strings.concat(root, "/good-work-", tags[si]);
mkdirall(goodwork);
let goodav: []str = [driver(stages[si]), "build", "-w", goodwork,
"-I", source, "-o", goodbins[si], "good"];
runcommand(root, strings.concat("nul-wrong-target-", tags[si]), goodav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0
&& !directoryhasnew(goodwork)
&& !directoryhasfragment(goodwork, ".wwtxn."));
let runav: []str = [goodbins[si]];
runcommand(root, strings.concat("nul-good-run-", tags[si]), runav,
time.second, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
si += 1;
};
assert(same(invaliddiags[0], invaliddiags[1])
&& same(rootdiags[0], rootdiags[1])
&& same(readfile(goodbins[0]), readfile(goodbins[1])));
// Production, same-package, external-package, and test-only sources all
// reject before product construction, execution, accounting, and retention.
let prodcase: str = strings.concat(source, "/prodcase");
let samecase: str = strings.concat(source, "/samecase");
let externalcase: str = strings.concat(source, "/externalcase");
let onlycase: str = strings.concat(source, "/onlycase");
let repeatcase: str = strings.concat(source, "/repeatcase");
mkdirall(prodcase); mkdirall(samecase); mkdirall(externalcase);
mkdirall(onlycase); mkdirall(repeatcase);
writefile(strings.concat(prodcase, "/more.ww"),
"package prodcase;\nfn more() i32 = { return 2; };\n");
writenulfile(strings.concat(prodcase, "/prod.ww"),
"package prodcase;\n// NUL: ",
" production\nexport fn value() i32 = { return 1; };\n");
writefile(strings.concat(prodcase, "/same_test.ww"),
"package prodcase;\n@test fn must_not_run() void = { abort(); };\n");
writefile(strings.concat(samecase, "/prod.ww"),
"package samecase;\nfn value() i32 = { return 1; };\n");
writenulfile(strings.concat(samecase, "/same_test.ww"),
"package samecase;\n// NUL: ",
" same\n@test fn must_not_run() void = { abort(); };\n");
writefile(strings.concat(externalcase, "/prod.ww"),
"package externalcase;\nexport fn value() i32 = { return 1; };\n");
writenulfile(strings.concat(externalcase, "/external_test.ww"),
"package externalcase_test;\n// NUL: ", strings.concat(
" external\nimport externalcase;\n",
"@test fn must_not_run() void = { abort(); };\n"));
writenulfile(strings.concat(onlycase, "/only_test.ww"), "",
"package onlycase;\n@test fn must_not_run() void = { abort(); };\n");
writedoublenulfile(strings.concat(repeatcase, "/prod.ww"),
"package repeatcase;\n// pair ",
" here\nfn value() i32 = { return 1; };\n");
let testids: []str = ["prodcase", "samecase", "externalcase", "onlycase",
"repeatcase"];
let testpositions: []str = ["/prodcase/prod.ww:2:9: error: invalid NUL character\n",
"/samecase/same_test.ww:2:9: error: invalid NUL character\n",
"/externalcase/external_test.ww:2:9: error: invalid NUL character\n",
"/onlycase/only_test.ww:1:1: error: invalid NUL character\n",
"/repeatcase/prod.ww:2:9: error: invalid NUL character\n"];
let testcounts: []i32 = [1, 1, 1, 1, 2];
let testdiags: []str = ["", "", "", "", ""];
si = 0;
for (si < stages.len) {
let ti: i32 = 0;
for (ti < testids.len) {
let testwork: str = strings.concat(root, "/test-work-", tags[si], "-",
boundarypkgname(ti));
let testout: str = strings.concat(root, "/test-output-", tags[si], "-",
boundarypkgname(ti));
mkdirall(testwork);
let av: []str = [driver(stages[si]), "test", "-w", testwork,
"-I", source, "-o", testout, testids[ti]];
let out: commandout;
runcommand(root, strings.concat("nul-test-", tags[si], "-",
boundarypkgname(ti)), av,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(out.stdout, "FAIL\n")
&& has(out.stderr, testpositions[ti]));
assert(occurrences(out.stderr,
"invalid NUL character") == testcounts[ti]
&& !has(out.stdout, "must_not_run")
&& !has(out.stdout, "discovered") && !has(out.stdout, "ok "));
if (ti == 4) {
assert(has(out.stderr,
"/repeatcase/prod.ww:2:10: error: invalid NUL character\n"));
};
assert(!os.exists(testout) && directoryisempty(testwork));
if (si == 0) { testdiags[ti] = strings.dup(out.stderr); }
else { assert(same(testdiags[ti], out.stderr)); };
ti += 1;
};
si += 1;
};
// A warm invalid edit commits nothing and preserves prior public and action
// bytes. Exact restoration reuses the committed generation without a tool.
let warm: str = strings.concat(source, "/warm");
mkdirall(warm);
let warmpath: str = strings.concat(warm, "/main.ww");
let warmvalid: str =
"package main;\nfn main() i32 = { return 0; };\n";
writefile(warmpath, warmvalid);
let wrapper: str = strings.concat(root, "/nul-compiler-wrapper.sh");
writeexecutable(wrapper, strings.concat(
"#!/bin/sh\n",
"printf 'compile\\n' >> \"$WW_NUL_TRACE\"\n",
"exec \"$WW_NUL_REAL\" \"$@\"\n"));
let commandsuffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
".init.unit.ww", ".init.s", ".init.o"];
si = 0;
for (si < stages.len) {
let warmwork: str = strings.concat(root, "/warm-work-", tags[si]);
let warmout: str = strings.concat(root, "/warm-output-", tags[si]);
let trace: str = strings.concat(root, "/warm-trace-", tags[si]);
mkdirall(warmwork); writefile(trace, "");
let env: []str = os.getenvs();
append(env, strings.concat("WW_W6C=", wrapper));
append(env, strings.concat("WW_NUL_TRACE=", trace));
append(env, strings.concat("WW_NUL_REAL=", driver(compilers[si])));
let av: []str = [driver(stages[si]), "build", "-w", warmwork,
"-I", source, "-o", warmout, "warm"];
let out: commandout;
runcommandenv(root, strings.concat("nul-warm-cold-", tags[si]), av,
env, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0
&& readfile(trace).len != 0);
let refs: []str = alloc([], commandsuffixes.len: u64)!;
let fi: i32 = 0;
for (fi < commandsuffixes.len) {
append(refs, strings.dup(readfile(strings.concat(warmwork,
"/warm", commandsuffixes[fi]))));
fi += 1;
};
let toolpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c",
"/.wwtool.w6a", "/.wwtool.stamp"];
let toolrefs: []str = alloc([], toolpaths.len: u64)!;
fi = 0;
for (fi < toolpaths.len) {
append(toolrefs, strings.dup(readfile(strings.concat(warmwork,
toolpaths[fi]))));
fi += 1;
};
let binref: str = strings.dup(readfile(warmout));
rewritefile(trace, "");
rewritenulfile(warmpath, "package main;\n// NUL: ",
" invalid\nfn main() i32 = { return 0; };\n");
runcommandenv(root, strings.concat("nul-warm-invalid-", tags[si]), av,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stdout.len == 0
&& has(out.stderr, ":2:9: error: invalid NUL character\n"));
assert(readfile(trace).len == 0 && same(binref, readfile(warmout)));
fi = 0;
for (fi < commandsuffixes.len) {
assert(same(refs[fi], readfile(strings.concat(warmwork,
"/warm", commandsuffixes[fi]))));
fi += 1;
};
fi = 0;
for (fi < toolpaths.len) {
assert(same(toolrefs[fi], readfile(strings.concat(warmwork,
toolpaths[fi]))));
fi += 1;
};
assert(!directoryhasnew(warmwork)
&& !directoryhasfragment(warmwork, ".wwtxn."));
rewritefile(warmpath, warmvalid);
runcommandenv(root, strings.concat("nul-warm-restored-", tags[si]), av,
env, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0
&& readfile(trace).len == 0 && same(binref, readfile(warmout)));
fi = 0;
for (fi < commandsuffixes.len) {
assert(same(refs[fi], readfile(strings.concat(warmwork,
"/warm", commandsuffixes[fi]))));
fi += 1;
};
fi = 0;
for (fi < toolpaths.len) {
assert(same(toolrefs[fi], readfile(strings.concat(warmwork,
toolpaths[fi]))));
fi += 1;
};
si += 1;
};
// Source validation is request-local under overlapping valid and invalid
// builds, including independent work, outputs, diagnostics, and cleanup.
let pcwork: str = strings.concat(root, "/parallel-c-work");
let pwwork: str = strings.concat(root, "/parallel-ww-work");
let pcout: str = strings.concat(root, "/parallel-c-output");
let pwout: str = strings.concat(root, "/parallel-ww-output");
mkdirall(pcwork); mkdirall(pwwork);
let pcav: []str = [driver("ww"), "build", "-w", pcwork,
"-I", source, "-o", pcout, "good"];
let pwav: []str = [driver("ww_ww"), "build", "-w", pwwork,
"-I", source, "-o", pwout, "bad"];
let pc: exec.command;
pc.path = pcav[0]; pc.argv = pcav; pc.env = os.getenvs(); pc.dir = repo();
pc.stdoutpath = strings.concat(root, "/parallel-c.stdout");
pc.stderrpath = strings.concat(root, "/parallel-c.stderr");
pc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
pc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let pw: exec.command;
pw.path = pwav[0]; pw.argv = pwav; pw.env = os.getenvs(); pw.dir = repo();
pw.stdoutpath = strings.concat(root, "/parallel-ww.stdout");
pw.stderrpath = strings.concat(root, "/parallel-ww.stderr");
pw.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
pw.grace = (100i64 * (time.millisecond: i64)): time.duration;
let pcp: exec.process;
let pwp: exec.process;
exec.start(&pcp, &pc); exec.start(&pwp, &pw);
let pcdone: bool = false;
let pwdone: bool = false;
for (!pcdone || !pwdone) {
if (!pcdone) { pcdone = exec.poll(&pcp); };
if (!pwdone) { pwdone = exec.poll(&pwp); };
if (!pcdone || !pwdone) {
time.sleep(time.millisecond, time.clock.monotonic);
};
};
assert(pcp.result.errno == 0 && pcp.result.cleanuperrno == 0
&& pcp.result.termination == exec.termination.EXIT
&& pcp.result.code == 0);
assert(pwp.result.errno == 0 && pwp.result.cleanuperrno == 0
&& pwp.result.termination == exec.termination.EXIT
&& pwp.result.code == 1);
assert(readfile(pc.stdoutpath).len == 0 && readfile(pc.stderrpath).len == 0);
assert(readfile(pw.stdoutpath).len == 0
&& has(readfile(pw.stderrpath), "invalid NUL character"));
assert(same(readfile(pcout), readfile(goodbins[0])) && !os.exists(pwout));
assert(directoryisempty(pwwork) && !directoryhasnew(pcwork)
&& !directoryhasfragment(pcwork, ".wwtxn."));
let runav: []str = [pcout];
let out: commandout;
runcommand(root, "nul-parallel-run", runav, time.second, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
assert(!directoryhasfragment(root, ".wwtxn.")
&& !directoryhasfragment(root, ".install")
&& !directoryhasnew(root));
clean(root);
};

View File

@@ -18,11 +18,11 @@ toklit(Arena *a, Tok t)
}
static int
runrow(const char *src, const char *expect)
runrown(const char *src, size_t len, const char *expect, int expecterrs)
{
Arena *a = newarena();
Lex l;
lexinit(&l, a, "<test>", src, strlen(src));
lexinit(&l, a, "<test>", src, len);
char *got = amalloc(a, 1);
got[0] = '\0';
@@ -48,15 +48,23 @@ runrow(const char *src, const char *expect)
got[n] = '\0';
}
int ok = strcmp(got, expect) == 0;
int ok = strcmp(got, expect) == 0
&& (expecterrs < 0 || l.errs == expecterrs);
if (!ok) {
fprintf(stderr, "lex mismatch:\n src: %s\n"
" want: %s\n got: %s\n", src, expect, got);
fprintf(stderr, "lex mismatch:\n want: %s (%d errors)\n"
" got: %s (%d errors)\n", expect, expecterrs, got,
l.errs);
}
freearena(a);
return ok;
}
static int
runrow(const char *src, const char *expect)
{
return runrown(src, strlen(src), expect, -1);
}
struct row { const char *src, *expect; };
static const struct row rows[] = {
@@ -133,11 +141,11 @@ static const struct row rows[] = {
* not the token stream. The verbatim Hare messages live at lexunicode
* in cmd/wcc/lex.c. */
static int
runerr(const char *src)
runerrn(const char *src, size_t len)
{
Arena *a = newarena();
Lex l;
lexinit(&l, a, "<test>", src, strlen(src));
lexinit(&l, a, "<test>", src, len);
for (;;) {
Tok t = lexnext(&l);
if (t.kind == TK_EOF)
@@ -151,6 +159,12 @@ runerr(const char *src)
return ok;
}
static int
runerr(const char *src)
{
return runerrn(src, strlen(src));
}
static int
runrewind(void)
{
@@ -205,6 +219,93 @@ static const char *const errrows[] = {
"'\\uDFFF'", /* top of the UTF-16 surrogate range */
};
static const struct {
const char *src;
size_t len;
const char *expect;
int errs;
} nulrows[] = {
{ "\0package main;", sizeof "\0package main;" - 1,
"package IDENT(main) ;", 1 },
{ "pack\0age main;", sizeof "pack\0age main;" - 1,
"package IDENT(main) ;", 1 },
{ "1\0_0", sizeof "1\0_0" - 1, "INT(10)", 1 },
{ "1f\0oo", sizeof "1f\0oo" - 1, "INT(1) IDENT(foo)", 1 },
{ "=\0=", sizeof "=\0=" - 1, "==", 1 },
{ "/\0/ comment\nfn", sizeof "/\0/ comment\nfn" - 1, "fn", 1 },
{ "/* end *\0/ fn", sizeof "/* end *\0/ fn" - 1, "fn", 1 },
{ "\"a\0b\"", sizeof "\"a\0b\"" - 1, "STR(ab)", 1 },
{ "'\0A'", sizeof "'\0A'" - 1, "RUNE(65)", 1 },
{ "fn\0", sizeof "fn\0" - 1, "fn", 1 },
{ "f\0\0n", sizeof "f\0\0n" - 1, "fn", 2 },
};
static int
runescapenul(void)
{
static const char line[] = "\"\\\0n\"";
static const char hex[] = "\"\\x0" "\0" "0\"";
Arena *a = newarena();
Lex l;
lexinit(&l, a, "<test>", line, sizeof line - 1);
Tok t = lexnext(&l);
int ok = t.kind == TK_STR && t.tlen == 1 && t.text[0] == '\n'
&& l.errs == 1 && lexnext(&l).kind == TK_EOF;
lexinit(&l, a, "<test>", hex, sizeof hex - 1);
t = lexnext(&l);
ok = ok && t.kind == TK_STR && t.tlen == 1 && t.text[0] == '\0'
&& l.errs == 1 && lexnext(&l).kind == TK_EOF;
if (!ok)
fprintf(stderr, "NUL escape recovery failed\n");
freearena(a);
return ok;
}
static int
runsuffixnul(void)
{
static const char src[] = "1i" "\0" "8";
Arena *a = newarena();
Lex l;
lexinit(&l, a, "<test>", src, sizeof src - 1);
Tok t = lexnext(&l);
int ok = t.kind == TK_INT && t.v.uval == 1 && t.tsuffix != NULL
&& strcmp(t.tsuffix, "i8") == 0 && l.errs == 1
&& lexnext(&l).kind == TK_EOF;
if (!ok)
fprintf(stderr, "NUL typed-suffix recovery failed\n");
freearena(a);
return ok;
}
static int
runlongdirective(void)
{
static const char pre[] = "//ww:module ";
static const char tail[] = "\nfn";
const size_t pathlen = 16384;
const size_t len = sizeof pre - 1 + pathlen + sizeof tail - 1;
char *src = malloc(len);
if (!src)
return 0;
memcpy(src, pre, sizeof pre - 1);
memset(src + sizeof pre - 1, 'a', pathlen);
memcpy(src + sizeof pre - 1 + pathlen, tail, sizeof tail - 1);
Arena *a = newarena();
Lex l;
lexinit(&l, a, "<test>", src, len);
Tok m = lexnext(&l);
Tok f = lexnext(&l);
int ok = m.kind == TK_MODPATH && m.tlen == pathlen
&& m.text[0] == 'a' && m.text[pathlen - 1] == 'a'
&& f.kind == TK_FN && l.errs == 0;
if (!ok)
fprintf(stderr, "long module directive failed\n");
freearena(a);
free(src);
return ok;
}
int
main(void)
{
@@ -225,6 +326,19 @@ main(void)
fail++;
}
}
for (size_t i = 0; i < sizeof nulrows / sizeof nulrows[0]; i++) {
if (!runrown(nulrows[i].src, nulrows[i].len,
nulrows[i].expect, nulrows[i].errs)) {
fprintf(stderr, "NUL row %zu failed\n", i);
fail++;
}
}
if (!runescapenul())
fail++;
if (!runsuffixnul())
fail++;
if (!runlongdirective())
fail++;
if (fail) {
fprintf(stderr, "%d/%zu lex tests failed\n", fail,
sizeof rows / sizeof rows[0]);