ww source: reject malformed UTF-8
This commit is contained in:
118
cmd/wcc/lex.c
118
cmd/wcc/lex.c
@@ -16,6 +16,61 @@ bomat(const char *src, u64 len, u64 pos)
|
|||||||
&& (unsigned char)src[pos + 2] == 0xbf;
|
&& (unsigned char)src[pos + 2] == 0xbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Return the width of the valid UTF-8 sequence beginning at pos, or zero.
|
||||||
|
* This is the same scalar-value partition used by unicode/utf8.DecodeRune:
|
||||||
|
* overlong encodings, surrogates, values above U+10FFFF, stray continuation
|
||||||
|
* bytes, and truncated sequences are invalid. */
|
||||||
|
static int
|
||||||
|
utf8seqwidth(const char *src, u64 len, u64 pos)
|
||||||
|
{
|
||||||
|
if (pos >= len)
|
||||||
|
return 0;
|
||||||
|
const unsigned char *s = (const unsigned char *)src;
|
||||||
|
unsigned char c = s[pos];
|
||||||
|
if (c < 0x80)
|
||||||
|
return 1;
|
||||||
|
if (c >= 0xc2 && c <= 0xdf && len - pos >= 2
|
||||||
|
&& s[pos + 1] >= 0x80 && s[pos + 1] <= 0xbf)
|
||||||
|
return 2;
|
||||||
|
if (len - pos >= 3 && s[pos + 2] >= 0x80
|
||||||
|
&& s[pos + 2] <= 0xbf) {
|
||||||
|
unsigned char c1 = s[pos + 1];
|
||||||
|
if ((c == 0xe0 && c1 >= 0xa0 && c1 <= 0xbf)
|
||||||
|
|| (c >= 0xe1 && c <= 0xec && c1 >= 0x80 && c1 <= 0xbf)
|
||||||
|
|| (c == 0xed && c1 >= 0x80 && c1 <= 0x9f)
|
||||||
|
|| (c >= 0xee && c <= 0xef && c1 >= 0x80 && c1 <= 0xbf))
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
if (len - pos >= 4 && s[pos + 2] >= 0x80
|
||||||
|
&& s[pos + 2] <= 0xbf && s[pos + 3] >= 0x80
|
||||||
|
&& s[pos + 3] <= 0xbf) {
|
||||||
|
unsigned char c1 = s[pos + 1];
|
||||||
|
if ((c == 0xf0 && c1 >= 0x90 && c1 <= 0xbf)
|
||||||
|
|| (c >= 0xf1 && c <= 0xf3 && c1 >= 0x80 && c1 <= 0xbf)
|
||||||
|
|| (c == 0xf4 && c1 >= 0x80 && c1 <= 0x8f))
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The lexer deliberately keeps valid non-ASCII source byte-oriented. Decide
|
||||||
|
* whether one raw byte belongs to a complete valid UTF-8 sequence without
|
||||||
|
* changing that established token model. A continuation byte is valid only
|
||||||
|
* when a valid sequence beginning at most three bytes earlier contains it. */
|
||||||
|
static int
|
||||||
|
utf8bytevalid(const char *src, u64 len, u64 pos)
|
||||||
|
{
|
||||||
|
if (utf8seqwidth(src, len, pos) != 0)
|
||||||
|
return 1;
|
||||||
|
unsigned char c = (unsigned char)src[pos];
|
||||||
|
if (c < 0x80 || c > 0xbf)
|
||||||
|
return 0;
|
||||||
|
for (u64 back = 1; back <= 3 && back <= pos; back++)
|
||||||
|
if (utf8seqwidth(src, len, pos - back) > (int)back)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
lexinit(Lex *l, Arena *a, const char *file, const char *src, u64 len)
|
lexinit(Lex *l, Arena *a, const char *file, const char *src, u64 len)
|
||||||
{
|
{
|
||||||
@@ -47,16 +102,39 @@ lskipnul(Lex *l)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return the raw offset of a logical byte lookahead. Raw NUL bytes do not
|
/* Go 1.26.5 syntax.source.nextch reports and discards one byte whenever
|
||||||
* occupy a slot in the token stream: Go's source.nextch diagnoses them and
|
* utf8.DecodeRune returns RuneError with width one. Drain the same malformed
|
||||||
* immediately resumes decoding at the following character. */
|
* bytes before they can affect token recovery. */
|
||||||
|
static void
|
||||||
|
lskiputf8(Lex *l)
|
||||||
|
{
|
||||||
|
while (l->pos < l->srclen
|
||||||
|
&& (unsigned char)l->src[l->pos] >= 0x80
|
||||||
|
&& !utf8bytevalid(l->src, l->srclen, l->pos)) {
|
||||||
|
Pos p = { l->file, l->line, l->col };
|
||||||
|
l->pos++;
|
||||||
|
l->col++;
|
||||||
|
errorf(p, "invalid UTF-8 encoding");
|
||||||
|
l->errs++;
|
||||||
|
l->utf8count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the raw offset of a logical byte lookahead. Raw NUL and malformed
|
||||||
|
* UTF-8 bytes do not occupy a slot in the token stream: Go's source.nextch
|
||||||
|
* diagnoses them and immediately resumes at the following byte. */
|
||||||
static u64
|
static u64
|
||||||
lrawoff(Lex *l, u64 ahead)
|
lrawoff(Lex *l, u64 ahead)
|
||||||
{
|
{
|
||||||
u64 p = l->pos;
|
u64 p = l->pos;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
while (p < l->srclen && l->src[p] == '\0')
|
while (p < l->srclen) {
|
||||||
|
unsigned char c = (unsigned char)l->src[p];
|
||||||
|
if (c != 0 && (c < 0x80
|
||||||
|
|| utf8bytevalid(l->src, l->srclen, p)))
|
||||||
|
break;
|
||||||
p++;
|
p++;
|
||||||
|
}
|
||||||
if (ahead == 0 || p >= l->srclen)
|
if (ahead == 0 || p >= l->srclen)
|
||||||
return p;
|
return p;
|
||||||
p++;
|
p++;
|
||||||
@@ -77,6 +155,11 @@ lpeek(Lex *l, u64 ahead)
|
|||||||
lskipnul(l);
|
lskipnul(l);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (c >= 0x80
|
||||||
|
&& !utf8bytevalid(l->src, l->srclen, l->pos)) {
|
||||||
|
lskiputf8(l);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (ahead == 0) {
|
if (ahead == 0) {
|
||||||
if (bomat(l->src, l->srclen, l->pos))
|
if (bomat(l->src, l->srclen, l->pos))
|
||||||
return 0xfeff;
|
return 0xfeff;
|
||||||
@@ -102,6 +185,11 @@ lget(Lex *l)
|
|||||||
lskipnul(l);
|
lskipnul(l);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (c >= 0x80
|
||||||
|
&& !utf8bytevalid(l->src, l->srclen, l->pos)) {
|
||||||
|
lskiputf8(l);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (bomat(l->src, l->srclen, l->pos)) {
|
if (bomat(l->src, l->srclen, l->pos)) {
|
||||||
Pos p = { l->file, l->line, l->col };
|
Pos p = { l->file, l->line, l->col };
|
||||||
l->pos += 3;
|
l->pos += 3;
|
||||||
@@ -128,17 +216,20 @@ lpos(Lex *l)
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy one raw source span into token text while omitting diagnosed NUL
|
/* Copy one raw source span into token text while omitting diagnosed NUL and
|
||||||
* bytes. This keeps keyword, numeric, suffix, and directive recovery on the
|
* malformed UTF-8 bytes. This keeps keyword, numeric, suffix, and directive
|
||||||
* same logical character stream as lpeek/lget. */
|
* recovery on the same logical character stream as lpeek/lget. */
|
||||||
static char *
|
static char *
|
||||||
lexspan(Lex *l, u64 begin, u64 end, u64 *len)
|
lexspan(Lex *l, u64 begin, u64 end, u64 *len)
|
||||||
{
|
{
|
||||||
char *s = amalloc(l->a, end - begin + 1);
|
char *s = amalloc(l->a, end - begin + 1);
|
||||||
u64 j = 0;
|
u64 j = 0;
|
||||||
for (u64 i = begin; i < end; i++)
|
for (u64 i = begin; i < end; i++) {
|
||||||
if (l->src[i] != '\0')
|
unsigned char c = (unsigned char)l->src[i];
|
||||||
|
if (c != 0 && (c < 0x80
|
||||||
|
|| utf8bytevalid(l->src, l->srclen, i)))
|
||||||
s[j++] = l->src[i];
|
s[j++] = l->src[i];
|
||||||
|
}
|
||||||
s[j] = '\0';
|
s[j] = '\0';
|
||||||
*len = j;
|
*len = j;
|
||||||
return s;
|
return s;
|
||||||
@@ -173,13 +264,14 @@ linecomment(Lex *l)
|
|||||||
{
|
{
|
||||||
u64 begin = l->pos;
|
u64 begin = l->pos;
|
||||||
u64 nulbegin = l->nulcount;
|
u64 nulbegin = l->nulcount;
|
||||||
|
u64 utf8begin = l->utf8count;
|
||||||
int c;
|
int c;
|
||||||
while ((c = lpeek(l, 0)) >= 0 && c != '\n')
|
while ((c = lpeek(l, 0)) >= 0 && c != '\n')
|
||||||
lget(l);
|
lget(l);
|
||||||
u64 end = l->pos;
|
u64 end = l->pos;
|
||||||
u64 n;
|
u64 n;
|
||||||
const char *body;
|
const char *body;
|
||||||
if (l->nulcount == nulbegin) {
|
if (l->nulcount == nulbegin && l->utf8count == utf8begin) {
|
||||||
n = end - begin;
|
n = end - begin;
|
||||||
body = l->src + begin;
|
body = l->src + begin;
|
||||||
} else {
|
} else {
|
||||||
@@ -414,6 +506,7 @@ lexnum(Lex *l, Pos start)
|
|||||||
Tok t = (Tok){ TK_INT, start, NULL, 0, {0}, TK_NONE };
|
Tok t = (Tok){ TK_INT, start, NULL, 0, {0}, TK_NONE };
|
||||||
u64 begin = l->pos;
|
u64 begin = l->pos;
|
||||||
u64 nulbegin = l->nulcount;
|
u64 nulbegin = l->nulcount;
|
||||||
|
u64 utf8begin = l->utf8count;
|
||||||
int base = 10;
|
int base = 10;
|
||||||
int isfloat = 0;
|
int isfloat = 0;
|
||||||
int c = lpeek(l, 0);
|
int c = lpeek(l, 0);
|
||||||
@@ -454,7 +547,7 @@ lexnum(Lex *l, Pos start)
|
|||||||
|
|
||||||
u64 rawend = l->pos;
|
u64 rawend = l->pos;
|
||||||
u64 n;
|
u64 n;
|
||||||
if (l->nulcount == nulbegin) {
|
if (l->nulcount == nulbegin && l->utf8count == utf8begin) {
|
||||||
n = rawend - begin;
|
n = rawend - begin;
|
||||||
t.text = astrndup(l->a, l->src + begin, n);
|
t.text = astrndup(l->a, l->src + begin, n);
|
||||||
} else {
|
} else {
|
||||||
@@ -511,11 +604,12 @@ lexident(Lex *l, Pos start)
|
|||||||
{
|
{
|
||||||
u64 begin = l->pos;
|
u64 begin = l->pos;
|
||||||
u64 nulbegin = l->nulcount;
|
u64 nulbegin = l->nulcount;
|
||||||
|
u64 utf8begin = l->utf8count;
|
||||||
while (isidcont(lpeek(l, 0)))
|
while (isidcont(lpeek(l, 0)))
|
||||||
lget(l);
|
lget(l);
|
||||||
u64 n;
|
u64 n;
|
||||||
const char *p;
|
const char *p;
|
||||||
int filtered = l->nulcount != nulbegin;
|
int filtered = l->nulcount != nulbegin || l->utf8count != utf8begin;
|
||||||
if (!filtered) {
|
if (!filtered) {
|
||||||
n = l->pos - begin;
|
n = l->pos - begin;
|
||||||
p = l->src + begin;
|
p = l->src + begin;
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ struct Lex {
|
|||||||
Arena *a; /* token-text arena */
|
Arena *a; /* token-text arena */
|
||||||
int errs;
|
int errs;
|
||||||
u64 nulcount; /* raw NUL bytes diagnosed by the source decoder */
|
u64 nulcount; /* raw NUL bytes diagnosed by the source decoder */
|
||||||
|
u64 utf8count; /* malformed UTF-8 bytes diagnosed/filtered */
|
||||||
int modreset; /* a `//ww:module-reset` directive was seen in
|
int modreset; /* a `//ww:module-reset` directive was seen in
|
||||||
* the last skipped run; lexnext emits TK_MODRESET
|
* the last skipped run; lexnext emits TK_MODRESET
|
||||||
* before the next real token. */
|
* before the next real token. */
|
||||||
|
|||||||
@@ -8828,6 +8828,211 @@ This is command parsing and output presentation only. No persisted-byte
|
|||||||
contract changes: build workdir format remains `18`, test workdir format
|
contract changes: build workdir format remains `18`, test workdir format
|
||||||
remains `19`, and semantic storage format remains `3`.
|
remains `19`, and semantic storage format remains `3`.
|
||||||
|
|
||||||
|
### 11.45 Implemented selected-source malformed UTF-8 rejection
|
||||||
|
|
||||||
|
Every malformed UTF-8 byte in an eligible selected physical `.ww` source is
|
||||||
|
rejected at its 1-based physical line and raw-byte column with exactly
|
||||||
|
`invalid UTF-8 encoding`. The source decoder consumes that byte, omits it from
|
||||||
|
the logical character stream, and resumes. Consequently, a malformed
|
||||||
|
multi-byte spelling is diagnosed once for every byte that decodes as U+FFFD
|
||||||
|
with width one, while a correctly encoded U+FFFD remains valid. Validation is
|
||||||
|
source-wide: comment and literal contexts do not hide malformed bytes, and an
|
||||||
|
invalid byte cannot split an identifier, number or suffix, operator, escape,
|
||||||
|
comment delimiter, package keyword, or import spelling into a different token.
|
||||||
|
|
||||||
|
This section adds only malformed-UTF-8 validation. It does not reopen the
|
||||||
|
per-source leading-BOM contract in §11.42, raw-U+0000 rejection in §11.43, or
|
||||||
|
exact output-option parsing in §11.44. BOM, raw NUL, and malformed UTF-8 remain
|
||||||
|
independent positioned source conditions and are handled in raw-byte order.
|
||||||
|
|
||||||
|
#### Pinned evidence, applicability, and fact classification
|
||||||
|
|
||||||
|
The sole authority is official Go 1.26.5 at
|
||||||
|
`c19862e5f8415b4f24b189d065ed739517c548ba`:
|
||||||
|
|
||||||
|
- compiler reader `(*source).init`, `(*source).pos`/`error`, and
|
||||||
|
`(*source).nextch` establish the 1-based byte-positioned decoding boundary in
|
||||||
|
[`cmd/compile/internal/syntax/source.go`, lines 60–88 and 113–165](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/source.go#L60-L165);
|
||||||
|
- `(*source).nextch` calls `utf8.DecodeRune` at lines 149–150, and a U+FFFD
|
||||||
|
result of width one reports exactly `invalid UTF-8 encoding`, consumes that
|
||||||
|
one byte, and resumes at lines 152–154;
|
||||||
|
- compiler-scanner `TestScanErrors` asserts the positioned malformed byte and
|
||||||
|
truncated-`EF` regression in
|
||||||
|
[`cmd/compile/internal/syntax/scanner_test.go`, lines 587–600 and 658](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner_test.go#L587-L658);
|
||||||
|
- compiler testdata requires UTF-8 errors in interpreted and raw strings,
|
||||||
|
comments, identifiers, and ordinary source in
|
||||||
|
[`test/nul1.go`, lines 7–52](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/nul1.go#L7-L52);
|
||||||
|
- the independent public scanner corroborates width-one malformed decoding in
|
||||||
|
[`go/scanner/scanner.go`, lines 63–108](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/scanner/scanner.go#L63-L108)
|
||||||
|
and its literal test at
|
||||||
|
[`go/scanner/scanner_test.go`, lines 810–811](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/scanner/scanner_test.go#L810-L811);
|
||||||
|
- directory enumeration, filename eligibility, source reading, and test-role
|
||||||
|
classification are ordered by `Context.Import`, `Context.matchFile`, and
|
||||||
|
`Context.goodOSArchFile` in
|
||||||
|
[`go/build/build.go`, lines 859–953 and 1005–1036](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L859-L1036),
|
||||||
|
[`Context.matchFile`, lines 1438–1509](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L1438-L1509),
|
||||||
|
and
|
||||||
|
[`Context.goodOSArchFile`, lines 1980–2027](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L1980-L2027),
|
||||||
|
while internal/external test variants consume those selected lists in
|
||||||
|
[`cmd/go/internal/load/test.go`, `TestPackagesAndErrors`, lines 85–102 and 175–240](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/test.go#L85-L240).
|
||||||
|
|
||||||
|
Those decoder branches, source-selection branches, and official assertions are
|
||||||
|
**behavior directly implemented or asserted by pinned Go**. Applying the same
|
||||||
|
per-selected-physical-source rule to `.ww` inputs, after WW's fixed-target
|
||||||
|
filename/test-role eligibility and before its package/import interpretation,
|
||||||
|
is **behavior derived from the pinned implementation**. It honestly applies to
|
||||||
|
WW's declared UTF-8 source without adding modules, manifests, registries, lock
|
||||||
|
files, caches, databases, CAS, network resolution, quoted/grouped/dot/general
|
||||||
|
imports, or a source-level build language.
|
||||||
|
|
||||||
|
Before this change, the following observations were **directly measured WW
|
||||||
|
behavior**:
|
||||||
|
|
||||||
|
- direct `w6c` and `w6c_ww` accepted comments containing stray continuation
|
||||||
|
`80`, lead `FF`, overlong `C0 80`, and surrogate `ED A0 80` bytes, exited
|
||||||
|
zero with empty streams, and emitted byte-identical 89-byte assembly with
|
||||||
|
SHA-256
|
||||||
|
`7385e3ce0107324edc94ee4377c5c2b0262c5b690939cb54f599ed09f9730db8`;
|
||||||
|
- both direct compilers accepted raw `FF` in a string and emitted
|
||||||
|
byte-identical 236-byte assembly with SHA-256
|
||||||
|
`4dbe79617badb56d541526ca276a9867d0d717ad9aa0c540f629882f87e3f3ad`,
|
||||||
|
while valid Korean and accented controls remained accepted and
|
||||||
|
stage-identical;
|
||||||
|
- both public build stages accepted malformed comments and literals, published
|
||||||
|
runnable binaries, and built and ran an imported dependency containing
|
||||||
|
malformed UTF-8; that dependency executable was 4,317 stage-identical bytes
|
||||||
|
with SHA-256
|
||||||
|
`f792da043743cc512c9d9a41deb4cb4af1e42d35e2716fcd5b57355fd7f566eb`;
|
||||||
|
- malformed production, same-package, external-package, and test-only selected
|
||||||
|
sources ran successfully in both stages; one same-package compile-only binary
|
||||||
|
was 112,829 stage-identical bytes with SHA-256
|
||||||
|
`108150d071a3ab4264d021d2c3fdc7a94ff71a00153fdf14bfe8e26520cc5da2`;
|
||||||
|
- a corrupted `pack<FF>age` produced a package-clause diagnostic plus
|
||||||
|
`unexpected character 0xff` in Cstage but a generic `unexpected character`
|
||||||
|
in WWstage, with 176-byte versus 171-byte stderr; corrupted imports reached
|
||||||
|
analogous fallback recovery rather than the pinned decoder diagnostic;
|
||||||
|
- malformed source before `import nowhere;` allowed missing-import resolution
|
||||||
|
to win, while a malformed same-package test keyword reached the
|
||||||
|
coordinator's unpositioned `invalid or missing package clause`; and
|
||||||
|
- malformed wrong-target and ordinary-build-excluded test files were ignored,
|
||||||
|
and reverse-created selected files were still diagnosed in byte-sorted name
|
||||||
|
order.
|
||||||
|
|
||||||
|
No installed host Go result supplies any authority or measurement above.
|
||||||
|
|
||||||
|
#### True ownership and complete four-axis result
|
||||||
|
|
||||||
|
The semantic owners are the source-decoder twins in `cmd/wcc/lex.c` and
|
||||||
|
`lib/ww/syntax/lex.ww`, plus the selected-physical-source preflight in
|
||||||
|
`internal/wwpackage/package.ww`. The decoders enforce bytewise recovery for
|
||||||
|
complete direct and composed compiler inputs. The shared coordinator enforces
|
||||||
|
the same validation before its textual package-clause classifier and import
|
||||||
|
discovery, so a coordinator fallback cannot outrank the physical-source error.
|
||||||
|
The Cstage/WWstage package-unit composers only transport already admitted
|
||||||
|
source bytes and are not additional owners.
|
||||||
|
|
||||||
|
- **Go-like build:** each eligible selected root, library, or dependency source
|
||||||
|
is validated before its imports complete the graph or any compiler,
|
||||||
|
assembler, archiver, linker, install, publication, or runtime action starts.
|
||||||
|
A source error in a selected root precedes missing, self, cycle, `internal`,
|
||||||
|
vendor, and imported-command resolution. Wrong-target and test-only files
|
||||||
|
excluded from ordinary build are not semantic inputs and are not decoded.
|
||||||
|
- **Go-like test:** production, same-package, external-package, and test-only
|
||||||
|
selected physical sources are validated before grouping or variant/product
|
||||||
|
construction. Rejection builds no support action or generated main, starts
|
||||||
|
no test process, emits no accounting or package `ok` line, and retains or
|
||||||
|
publishes no executable. An attributable explicit request keeps its existing
|
||||||
|
command-owned final `FAIL\n` presentation.
|
||||||
|
- **Go-like package:** each selected physical file owns its positioned errors;
|
||||||
|
selected filenames retain byte-sorted order. Correctly encoded non-ASCII
|
||||||
|
content remains legal in WW's permitted comment/literal contexts. Declared
|
||||||
|
package names, source roles, package conflicts, command/test classification,
|
||||||
|
and variant boundaries do not change.
|
||||||
|
- **Go-like import:** a malformed byte is filtered before it can manufacture,
|
||||||
|
split, or change an import occurrence, qualifier, or edge. Valid spelling,
|
||||||
|
aliases, file-scoped binding, local/vendor/internal resolution, visibility,
|
||||||
|
cycle detection, graph order, and initialization remain unchanged.
|
||||||
|
|
||||||
|
Canonical dotted package and import identity remains exact and
|
||||||
|
case-sensitive. Physical directory, declared name, alias, path leaf, filename,
|
||||||
|
source bytes, artifact name, output path, and linker order remain loader,
|
||||||
|
runtime, or presentation metadata only where already specified; none becomes
|
||||||
|
package, graph, action, symbol, `.wwi`, publication, or persistence identity.
|
||||||
|
|
||||||
|
#### Loading, graph, action, runtime, and diagnostics
|
||||||
|
|
||||||
|
Filename and test-role eligibility occurs first. The shared preflight then
|
||||||
|
scans eligible selected files in existing byte-sorted order, reporting every
|
||||||
|
malformed byte in the first invalid physical file in position order before
|
||||||
|
package-clause classification. Lines and columns advance by raw source bytes.
|
||||||
|
A legal leading BOM still advances three columns, raw NUL keeps its own exact
|
||||||
|
diagnostic, and the three source conditions interleave without one being
|
||||||
|
reclassified as another.
|
||||||
|
|
||||||
|
An invalid source completes no package node, import edge, test variant,
|
||||||
|
support action, or generated-main action. No compiler, assembler, archiver,
|
||||||
|
linker, installer, or runtime process is scheduled for that invalid request.
|
||||||
|
Independent valid siblings and requests keep the established command-global
|
||||||
|
planning and scheduling rules; validation adds no global state and cannot
|
||||||
|
cancel or mutate them. Valid loading, graph identity, action order,
|
||||||
|
initialization, runtime behavior, result order, and output selection are
|
||||||
|
explicit non-effects.
|
||||||
|
|
||||||
|
Diagnostics use exact text `invalid UTF-8 encoding` with path, 1-based line,
|
||||||
|
and 1-based raw-byte column. Each width-one malformed decode is consumed and
|
||||||
|
removed before token recovery, preventing a second stage-specific package,
|
||||||
|
import, identifier, literal, operator, escape, or EOF interpretation. Complete
|
||||||
|
direct frontend inputs report all malformed bytes. Public package/test loading
|
||||||
|
uses the same sequential physical-source preflight and therefore preserves
|
||||||
|
Cstage/WWstage diagnostic-byte parity and source-before-resolution precedence.
|
||||||
|
Existing valid-input, BOM, NUL, package, import, and output-option diagnostics
|
||||||
|
retain their owners and wording.
|
||||||
|
|
||||||
|
#### Publication, persistence, artifacts, and failure lifecycle
|
||||||
|
|
||||||
|
Cold malformed-source rejection creates no synthetic unit, `.wwi`, assembly,
|
||||||
|
object, archive, executable, retained test binary, result status, capture, or
|
||||||
|
published output. It leaves no `.new`, `.install`, `.wwtxn.*`, adjacent
|
||||||
|
`.sepwork`, tool-stage transaction, or scratch residue. Invalid input has no
|
||||||
|
artifact-byte comparison beyond identical absence.
|
||||||
|
|
||||||
|
Warm rejection commits no generation and preserves every prior unit,
|
||||||
|
interface, assembly, object, archive, tool record, stamp, executable, retained
|
||||||
|
binary, and public output byte for byte. Because staging has not begun, the
|
||||||
|
source branch requires no new rollback mechanism. Restoring the exact valid
|
||||||
|
source follows ordinary content comparison and may reuse the prior committed
|
||||||
|
generation. Valid Cstage and WWstage unit, interface, assembly, object,
|
||||||
|
archive, generated-main, executable, and retained-test bytes keep their
|
||||||
|
existing byte-identity contract.
|
||||||
|
|
||||||
|
Producer failure, runtime failure, publication-only failure, and cleanup-only
|
||||||
|
failure remain downstream owners and are not redefined; malformed-source
|
||||||
|
rejection makes those phases unreachable for the invalid request. Validation
|
||||||
|
state is source/request-local. Concurrent valid and invalid requests retain
|
||||||
|
independent workdirs, outputs, captures, diagnostics, processes, and
|
||||||
|
transactions. The change adds no process, wait, signal, timeout, cancellation,
|
||||||
|
or interruption boundary, so existing process-group ownership, interruption,
|
||||||
|
rollback, and cleanup remain unchanged. The recipe-owned fixed
|
||||||
|
`out/bootstrap` tree is not transaction residue.
|
||||||
|
|
||||||
|
#### Proof, twin parity, and formats
|
||||||
|
|
||||||
|
Focused C and WW lexer proofs cover valid encodings and encoded U+FFFD;
|
||||||
|
invalid leads and continuations; overlong, surrogate, out-of-range, truncated,
|
||||||
|
and repeated malformed spellings; token boundaries; and BOM/NUL interaction.
|
||||||
|
The WW-native `malformed_utf8_is_rejected_in_every_selected_source` observer
|
||||||
|
owns direct compiler, root/dependency build, source/import precedence, every
|
||||||
|
test source role, wrong-target selection, cold/warm rejection, exact
|
||||||
|
restoration and reuse, valid sibling concurrency, artifact absence, residue
|
||||||
|
cleanup, stage diagnostic parity, and valid-artifact byte parity. Concrete
|
||||||
|
post-change byte counts, hashes, and gate results are recorded only after
|
||||||
|
focused and full validation; they are not inferred from the implementation.
|
||||||
|
|
||||||
|
No format bump. This changes invalid-source acceptance and diagnostics only;
|
||||||
|
the valid persisted-byte contract is unchanged. Build workdir format remains
|
||||||
|
`18`, test workdir format remains `19`, and semantic storage format remains
|
||||||
|
`3`. No test-result cache is introduced.
|
||||||
|
|
||||||
## 12. Candidate architectures and hard-gate decision
|
## 12. Candidate architectures and hard-gate decision
|
||||||
|
|
||||||
Five candidates were developed as coherent systems, not as feature bins.
|
Five candidates were developed as coherent systems, not as feature bins.
|
||||||
|
|||||||
18
docs/spec.md
18
docs/spec.md
@@ -60,6 +60,19 @@ every other source position, including inside string and rune literals and
|
|||||||
comments. Apart from that marker rule, the lexer operates on bytes and
|
comments. Apart from that marker rule, the lexer operates on bytes and
|
||||||
non-ASCII bytes are legal only inside string and rune literals and comments.
|
non-ASCII bytes are legal only inside string and rune literals and comments.
|
||||||
|
|
||||||
|
Every malformed UTF-8 byte in an eligible selected physical source produces
|
||||||
|
one positioned `invalid UTF-8 encoding` error at its 1-based physical line and
|
||||||
|
raw-byte column. The byte is consumed and omitted from the lexer's logical
|
||||||
|
character stream before token recovery. A malformed multi-byte spelling is
|
||||||
|
therefore diagnosed once for each byte that decodes as U+FFFD with width one;
|
||||||
|
a correctly encoded U+FFFD is valid. Malformed bytes cannot split an
|
||||||
|
identifier, number or suffix, operator, escape, comment delimiter, package
|
||||||
|
keyword, or import spelling into different tokens. Filename and test-role
|
||||||
|
eligibility precede validation, so an excluded physical file contributes no
|
||||||
|
UTF-8 diagnostic. This rule is independent of the leading-BOM and raw-NUL
|
||||||
|
rules below and does not make source bytes package, import, graph, action,
|
||||||
|
artifact, publication, or persistence identity.
|
||||||
|
|
||||||
Each raw byte `00` (U+0000) is invalid at every physical source position,
|
Each raw byte `00` (U+0000) is invalid at every physical source position,
|
||||||
including in comments and string or rune literal text. It produces one
|
including in comments and string or rune literal text. It produces one
|
||||||
positioned `invalid NUL character` error at that byte's source position and
|
positioned `invalid NUL character` error at that byte's source position and
|
||||||
@@ -296,7 +309,10 @@ ImportPath = ident { "." ident } .
|
|||||||
Production excludes selected `*_test.ww`; test variants classify only those
|
Production excludes selected `*_test.ww`; test variants classify only those
|
||||||
selected test files. An excluded file contributes no declarations, imports,
|
selected test files. An excluded file contributes no declarations, imports,
|
||||||
filename collision, package edge, action, export, artifact, initialization,
|
filename collision, package edge, action, export, artifact, initialization,
|
||||||
test, or persistent invalidation. After eligibility, two distinct selected
|
test, diagnostic, or persistent invalidation. After eligibility, each
|
||||||
|
selected physical source is validated for malformed UTF-8 and raw NUL before
|
||||||
|
package-clause or import interpretation, in the existing byte-sorted file
|
||||||
|
order. After that source preflight, two distinct selected
|
||||||
basenames in one canonical directory that are equal under Go 1.26.5 Unicode
|
basenames in one canonical directory that are equal under Go 1.26.5 Unicode
|
||||||
simple folding are rejected after the coordinator's required package-clause
|
simple folding are rejected after the coordinator's required package-clause
|
||||||
classification and production `@test` validation parses, but before the
|
classification and production `@test` validation parses, but before the
|
||||||
|
|||||||
@@ -459,6 +459,48 @@ remains a valid literal value. This rule is limited to raw U+0000; malformed
|
|||||||
UTF-8 and the independent per-source BOM rule retain their existing, separate
|
UTF-8 and the independent per-source BOM rule retain their existing, separate
|
||||||
contracts.
|
contracts.
|
||||||
|
|
||||||
|
After filename and test-role eligibility and before package-clause
|
||||||
|
classification, every malformed UTF-8 byte in a selected production,
|
||||||
|
same-package test, external-package test, or test-only source is one positioned
|
||||||
|
`invalid UTF-8 encoding` error. The shared coordinator scans selected physical
|
||||||
|
files in existing byte-sorted order and reports all malformed bytes in the
|
||||||
|
first invalid file before package grouping, import discovery, or delegated
|
||||||
|
tools. The C and WW source decoders consume each malformed byte, omit it from
|
||||||
|
their logical streams, and resume, so the byte cannot manufacture or alter a
|
||||||
|
package token, import spelling, binding, or edge or produce stage-specific
|
||||||
|
fallback recovery. Correctly encoded U+FFFD and other valid non-ASCII text in
|
||||||
|
WW's permitted comment and literal contexts remain valid. A malformed
|
||||||
|
multi-byte spelling is diagnosed byte by byte according to width-one UTF-8
|
||||||
|
decodes. Raw NUL and per-source BOM diagnostics remain independent and
|
||||||
|
interleave with malformed-byte diagnostics in physical byte order.
|
||||||
|
|
||||||
|
Malformed selected source rejects before package/test variant construction,
|
||||||
|
generated main, compiler, assembler, archiver, linker, runtime, accounting,
|
||||||
|
retention, or publication. The existing attributable explicit test request
|
||||||
|
still owns its final `FAIL\n`; no package `ok` line or test-result cache is
|
||||||
|
created. Cold rejection leaves no unit, `.wwi`, assembly, object, archive,
|
||||||
|
binary, capture, status, stage, or transaction residue. Warm rejection commits
|
||||||
|
nothing and preserves the prior generation and public output byte for byte;
|
||||||
|
restoring the valid bytes follows ordinary exact-content reuse. Producer,
|
||||||
|
runtime, publication-only, and cleanup failures are unchanged because source
|
||||||
|
rejection precedes those phases. Validation is source/request-local, adds no
|
||||||
|
process or signal boundary, cannot contaminate an overlapping valid request,
|
||||||
|
and leaves interruption and owned-process cleanup with their established
|
||||||
|
owners. Wrong-target and ordinary-build-excluded test sources remain unread by
|
||||||
|
this semantic preflight.
|
||||||
|
|
||||||
|
`malformed_utf8_is_rejected_in_every_selected_source` in
|
||||||
|
`test/package/package_test.ww`, together with focused C and WW lexer coverage,
|
||||||
|
is the focused proof owner for valid encodings and encoded U+FFFD; stray leads
|
||||||
|
and continuations; overlong, surrogate, out-of-range, truncated, repeated, and
|
||||||
|
token-boundary cases; BOM/NUL interaction; root and dependency builds; source
|
||||||
|
versus import-resolution precedence; all test roles; wrong-target exclusion;
|
||||||
|
cold and warm rejection; exact restoration/reuse; parallel isolation;
|
||||||
|
diagnostic and valid-artifact stage parity; and absence of residue. Concrete
|
||||||
|
post-change measurements and hashes are recorded only after focused and full
|
||||||
|
validation. The completed BOM, raw-NUL, and exact output-option slices are not
|
||||||
|
reopened, and canonical dotted package/import identity remains unchanged.
|
||||||
|
|
||||||
After that eligibility boundary and the coordinator's required package-clause
|
After that eligibility boundary and the coordinator's required package-clause
|
||||||
classification and production `@test` validation parses, the delegated loader
|
classification and production `@test` validation parses, the delegated loader
|
||||||
performs selected-basename Go 1.26.5 simple-fold preflight before its graph
|
performs selected-basename Go 1.26.5 simple-fold preflight before its graph
|
||||||
|
|||||||
@@ -699,23 +699,34 @@ fn pkgident(c: u8) bool = {
|
|||||||
return c >= '0' && c <= '9';
|
return c >= '0' && c <= '9';
|
||||||
};
|
};
|
||||||
|
|
||||||
fn pkgnulerrors(path: str, src: str) bool = {
|
// Validate the selected physical source before package-clause classification.
|
||||||
|
// Go's source reader consumes malformed UTF-8 one byte at a time; raw NUL is
|
||||||
|
// the independent ASCII source error. Reporting both in byte order preserves
|
||||||
|
// the decoder's diagnostic precedence and raw-byte columns.
|
||||||
|
fn pkgsourceerrors(path: str, src: str) bool = {
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
let ln: i32 = 1;
|
let ln: i32 = 1;
|
||||||
let cl: i32 = 1;
|
let cl: i32 = 1;
|
||||||
let found: bool = false;
|
let found: bool = false;
|
||||||
for (i < src.len) {
|
for (i < src.len) {
|
||||||
if (src[i] == 0u8) {
|
let c: u8 = src[i];
|
||||||
|
let width: i32 = 1;
|
||||||
|
if (c >= 128u8) { width = pkgutf8width(src, i); };
|
||||||
|
if (c == 0u8) {
|
||||||
pkgfailsource(path, ln, cl, "invalid NUL character");
|
pkgfailsource(path, ln, cl, "invalid NUL character");
|
||||||
found = true;
|
found = true;
|
||||||
|
} else { if (c >= 128u8 && width == 1) {
|
||||||
|
pkgfailsource(path, ln, cl, "invalid UTF-8 encoding");
|
||||||
|
found = true;
|
||||||
};
|
};
|
||||||
if (src[i] == '\n') {
|
};
|
||||||
|
if (c == '\n') {
|
||||||
ln += 1;
|
ln += 1;
|
||||||
cl = 1;
|
cl = 1;
|
||||||
} else {
|
} else {
|
||||||
cl += 1;
|
cl += width;
|
||||||
};
|
};
|
||||||
i += 1;
|
i += width;
|
||||||
};
|
};
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
@@ -2299,7 +2310,7 @@ export fn packagecommand(args: []str) int = {
|
|||||||
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
||||||
return pkgteststatusfail(explicitstatus, compileonly);
|
return pkgteststatusfail(explicitstatus, compileonly);
|
||||||
};
|
};
|
||||||
if (pkgnulerrors(ds.paths[i], body)) {
|
if (pkgsourceerrors(ds.paths[i], body)) {
|
||||||
return pkgteststatusfail(explicitstatus, compileonly);
|
return pkgteststatusfail(explicitstatus, compileonly);
|
||||||
};
|
};
|
||||||
if (!pkgclause(body, &pn)) {
|
if (!pkgclause(body, &pn)) {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type lex = struct {
|
|||||||
col: i32,
|
col: i32,
|
||||||
errs: i32,
|
errs: i32,
|
||||||
nulcount: u64,
|
nulcount: u64,
|
||||||
|
utf8count: u64,
|
||||||
// a `//ww:module-reset` directive was seen in the last skipped run;
|
// a `//ww:module-reset` directive was seen in the last skipped run;
|
||||||
// lexnext emits TK_MODRESET before the next real token (#16 opt-B).
|
// lexnext emits TK_MODRESET before the next real token (#16 opt-B).
|
||||||
modreset: i32,
|
modreset: i32,
|
||||||
@@ -74,6 +75,54 @@ fn bomat(src: *u8, len: u64, off: u64) bool = {
|
|||||||
&& src[i + 2] == 0xbfu8;
|
&& src[i + 2] == 0xbfu8;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Width of the valid UTF-8 scalar sequence beginning at off, or zero. This
|
||||||
|
// mirrors unicode/utf8.DecodeRune's validity partition without changing WW's
|
||||||
|
// established byte-oriented treatment of valid non-ASCII token text.
|
||||||
|
fn utf8seqwidth(src: *u8, len: u64, off: u64) i32 = {
|
||||||
|
if (off >= len) { return 0; };
|
||||||
|
let c: u8 = src[off];
|
||||||
|
if (c < 128u8) { return 1; };
|
||||||
|
if (c >= 194u8 && c <= 223u8 && len - off >= 2u64
|
||||||
|
&& src[off + 1u64] >= 128u8 && src[off + 1u64] <= 191u8) {
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
if (len - off >= 3u64 && src[off + 2u64] >= 128u8
|
||||||
|
&& src[off + 2u64] <= 191u8) {
|
||||||
|
let c1: u8 = src[off + 1u64];
|
||||||
|
if ((c == 224u8 && c1 >= 160u8 && c1 <= 191u8)
|
||||||
|
|| (c >= 225u8 && c <= 236u8 && c1 >= 128u8 && c1 <= 191u8)
|
||||||
|
|| (c == 237u8 && c1 >= 128u8 && c1 <= 159u8)
|
||||||
|
|| (c >= 238u8 && c <= 239u8 && c1 >= 128u8 && c1 <= 191u8)) {
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (len - off >= 4u64 && src[off + 2u64] >= 128u8
|
||||||
|
&& src[off + 2u64] <= 191u8 && src[off + 3u64] >= 128u8
|
||||||
|
&& src[off + 3u64] <= 191u8) {
|
||||||
|
let c1: u8 = src[off + 1u64];
|
||||||
|
if ((c == 240u8 && c1 >= 144u8 && c1 <= 191u8)
|
||||||
|
|| (c >= 241u8 && c <= 243u8 && c1 >= 128u8 && c1 <= 191u8)
|
||||||
|
|| (c == 244u8 && c1 >= 128u8 && c1 <= 143u8)) {
|
||||||
|
return 4;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn utf8bytevalid(src: *u8, len: u64, off: u64) bool = {
|
||||||
|
if (utf8seqwidth(src, len, off) != 0) { return true; };
|
||||||
|
let c: u8 = src[off];
|
||||||
|
if (c < 128u8 || c > 191u8) { return false; };
|
||||||
|
let back: u64 = 1u64;
|
||||||
|
for (back <= 3u64 && back <= off) {
|
||||||
|
if (utf8seqwidth(src, len, off - back) > (back: i32)) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
back += 1u64;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = {
|
export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = {
|
||||||
l.file = file;
|
l.file = file;
|
||||||
l.src = src;
|
l.src = src;
|
||||||
@@ -83,6 +132,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = {
|
|||||||
l.col = 1;
|
l.col = 1;
|
||||||
l.errs = 0;
|
l.errs = 0;
|
||||||
l.nulcount = 0u64;
|
l.nulcount = 0u64;
|
||||||
|
l.utf8count = 0u64;
|
||||||
l.modreset = 0;
|
l.modreset = 0;
|
||||||
l.modpathset = 0;
|
l.modpathset = 0;
|
||||||
l.modresetpathset = 0;
|
l.modresetpathset = 0;
|
||||||
@@ -111,15 +161,34 @@ fn lskipnul(l: *lex) void = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the raw offset of a logical byte lookahead. Raw NUL bytes do not
|
// Go 1.26.5 syntax.source.nextch diagnoses and discards one raw byte for each
|
||||||
// occupy a slot in the token stream: Go's source.nextch diagnoses them and
|
// utf8.DecodeRune RuneError of width one.
|
||||||
// immediately resumes decoding at the following character.
|
fn lskiputf8(l: *lex) void = {
|
||||||
|
for (l.lpos < l.srclen
|
||||||
|
&& srcb(l, l.lpos) >= 128
|
||||||
|
&& !utf8bytevalid(l.src, l.srclen, l.lpos)) {
|
||||||
|
let ep: pos;
|
||||||
|
ep.file = l.file;
|
||||||
|
ep.line = l.line;
|
||||||
|
ep.col = l.col;
|
||||||
|
l.lpos += 1u64;
|
||||||
|
l.col += 1;
|
||||||
|
errat(l, &ep, "invalid UTF-8 encoding");
|
||||||
|
l.utf8count += 1u64;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the raw offset of a logical byte lookahead. Raw NUL and malformed
|
||||||
|
// UTF-8 bytes do not occupy a slot in the token stream: Go's source.nextch
|
||||||
|
// diagnoses them and immediately resumes at the following byte.
|
||||||
fn lrawoff(l: *lex, ahead0: u64) u64 = {
|
fn lrawoff(l: *lex, ahead0: u64) u64 = {
|
||||||
let p: u64 = l.lpos;
|
let p: u64 = l.lpos;
|
||||||
let ahead: u64 = ahead0;
|
let ahead: u64 = ahead0;
|
||||||
for (true) {
|
for (true) {
|
||||||
for (p < l.srclen) {
|
for (p < l.srclen) {
|
||||||
if (srcb(l, p) != 0) { break; };
|
let b: i32 = srcb(l, p);
|
||||||
|
if (b != 0 && (b < 128
|
||||||
|
|| utf8bytevalid(l.src, l.srclen, p))) { break; };
|
||||||
p += 1u64;
|
p += 1u64;
|
||||||
};
|
};
|
||||||
if (ahead == 0u64 || p >= l.srclen) { return p; };
|
if (ahead == 0u64 || p >= l.srclen) { return p; };
|
||||||
@@ -135,6 +204,10 @@ fn lpeek(l: *lex, ahead: u64) i32 = {
|
|||||||
if (l.lpos >= l.srclen) { return -1; };
|
if (l.lpos >= l.srclen) { return -1; };
|
||||||
let c: i32 = srcb(l, l.lpos);
|
let c: i32 = srcb(l, l.lpos);
|
||||||
if (c == 0) { lskipnul(l); continue; };
|
if (c == 0) { lskipnul(l); continue; };
|
||||||
|
if (c >= 128 && !utf8bytevalid(l.src, l.srclen, l.lpos)) {
|
||||||
|
lskiputf8(l);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if (ahead == 0u64) {
|
if (ahead == 0u64) {
|
||||||
if (bomat(l.src, l.srclen, l.lpos)) { return 0xFEFF; };
|
if (bomat(l.src, l.srclen, l.lpos)) { return 0xFEFF; };
|
||||||
return c;
|
return c;
|
||||||
@@ -151,6 +224,10 @@ fn lget(l: *lex) i32 = {
|
|||||||
if (l.lpos >= l.srclen) { return -1; };
|
if (l.lpos >= l.srclen) { return -1; };
|
||||||
let c: i32 = srcb(l, l.lpos);
|
let c: i32 = srcb(l, l.lpos);
|
||||||
if (c == 0) { lskipnul(l); continue; };
|
if (c == 0) { lskipnul(l); continue; };
|
||||||
|
if (c >= 128 && !utf8bytevalid(l.src, l.srclen, l.lpos)) {
|
||||||
|
lskiputf8(l);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if (bomat(l.src, l.srclen, l.lpos)) {
|
if (bomat(l.src, l.srclen, l.lpos)) {
|
||||||
let bp: pos;
|
let bp: pos;
|
||||||
bp.file = l.file;
|
bp.file = l.file;
|
||||||
@@ -172,16 +249,17 @@ fn lget(l: *lex) i32 = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Copy one raw source span into token text while omitting diagnosed NUL
|
// Copy one raw source span into token text while omitting diagnosed NUL and
|
||||||
// bytes. This keeps keyword, numeric, suffix, and directive recovery on the
|
// malformed UTF-8 bytes. This keeps keyword, numeric, suffix, and directive
|
||||||
// same logical character stream as lpeek/lget.
|
// recovery on the same logical character stream as lpeek/lget.
|
||||||
fn lexspan(l: *lex, begin: u64, end: u64) str = {
|
fn lexspan(l: *lex, begin: u64, end: u64) str = {
|
||||||
let buf: []u8 = alloc([], end - begin + 1u64)!;
|
let buf: []u8 = alloc([], end - begin + 1u64)!;
|
||||||
let i: u64 = begin;
|
let i: u64 = begin;
|
||||||
let j: u64 = 0u64;
|
let j: u64 = 0u64;
|
||||||
for (i < end) {
|
for (i < end) {
|
||||||
let b: i32 = srcb(l, i);
|
let b: i32 = srcb(l, i);
|
||||||
if (b != 0) {
|
if (b != 0 && (b < 128
|
||||||
|
|| utf8bytevalid(l.src, l.srclen, i))) {
|
||||||
buf[j] = b: u8;
|
buf[j] = b: u8;
|
||||||
j += 1u64;
|
j += 1u64;
|
||||||
};
|
};
|
||||||
@@ -221,6 +299,7 @@ fn errat(l: *lex, p: *pos, msg: str) void = {
|
|||||||
fn linecomment(l: *lex) void = {
|
fn linecomment(l: *lex) void = {
|
||||||
let begin: u64 = l.lpos;
|
let begin: u64 = l.lpos;
|
||||||
let nulbegin: u64 = l.nulcount;
|
let nulbegin: u64 = l.nulcount;
|
||||||
|
let utf8begin: u64 = l.utf8count;
|
||||||
for (true) {
|
for (true) {
|
||||||
let c: i32 = lpeek(l, 0u64);
|
let c: i32 = lpeek(l, 0u64);
|
||||||
if (c < 0 || c == '\n') { break; };
|
if (c < 0 || c == '\n') { break; };
|
||||||
@@ -228,7 +307,7 @@ fn linecomment(l: *lex) void = {
|
|||||||
};
|
};
|
||||||
let end: u64 = l.lpos;
|
let end: u64 = l.lpos;
|
||||||
let body: str;
|
let body: str;
|
||||||
if (l.nulcount == nulbegin) {
|
if (l.nulcount == nulbegin && l.utf8count == utf8begin) {
|
||||||
body.ptr = l.src + begin;
|
body.ptr = l.src + begin;
|
||||||
body.len = (end - begin): i32;
|
body.len = (end - begin): i32;
|
||||||
} else {
|
} else {
|
||||||
@@ -534,6 +613,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
out.col = start.col;
|
out.col = start.col;
|
||||||
let begin: u64 = l.lpos;
|
let begin: u64 = l.lpos;
|
||||||
let nulbegin: u64 = l.nulcount;
|
let nulbegin: u64 = l.nulcount;
|
||||||
|
let utf8begin: u64 = l.utf8count;
|
||||||
let base: i32 = 10;
|
let base: i32 = 10;
|
||||||
let isfloat: bool = false;
|
let isfloat: bool = false;
|
||||||
|
|
||||||
@@ -583,7 +663,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let rawend: u64 = l.lpos;
|
let rawend: u64 = l.lpos;
|
||||||
if (l.nulcount == nulbegin) {
|
if (l.nulcount == nulbegin && l.utf8count == utf8begin) {
|
||||||
let view: str;
|
let view: str;
|
||||||
view.ptr = l.src + begin;
|
view.ptr = l.src + begin;
|
||||||
view.len = (rawend - begin): i32;
|
view.len = (rawend - begin): i32;
|
||||||
@@ -671,6 +751,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
||||||
let begin: u64 = l.lpos;
|
let begin: u64 = l.lpos;
|
||||||
let nulbegin: u64 = l.nulcount;
|
let nulbegin: u64 = l.nulcount;
|
||||||
|
let utf8begin: u64 = l.utf8count;
|
||||||
for (true) {
|
for (true) {
|
||||||
let c: i32 = lpeek(l, 0u64);
|
let c: i32 = lpeek(l, 0u64);
|
||||||
if (c < 0) { break; };
|
if (c < 0) { break; };
|
||||||
@@ -678,7 +759,7 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
lget(l);
|
lget(l);
|
||||||
};
|
};
|
||||||
let text: str;
|
let text: str;
|
||||||
if (l.nulcount == nulbegin) {
|
if (l.nulcount == nulbegin && l.utf8count == utf8begin) {
|
||||||
let view: str;
|
let view: str;
|
||||||
view.ptr = l.src + begin;
|
view.ptr = l.src + begin;
|
||||||
view.len = (l.lpos - begin): i32;
|
view.len = (l.lpos - begin): i32;
|
||||||
|
|||||||
@@ -437,3 +437,138 @@ fn doublebomerror() bool = {
|
|||||||
assert(bomerror("\"", "\""));
|
assert(bomerror("\"", "\""));
|
||||||
assert(bomerror("'", "'"));
|
assert(bomerror("'", "'"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn utf8source(dst: *u8, before: str, middle: *u8, middlen: i32,
|
||||||
|
after: str) u64 = {
|
||||||
|
let n: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < before.len) { dst[n] = before[i]; n += 1; i += 1; };
|
||||||
|
i = 0;
|
||||||
|
for (i < middlen) { dst[n] = middle[i]; n += 1; i += 1; };
|
||||||
|
i = 0;
|
||||||
|
for (i < after.len) { dst[n] = after[i]; n += 1; i += 1; };
|
||||||
|
return n: u64;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn checkmalformedutf8(raw: *u8, rawlen: i32, errs: i32) void = {
|
||||||
|
let src: [32]u8;
|
||||||
|
let n: u64 = utf8source(src.ptr, "f", raw, rawlen, "n bar");
|
||||||
|
let l: syntax.lex;
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
let t: syntax.tok;
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN && t.line == 1 && t.col == 1);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_IDENT && t.text == "bar");
|
||||||
|
assert(t.line == 1 && t.col == 4 + rawlen);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_EOF && l.errs == errs);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Go 1.26.5 source.nextch reports and filters one byte for every
|
||||||
|
// DecodeRune width-one malformed result.
|
||||||
|
@test fn malformed_utf8_width_one_recovery() void = {
|
||||||
|
let lead: [1]u8 = [0xffu8];
|
||||||
|
let continuation: [1]u8 = [0x80u8];
|
||||||
|
let overlong: [2]u8 = [0xc0u8, 0x80u8];
|
||||||
|
let surrogate: [3]u8 = [0xedu8, 0xa0u8, 0x80u8];
|
||||||
|
let outofrange: [4]u8 = [0xf4u8, 0x90u8, 0x80u8, 0x80u8];
|
||||||
|
let truncated: [2]u8 = [0xe2u8, 0x82u8];
|
||||||
|
checkmalformedutf8(lead.ptr, 1, 1);
|
||||||
|
checkmalformedutf8(continuation.ptr, 1, 1);
|
||||||
|
checkmalformedutf8(overlong.ptr, 2, 2);
|
||||||
|
checkmalformedutf8(surrogate.ptr, 3, 3);
|
||||||
|
checkmalformedutf8(outofrange.ptr, 4, 4);
|
||||||
|
checkmalformedutf8(truncated.ptr, 2, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn valid_utf8_is_preserved() void = {
|
||||||
|
let raw: [12]u8 = [
|
||||||
|
0xc3u8, 0xa9u8,
|
||||||
|
0xeau8, 0xb0u8, 0x80u8,
|
||||||
|
0xefu8, 0xbfu8, 0xbdu8,
|
||||||
|
0xf0u8, 0x9fu8, 0x98u8, 0x80u8,
|
||||||
|
];
|
||||||
|
let src: [32]u8;
|
||||||
|
let n: u64 = utf8source(src.ptr, "\"", raw.ptr, 12, "\" fn");
|
||||||
|
let l: syntax.lex;
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
let t: syntax.tok;
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_STR && t.text.len == 12);
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < 12) { assert(t.text[i] == raw[i]); i += 1; };
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN && t.line == 1 && t.col == 16);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_EOF && l.errs == 0);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "// ", raw.ptr, 12, "\nfn");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN && t.line == 2 && t.col == 1);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_EOF && l.errs == 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn malformed_utf8_token_recovery() void = {
|
||||||
|
let bad: [1]u8 = [0xffu8];
|
||||||
|
let src: [64]u8;
|
||||||
|
let l: syntax.lex;
|
||||||
|
let t: syntax.tok;
|
||||||
|
let n: u64 = utf8source(src.ptr, "1", bad.ptr, 1, "_0");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_INT && t.uval == 10u64 && l.errs == 1);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "=", bad.ptr, 1, "=");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_EQ && l.errs == 1);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "/", bad.ptr, 1, "/ comment\nfn");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN && l.errs == 1);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "/* end *", bad.ptr, 1, "/ fn");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN && l.errs == 1);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "\"a", bad.ptr, 1, "b\"");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_STR && t.text == "ab" && l.errs == 1);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "\"\\", bad.ptr, 1, "n\"");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_STR && t.text.len == 1);
|
||||||
|
assert(t.text[0] == '\n' && l.errs == 1);
|
||||||
|
|
||||||
|
n = utf8source(src.ptr, "\"\\x0", bad.ptr, 1, "0\"");
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, n);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_STR && t.text.len == 1);
|
||||||
|
assert(t.text[0] == 0u8 && l.errs == 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn malformed_utf8_bom_nul_columns() void = {
|
||||||
|
let src: [12]u8 = [
|
||||||
|
0xefu8, 0xbbu8, 0xbfu8,
|
||||||
|
'f': u8, 0xffu8, 0u8, 'n': u8,
|
||||||
|
' ': u8, 'b': u8, 'a': u8, 'r': u8, 0u8,
|
||||||
|
];
|
||||||
|
let l: syntax.lex;
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, 11u64);
|
||||||
|
let t: syntax.tok;
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN && t.line == 1 && t.col == 4);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_IDENT && t.text == "bar");
|
||||||
|
assert(t.line == 1 && t.col == 9);
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_EOF);
|
||||||
|
assert(l.errs == 2 && l.nulcount == 1u64);
|
||||||
|
};
|
||||||
|
|||||||
@@ -161,6 +161,38 @@ fn rewritenulfile(path: str, before: str, after: str) void = {
|
|||||||
putnulfile(path, before, after, true);
|
putnulfile(path, before, after, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn putinvalidutf8file(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 invalid UTF-8 failed");
|
||||||
|
};
|
||||||
|
let invalid: [1]u8;
|
||||||
|
invalid[0] = 0xffu8;
|
||||||
|
match (os.writeall(fd, invalid.ptr, 1u64)) {
|
||||||
|
case let n: i64 => assert(n == 1i64);
|
||||||
|
case let e: os.oserror => abort("write invalid UTF-8 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 invalid UTF-8 failed");
|
||||||
|
};
|
||||||
|
assert(os.close(fd) == 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writeinvalidutf8file(path: str, before: str, after: str) void = {
|
||||||
|
putinvalidutf8file(path, before, after, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn rewriteinvalidutf8file(path: str, before: str, after: str) void = {
|
||||||
|
putinvalidutf8file(path, before, after, true);
|
||||||
|
};
|
||||||
|
|
||||||
fn writedoublenulfile(path: str, before: str, after: str) void = {
|
fn writedoublenulfile(path: str, before: str, after: str) void = {
|
||||||
let fd: i32 = os.open(path,
|
let fd: i32 = os.open(path,
|
||||||
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
||||||
@@ -17925,3 +17957,382 @@ fn runtimepath(relative: str) str = {
|
|||||||
&& !directoryhasnew(root));
|
&& !directoryhasnew(root));
|
||||||
clean(root);
|
clean(root);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Go 1.26.5 removes each malformed UTF-8 byte from the logical source after
|
||||||
|
// reporting it. WW applies that decoder rule only after physical-file
|
||||||
|
// eligibility and before package clauses or imports can affect the graph.
|
||||||
|
@test fn malformed_utf8_is_rejected_in_every_selected_source() void = {
|
||||||
|
let root: str = fresh();
|
||||||
|
let source: str = strings.concat(root, "/source");
|
||||||
|
mkdirall(source);
|
||||||
|
|
||||||
|
// Filtering the invalid byte reconstructs the keyword, so the decoder's
|
||||||
|
// positioned error is the only failure and neither frontend emits assembly.
|
||||||
|
let invaliddirect: str = strings.concat(root, "/invalid-direct.ww");
|
||||||
|
let validdirect: str = strings.concat(root, "/valid-direct.ww");
|
||||||
|
writeinvalidutf8file(invaliddirect, "pack",
|
||||||
|
"age main;\nfn main() i32 = { return 0; };\n");
|
||||||
|
writefile(validdirect, strings.concat(
|
||||||
|
"package main;\n",
|
||||||
|
"// 한국어, café, and <20> are valid UTF-8 source bytes.\n",
|
||||||
|
"fn main() i32 = { return 0; };\n"));
|
||||||
|
let compilers: []str = ["w6c", "w6c_ww"];
|
||||||
|
let stages: []str = ["ww", "ww_ww"];
|
||||||
|
let tags: []str = ["c", "ww"];
|
||||||
|
let invaliddiags: []str = ["", ""];
|
||||||
|
let validasms: []str = [strings.concat(root, "/valid-c.s"),
|
||||||
|
strings.concat(root, "/valid-ww.s")];
|
||||||
|
let si: i32 = 0;
|
||||||
|
for (si < compilers.len) {
|
||||||
|
let invalidasm: str = strings.concat(root, "/invalid-", tags[si], ".s");
|
||||||
|
let invalidav: []str = [driver(compilers[si]), "-c",
|
||||||
|
"--command-package", "-o", invalidasm, invaliddirect];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, strings.concat("utf8-direct-invalid-", tags[si]),
|
||||||
|
invalidav, (30i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0
|
||||||
|
&& has(out.stderr, ":1:5: error: invalid UTF-8 encoding\n")
|
||||||
|
&& occurrences(out.stderr, "invalid UTF-8 encoding") == 1
|
||||||
|
&& !has(out.stderr, "unexpected character")
|
||||||
|
&& !has(out.stderr, "invalid or missing package clause")
|
||||||
|
&& !os.exists(invalidasm));
|
||||||
|
invaliddiags[si] = strings.dup(out.stderr);
|
||||||
|
|
||||||
|
let validav: []str = [driver(compilers[si]), "-c",
|
||||||
|
"--command-package", "-o", validasms[si], validdirect];
|
||||||
|
runcommand(root, strings.concat("utf8-direct-valid-", tags[si]),
|
||||||
|
validav, (30i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0);
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
assert(same(invaliddiags[0], invaliddiags[1]));
|
||||||
|
assert(same(readfile(validasms[0]), readfile(validasms[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");
|
||||||
|
let ordered: str = strings.concat(source, "/ordered");
|
||||||
|
mkdirall(dep); mkdirall(app); mkdirall(bad); mkdirall(good);
|
||||||
|
mkdirall(ordered);
|
||||||
|
writeinvalidutf8file(strings.concat(dep, "/dep.ww"),
|
||||||
|
"package dep;\n// bad ",
|
||||||
|
" dependency\nexport fn value() i32 = { return 0; };\n");
|
||||||
|
writefile(strings.concat(app, "/main.ww"), strings.concat(
|
||||||
|
"package main;\nimport dep;\n",
|
||||||
|
"fn main() i32 = { return dep.value(); };\n"));
|
||||||
|
writeinvalidutf8file(strings.concat(bad, "/main.ww"),
|
||||||
|
"package main;\n// bad ", 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");
|
||||||
|
writeinvalidutf8file(strings.concat(good, "/ignored_windows.ww"),
|
||||||
|
"not package ", " and not valid WW\n");
|
||||||
|
writeinvalidutf8file(strings.concat(good, "/ignored_test.ww"),
|
||||||
|
"not package ", " and not valid WW\n");
|
||||||
|
writeinvalidutf8file(strings.concat(ordered, "/z.ww"),
|
||||||
|
"package main;\n// z ", "\nfn z() void = {};\n");
|
||||||
|
writefile(strings.concat(ordered, "/b.ww"),
|
||||||
|
"package main;\nfn main() i32 = { return 0; };\n");
|
||||||
|
writeinvalidutf8file(strings.concat(ordered, "/a.ww"),
|
||||||
|
"package main;\n// a ", "\nfn a() void = {};\n");
|
||||||
|
|
||||||
|
// Root and dependency decoding precede graph resolution and all producers.
|
||||||
|
// Excluded files are not semantic inputs, and selection remains byte-sorted.
|
||||||
|
let goodbins: []str = [strings.concat(root, "/good-c"),
|
||||||
|
strings.concat(root, "/good-ww")];
|
||||||
|
let depdiags: []str = ["", ""];
|
||||||
|
let rootdiags: []str = ["", ""];
|
||||||
|
let orderdiags: []str = ["", ""];
|
||||||
|
si = 0;
|
||||||
|
for (si < stages.len) {
|
||||||
|
let out: commandout;
|
||||||
|
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"];
|
||||||
|
runcommand(root, strings.concat("utf8-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:8: error: invalid UTF-8 encoding\n")
|
||||||
|
&& occurrences(out.stderr, "invalid UTF-8 encoding") == 1
|
||||||
|
&& !has(out.stderr, "w6c failed")
|
||||||
|
&& !os.exists(depout) && directoryisempty(depwork));
|
||||||
|
depdiags[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("utf8-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:8: error: invalid UTF-8 encoding\n")
|
||||||
|
&& occurrences(out.stderr, "invalid UTF-8 encoding") == 1
|
||||||
|
&& !has(out.stderr, "cannot find import")
|
||||||
|
&& !os.exists(badout) && directoryisempty(badwork));
|
||||||
|
rootdiags[si] = strings.dup(out.stderr);
|
||||||
|
|
||||||
|
let orderwork: str = strings.concat(root, "/order-work-", tags[si]);
|
||||||
|
let orderout: str = strings.concat(root, "/order-output-", tags[si]);
|
||||||
|
mkdirall(orderwork);
|
||||||
|
let orderav: []str = [driver(stages[si]), "build", "-w", orderwork,
|
||||||
|
"-I", source, "-o", orderout, "ordered"];
|
||||||
|
runcommand(root, strings.concat("utf8-order-", tags[si]), orderav,
|
||||||
|
(60i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0
|
||||||
|
&& has(out.stderr,
|
||||||
|
"/ordered/a.ww:2:6: error: invalid UTF-8 encoding\n")
|
||||||
|
&& !has(out.stderr, "/ordered/z.ww:")
|
||||||
|
&& !os.exists(orderout) && directoryisempty(orderwork));
|
||||||
|
orderdiags[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("utf8-excluded-", 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("utf8-good-run-", tags[si]), runav,
|
||||||
|
time.second, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0);
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
assert(same(depdiags[0], depdiags[1])
|
||||||
|
&& same(rootdiags[0], rootdiags[1])
|
||||||
|
&& same(orderdiags[0], orderdiags[1])
|
||||||
|
&& same(readfile(goodbins[0]), readfile(goodbins[1])));
|
||||||
|
|
||||||
|
// Every selected test-source role rejects before generated-main creation,
|
||||||
|
// execution, accounting, and retained-binary publication.
|
||||||
|
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");
|
||||||
|
mkdirall(prodcase); mkdirall(samecase); mkdirall(externalcase);
|
||||||
|
mkdirall(onlycase);
|
||||||
|
writeinvalidutf8file(strings.concat(prodcase, "/prod.ww"),
|
||||||
|
"package prodcase;\n// bad ",
|
||||||
|
" production\nfn value() i32 = { return 1; };\n");
|
||||||
|
writefile(strings.concat(prodcase, "/prod_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");
|
||||||
|
writeinvalidutf8file(strings.concat(samecase, "/same_test.ww"),
|
||||||
|
"package samecase;\n// bad ",
|
||||||
|
" 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");
|
||||||
|
writeinvalidutf8file(strings.concat(externalcase, "/external_test.ww"),
|
||||||
|
"package externalcase_test;\n// bad ", strings.concat(
|
||||||
|
" external\nimport externalcase;\n",
|
||||||
|
"@test fn must_not_run() void = { abort(); };\n"));
|
||||||
|
writeinvalidutf8file(strings.concat(onlycase, "/only_test.ww"), "",
|
||||||
|
"package onlycase;\n@test fn must_not_run() void = { abort(); };\n");
|
||||||
|
let testids: []str = ["prodcase", "samecase", "externalcase", "onlycase"];
|
||||||
|
let testpositions: []str = [
|
||||||
|
"/prodcase/prod.ww:2:8: error: invalid UTF-8 encoding\n",
|
||||||
|
"/samecase/same_test.ww:2:8: error: invalid UTF-8 encoding\n",
|
||||||
|
"/externalcase/external_test.ww:2:8: error: invalid UTF-8 encoding\n",
|
||||||
|
"/onlycase/only_test.ww:1:1: error: invalid UTF-8 encoding\n"];
|
||||||
|
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("utf8-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])
|
||||||
|
&& occurrences(out.stderr, "invalid UTF-8 encoding") == 1
|
||||||
|
&& !has(out.stdout, "must_not_run")
|
||||||
|
&& !has(out.stdout, "discovered")
|
||||||
|
&& !has(out.stdout, "passed") && !has(out.stdout, "failed")
|
||||||
|
&& !has(out.stdout, "ok ")
|
||||||
|
&& !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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invalid warm input reaches no tool and preserves every committed action,
|
||||||
|
// tool record, and public byte. Exact source restoration is a cache hit.
|
||||||
|
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, "/utf8-compiler-wrapper.sh");
|
||||||
|
writeexecutable(wrapper, strings.concat(
|
||||||
|
"#!/bin/sh\n",
|
||||||
|
"printf 'compile\\n' >> \"$WW_UTF8_TRACE\"\n",
|
||||||
|
"exec \"$WW_UTF8_REAL\" \"$@\"\n"));
|
||||||
|
let commandsuffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
|
||||||
|
".init.unit.ww", ".init.s", ".init.o"];
|
||||||
|
let toolpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c",
|
||||||
|
"/.wwtool.w6a", "/.wwtool.stamp"];
|
||||||
|
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_UTF8_TRACE=", trace));
|
||||||
|
append(env, strings.concat("WW_UTF8_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("utf8-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 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, "");
|
||||||
|
rewriteinvalidutf8file(warmpath, "package main;\n// bad ",
|
||||||
|
" warm\nfn main() i32 = { return 0; };\n");
|
||||||
|
runcommandenv(root, strings.concat("utf8-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:8: error: invalid UTF-8 encoding\n")
|
||||||
|
&& 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("utf8-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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation is request-local: an overlapping valid Cstage request is not
|
||||||
|
// cancelled or contaminated by an invalid WWstage request.
|
||||||
|
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 UTF-8 encoding"));
|
||||||
|
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, "utf8-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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -240,6 +240,113 @@ static const struct {
|
|||||||
{ "f\0\0n", sizeof "f\0\0n" - 1, "fn", 2 },
|
{ "f\0\0n", sizeof "f\0\0n" - 1, "fn", 2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Go 1.26.5 source.nextch reports each DecodeRune width-one result and
|
||||||
|
* removes that byte before scanner recovery. */
|
||||||
|
static const struct {
|
||||||
|
const char *src;
|
||||||
|
size_t len;
|
||||||
|
const char *expect;
|
||||||
|
int errs;
|
||||||
|
} utf8rows[] = {
|
||||||
|
{ "f" "\xff" "n bar", sizeof "f" "\xff" "n bar" - 1,
|
||||||
|
"fn IDENT(bar)", 1 },
|
||||||
|
{ "f" "\x80" "n bar", sizeof "f" "\x80" "n bar" - 1,
|
||||||
|
"fn IDENT(bar)", 1 },
|
||||||
|
{ "f" "\xc0\x80" "n bar", sizeof "f" "\xc0\x80" "n bar" - 1,
|
||||||
|
"fn IDENT(bar)", 2 },
|
||||||
|
{ "f" "\xed\xa0\x80" "n bar",
|
||||||
|
sizeof "f" "\xed\xa0\x80" "n bar" - 1,
|
||||||
|
"fn IDENT(bar)", 3 },
|
||||||
|
{ "f" "\xf4\x90\x80\x80" "n bar",
|
||||||
|
sizeof "f" "\xf4\x90\x80\x80" "n bar" - 1,
|
||||||
|
"fn IDENT(bar)", 4 },
|
||||||
|
{ "f" "\xe2\x82" "n bar", sizeof "f" "\xe2\x82" "n bar" - 1,
|
||||||
|
"fn IDENT(bar)", 2 },
|
||||||
|
{ "1" "\xff" "_0", sizeof "1" "\xff" "_0" - 1,
|
||||||
|
"INT(10)", 1 },
|
||||||
|
{ "=" "\xff" "=", sizeof "=" "\xff" "=" - 1,
|
||||||
|
"==", 1 },
|
||||||
|
{ "/" "\xff" "/ comment\nfn",
|
||||||
|
sizeof "/" "\xff" "/ comment\nfn" - 1, "fn", 1 },
|
||||||
|
{ "/* end *" "\xff" "/ fn",
|
||||||
|
sizeof "/* end *" "\xff" "/ fn" - 1, "fn", 1 },
|
||||||
|
{ "\"a" "\xff" "b\"", sizeof "\"a" "\xff" "b\"" - 1,
|
||||||
|
"STR(ab)", 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
runutf8valid(void)
|
||||||
|
{
|
||||||
|
static const char strsrc[] =
|
||||||
|
"\"" "\xc3\xa9" "\xea\xb0\x80" "\xef\xbf\xbd"
|
||||||
|
"\xf0\x9f\x98\x80" "\" fn";
|
||||||
|
static const char commentsrc[] =
|
||||||
|
"// " "\xc3\xa9" "\xea\xb0\x80" "\xef\xbf\xbd"
|
||||||
|
"\xf0\x9f\x98\x80" "\nfn";
|
||||||
|
static const unsigned char want[] = {
|
||||||
|
0xc3, 0xa9, 0xea, 0xb0, 0x80, 0xef, 0xbf, 0xbd,
|
||||||
|
0xf0, 0x9f, 0x98, 0x80,
|
||||||
|
};
|
||||||
|
Arena *a = newarena();
|
||||||
|
Lex l;
|
||||||
|
lexinit(&l, a, "<test>", strsrc, sizeof strsrc - 1);
|
||||||
|
Tok s = lexnext(&l);
|
||||||
|
Tok f = lexnext(&l);
|
||||||
|
int ok = s.kind == TK_STR && s.tlen == sizeof want
|
||||||
|
&& memcmp(s.text, want, sizeof want) == 0
|
||||||
|
&& f.kind == TK_FN && f.pos.col == 16 && l.errs == 0
|
||||||
|
&& lexnext(&l).kind == TK_EOF;
|
||||||
|
lexinit(&l, a, "<test>", commentsrc, sizeof commentsrc - 1);
|
||||||
|
f = lexnext(&l);
|
||||||
|
ok = ok && f.kind == TK_FN && f.pos.line == 2 && f.pos.col == 1
|
||||||
|
&& l.errs == 0 && lexnext(&l).kind == TK_EOF;
|
||||||
|
if (!ok)
|
||||||
|
fprintf(stderr, "valid UTF-8 preservation failed\n");
|
||||||
|
freearena(a);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runutf8cols(void)
|
||||||
|
{
|
||||||
|
static const char src[] =
|
||||||
|
"\xef\xbb\xbf" "f" "\xff" "\0" "n bar";
|
||||||
|
Arena *a = newarena();
|
||||||
|
Lex l;
|
||||||
|
lexinit(&l, a, "<test>", src, sizeof src - 1);
|
||||||
|
Tok f = lexnext(&l);
|
||||||
|
Tok b = lexnext(&l);
|
||||||
|
int ok = f.kind == TK_FN && f.pos.line == 1 && f.pos.col == 4
|
||||||
|
&& b.kind == TK_IDENT && b.pos.line == 1 && b.pos.col == 9
|
||||||
|
&& strcmp(b.text, "bar") == 0 && l.errs == 2
|
||||||
|
&& l.nulcount == 1 && lexnext(&l).kind == TK_EOF;
|
||||||
|
if (!ok)
|
||||||
|
fprintf(stderr, "UTF-8 raw-byte columns or BOM/NUL recovery failed\n");
|
||||||
|
freearena(a);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runutf8escape(void)
|
||||||
|
{
|
||||||
|
static const char line[] = "\"\\" "\xff" "n\"";
|
||||||
|
static const char hex[] = "\"\\x0" "\xff" "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, "UTF-8 escape recovery failed\n");
|
||||||
|
freearena(a);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
runescapenul(void)
|
runescapenul(void)
|
||||||
{
|
{
|
||||||
@@ -333,10 +440,23 @@ main(void)
|
|||||||
fail++;
|
fail++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (size_t i = 0; i < sizeof utf8rows / sizeof utf8rows[0]; i++) {
|
||||||
|
if (!runrown(utf8rows[i].src, utf8rows[i].len,
|
||||||
|
utf8rows[i].expect, utf8rows[i].errs)) {
|
||||||
|
fprintf(stderr, "UTF-8 row %zu failed\n", i);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!runescapenul())
|
if (!runescapenul())
|
||||||
fail++;
|
fail++;
|
||||||
if (!runsuffixnul())
|
if (!runsuffixnul())
|
||||||
fail++;
|
fail++;
|
||||||
|
if (!runutf8valid())
|
||||||
|
fail++;
|
||||||
|
if (!runutf8cols())
|
||||||
|
fail++;
|
||||||
|
if (!runutf8escape())
|
||||||
|
fail++;
|
||||||
if (!runlongdirective())
|
if (!runlongdirective())
|
||||||
fail++;
|
fail++;
|
||||||
if (fail) {
|
if (fail) {
|
||||||
|
|||||||
Reference in New Issue
Block a user