ww source: reject malformed UTF-8
This commit is contained in:
@@ -52,6 +52,7 @@ export type lex = struct {
|
||||
col: i32,
|
||||
errs: i32,
|
||||
nulcount: u64,
|
||||
utf8count: u64,
|
||||
// a `//ww:module-reset` directive was seen in the last skipped run;
|
||||
// lexnext emits TK_MODRESET before the next real token (#16 opt-B).
|
||||
modreset: i32,
|
||||
@@ -74,6 +75,54 @@ fn bomat(src: *u8, len: u64, off: u64) bool = {
|
||||
&& 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 = {
|
||||
l.file = file;
|
||||
l.src = src;
|
||||
@@ -83,6 +132,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = {
|
||||
l.col = 1;
|
||||
l.errs = 0;
|
||||
l.nulcount = 0u64;
|
||||
l.utf8count = 0u64;
|
||||
l.modreset = 0;
|
||||
l.modpathset = 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
|
||||
// occupy a slot in the token stream: Go's source.nextch diagnoses them and
|
||||
// immediately resumes decoding at the following character.
|
||||
// Go 1.26.5 syntax.source.nextch diagnoses and discards one raw byte for each
|
||||
// utf8.DecodeRune RuneError of width one.
|
||||
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 = {
|
||||
let p: u64 = l.lpos;
|
||||
let ahead: u64 = ahead0;
|
||||
for (true) {
|
||||
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;
|
||||
};
|
||||
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; };
|
||||
let c: i32 = srcb(l, l.lpos);
|
||||
if (c == 0) { lskipnul(l); continue; };
|
||||
if (c >= 128 && !utf8bytevalid(l.src, l.srclen, l.lpos)) {
|
||||
lskiputf8(l);
|
||||
continue;
|
||||
};
|
||||
if (ahead == 0u64) {
|
||||
if (bomat(l.src, l.srclen, l.lpos)) { return 0xFEFF; };
|
||||
return c;
|
||||
@@ -151,6 +224,10 @@ fn lget(l: *lex) i32 = {
|
||||
if (l.lpos >= l.srclen) { return -1; };
|
||||
let c: i32 = srcb(l, l.lpos);
|
||||
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)) {
|
||||
let bp: pos;
|
||||
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
|
||||
// bytes. This keeps keyword, numeric, suffix, and directive recovery on the
|
||||
// same logical character stream as lpeek/lget.
|
||||
// Copy one raw source span into token text while omitting diagnosed NUL and
|
||||
// malformed UTF-8 bytes. This keeps keyword, numeric, suffix, and directive
|
||||
// recovery on the same logical character stream as lpeek/lget.
|
||||
fn lexspan(l: *lex, begin: u64, end: u64) str = {
|
||||
let buf: []u8 = alloc([], end - begin + 1u64)!;
|
||||
let i: u64 = begin;
|
||||
let j: u64 = 0u64;
|
||||
for (i < end) {
|
||||
let b: i32 = srcb(l, i);
|
||||
if (b != 0) {
|
||||
if (b != 0 && (b < 128
|
||||
|| utf8bytevalid(l.src, l.srclen, i))) {
|
||||
buf[j] = b: u8;
|
||||
j += 1u64;
|
||||
};
|
||||
@@ -221,6 +299,7 @@ fn errat(l: *lex, p: *pos, msg: str) void = {
|
||||
fn linecomment(l: *lex) void = {
|
||||
let begin: u64 = l.lpos;
|
||||
let nulbegin: u64 = l.nulcount;
|
||||
let utf8begin: u64 = l.utf8count;
|
||||
for (true) {
|
||||
let c: i32 = lpeek(l, 0u64);
|
||||
if (c < 0 || c == '\n') { break; };
|
||||
@@ -228,7 +307,7 @@ fn linecomment(l: *lex) void = {
|
||||
};
|
||||
let end: u64 = l.lpos;
|
||||
let body: str;
|
||||
if (l.nulcount == nulbegin) {
|
||||
if (l.nulcount == nulbegin && l.utf8count == utf8begin) {
|
||||
body.ptr = l.src + begin;
|
||||
body.len = (end - begin): i32;
|
||||
} else {
|
||||
@@ -534,6 +613,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
||||
out.col = start.col;
|
||||
let begin: u64 = l.lpos;
|
||||
let nulbegin: u64 = l.nulcount;
|
||||
let utf8begin: u64 = l.utf8count;
|
||||
let base: i32 = 10;
|
||||
let isfloat: bool = false;
|
||||
|
||||
@@ -583,7 +663,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
||||
};
|
||||
|
||||
let rawend: u64 = l.lpos;
|
||||
if (l.nulcount == nulbegin) {
|
||||
if (l.nulcount == nulbegin && l.utf8count == utf8begin) {
|
||||
let view: str;
|
||||
view.ptr = l.src + begin;
|
||||
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 = {
|
||||
let begin: u64 = l.lpos;
|
||||
let nulbegin: u64 = l.nulcount;
|
||||
let utf8begin: u64 = l.utf8count;
|
||||
for (true) {
|
||||
let c: i32 = lpeek(l, 0u64);
|
||||
if (c < 0) { break; };
|
||||
@@ -678,7 +759,7 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
||||
lget(l);
|
||||
};
|
||||
let text: str;
|
||||
if (l.nulcount == nulbegin) {
|
||||
if (l.nulcount == nulbegin && l.utf8count == utf8begin) {
|
||||
let view: str;
|
||||
view.ptr = l.src + begin;
|
||||
view.len = (l.lpos - begin): i32;
|
||||
|
||||
@@ -437,3 +437,138 @@ fn doublebomerror() bool = {
|
||||
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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user