mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-20 05:40:17 +08:00
* feat: support static non-UTF-8 TextDecoder labels - Recognize static WHATWG labels and aliases during builtin lowering. - Add feature-gated portable decoders generated against the pinned Node runtime. - Cover legacy encodings, malformed input, and unsupported dynamic labels. * fix: match Node legacy decoder recovery * fix TextDecoder edge-case parity * fix(runtime): match Node malformed text decoding * fix TextDecoder recovery edge cases
207 lines
12 KiB
TypeScript
207 lines
12 KiB
TypeScript
// The WHATWG encoder pair, composed and same-scope const store-then-call:
|
|
// utf-8 round trips, BOM stripping, and maximal-subpart replacement,
|
|
// byte-compared against Node.
|
|
const enc = new TextEncoder().encode("héllo😀");
|
|
console.log(enc.length, enc[0], enc[1], enc[2], enc[6]);
|
|
console.log(new TextDecoder().decode(enc));
|
|
|
|
// A leading BOM strips (Buffer.toString("utf8") keeps it — the one
|
|
// behavioral difference between the two decodes).
|
|
console.log(new TextDecoder().decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41, 0x42])));
|
|
console.log(new TextDecoder().decode(new Uint8Array([0xef, 0xbb, 0xbf])).length);
|
|
console.log(Buffer.from([0xef, 0xbb, 0xbf, 0x41]).toString("utf8").length);
|
|
|
|
// Invalid sequences replace per maximal subpart, exactly like Node.
|
|
console.log(JSON.stringify(new TextDecoder().decode(new Uint8Array([0x41, 0xff, 0x42]))));
|
|
console.log(JSON.stringify(new TextDecoder().decode(new Uint8Array([0xf0, 0x9f, 0x98]))));
|
|
|
|
// Zero-argument decode is "" per spec; the explicit utf-8 label works.
|
|
console.log(new TextDecoder().decode().length);
|
|
console.log(new TextDecoder("utf-8").decode(Buffer.from("hi", "utf8")));
|
|
|
|
// Round trip through both, Buffer input included.
|
|
const rt = new TextDecoder().decode(new TextEncoder().encode("round ✓ trip"));
|
|
console.log(rt === "round ✓ trip", rt);
|
|
console.log(new TextEncoder().encode("").length);
|
|
|
|
// The ordinary stored-instance idiom is compile-time alias plumbing: the
|
|
// default constructors have no effects, and supported calls resolve back
|
|
// through a stable const without materializing the codec object.
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
const stored = encoder.encode("stored héllo 😀");
|
|
console.log(stored.length, decoder.decode(stored));
|
|
console.log(encoder.encode("again").length, decoder.decode(Buffer.from("twice", "utf8")));
|
|
|
|
// An explicit literal utf-8 label is equally effect-free. Function-local
|
|
// bindings take the same rewrite within their own execution scope.
|
|
function localRoundTrip(s: string): string {
|
|
const labelledDecoder = new TextDecoder("utf-8");
|
|
const localEncoder = new TextEncoder();
|
|
return labelledDecoder.decode(localEncoder.encode(s));
|
|
}
|
|
console.log(localRoundTrip("local ✓"));
|
|
|
|
// Every WHATWG legacy family lowers when the label is static. The generated
|
|
// single-byte tables are each touched here; JSON escaping keeps C0/C1 and
|
|
// undefined-byte replacements visible in the differential output.
|
|
const singleByteSamples = [
|
|
new TextDecoder("ibm866").decode(new Uint8Array([0x80])),
|
|
new TextDecoder("iso-8859-2").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-3").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-4").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-5").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-6").decode(new Uint8Array([0xac])),
|
|
new TextDecoder("iso-8859-7").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-8").decode(new Uint8Array([0xe0])),
|
|
new TextDecoder("iso-8859-8-i").decode(new Uint8Array([0xe1])),
|
|
new TextDecoder("iso-8859-10").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-13").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-14").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("iso-8859-15").decode(new Uint8Array([0xa4])),
|
|
new TextDecoder("iso-8859-16").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("koi8-r").decode(new Uint8Array([0xe1])),
|
|
new TextDecoder("koi8-u").decode(new Uint8Array([0xa4])),
|
|
new TextDecoder("macintosh").decode(new Uint8Array([0xdb])),
|
|
new TextDecoder("windows-874").decode(new Uint8Array([0xa1])),
|
|
new TextDecoder("windows-1250").decode(new Uint8Array([0x8a])),
|
|
new TextDecoder("windows-1251").decode(new Uint8Array([0xc0])),
|
|
new TextDecoder("windows-1252").decode(new Uint8Array([0x80])),
|
|
new TextDecoder("windows-1253").decode(new Uint8Array([0xc1])),
|
|
new TextDecoder("windows-1254").decode(new Uint8Array([0xd0])),
|
|
new TextDecoder("windows-1255").decode(new Uint8Array([0xe0])),
|
|
new TextDecoder("windows-1256").decode(new Uint8Array([0xc7])),
|
|
new TextDecoder("windows-1257").decode(new Uint8Array([0xc0])),
|
|
new TextDecoder("windows-1258").decode(new Uint8Array([0xd5])),
|
|
new TextDecoder("x-mac-cyrillic").decode(new Uint8Array([0x80])),
|
|
];
|
|
console.log(JSON.stringify(singleByteSamples));
|
|
|
|
// Labels trim ASCII whitespace and fold ASCII case; latin1/ascii are the
|
|
// Encoding Standard's windows-1252 aliases, not Buffer's byte-for-byte forms.
|
|
const latinDecoder = new TextDecoder(" \tLaTiN1\r\n");
|
|
console.log(latinDecoder.decode(new Uint8Array([0x41, 0x80, 0x92, 0x42])));
|
|
console.log(new TextDecoder("ASCII").decode(new Uint8Array([0x80])));
|
|
console.log(JSON.stringify(new TextDecoder("x-user-defined").decode(new Uint8Array([0x41, 0x80, 0xff]))));
|
|
|
|
// UTF-16 has TextDecoder-specific BOM stripping and replacement for lone
|
|
// surrogates / odd trailing bytes (Buffer.toString("utf16le") differs).
|
|
console.log(new TextDecoder("utf-16le").decode(new Uint8Array([0xff, 0xfe, 0x61, 0x00, 0x3d, 0xd8, 0x00, 0xde])));
|
|
console.log(new TextDecoder("unicodefffe").decode(new Uint8Array([0xfe, 0xff, 0x00, 0x61, 0xd8, 0x3d, 0xde, 0x00])));
|
|
console.log(JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0x00, 0xd8, 0x61, 0x00, 0xff]))));
|
|
console.log(JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0x00, 0xd8, 0x12]))));
|
|
console.log(JSON.stringify(new TextDecoder("utf-16be").decode(new Uint8Array([0xd8, 0x00, 0x12]))));
|
|
|
|
// The multibyte families: two- and four-byte GB, traditional Chinese,
|
|
// Japanese stateful/stateless forms, and Korean.
|
|
console.log(new TextDecoder("GBK").decode(new Uint8Array([0xd6, 0xd0, 0xce, 0xc4])));
|
|
console.log(new TextDecoder("gb18030").decode(new Uint8Array([0x90, 0x30, 0x81, 0x30])));
|
|
console.log(new TextDecoder("big5").decode(new Uint8Array([0xa4, 0xa4, 0xa4, 0xe5])));
|
|
console.log(new TextDecoder("euc-jp").decode(new Uint8Array([0xc6, 0xfc, 0xcb, 0xdc, 0xb8, 0xec])));
|
|
console.log(new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x46, 0x7c, 0x4b, 0x5c, 0x38, 0x6c, 0x1b, 0x28, 0x42])));
|
|
console.log(new TextDecoder("shift_jis").decode(new Uint8Array([0x93, 0xfa, 0x96, 0x7b, 0x8c, 0xea])));
|
|
console.log(new TextDecoder("euc-kr").decode(new Uint8Array([0xc7, 0xd1, 0xb1, 0xdb, 0xbe, 0xee])));
|
|
|
|
// Invalid/truncated sequences pin replacement and byte reprocessing.
|
|
console.log(JSON.stringify(new TextDecoder("gb18030").decode(new Uint8Array([0x81, 0x30, 0x41]))));
|
|
console.log(JSON.stringify(new TextDecoder("big5").decode(new Uint8Array([0x81, 0x20, 0xff]))));
|
|
console.log(JSON.stringify(new TextDecoder("euc-jp").decode(new Uint8Array([0xa1, 0x20, 0x8f]))));
|
|
console.log(JSON.stringify(new TextDecoder("shift_jis").decode(new Uint8Array([0x81, 0x20, 0x81]))));
|
|
console.log(JSON.stringify(new TextDecoder("euc-kr").decode(new Uint8Array([0xa1, 0x20, 0xa1]))));
|
|
|
|
// Recovery rules vary by converter: some invalid bytes are consumed, some
|
|
// return to the input queue, and ICU recognizes a few historical extensions.
|
|
const recoverySamples = [
|
|
new TextDecoder("gb18030").decode(new Uint8Array([0x81, 0xff])),
|
|
new TextDecoder("gb18030").decode(new Uint8Array([0x84, 0x31, 0xa4, 0x37])),
|
|
new TextDecoder("big5").decode(new Uint8Array([0x81, 0xff])),
|
|
new TextDecoder("euc-jp").decode(new Uint8Array([0xa1, 0xff])),
|
|
new TextDecoder("euc-jp").decode(new Uint8Array([0x8e, 0xe0])),
|
|
new TextDecoder("euc-jp").decode(new Uint8Array([0x8f, 0xea, 0x00])),
|
|
new TextDecoder("euc-kr").decode(new Uint8Array([0xa1, 0xff])),
|
|
new TextDecoder("euc-kr").decode(new Uint8Array([0x8e, 0xa1])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x41])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x4f])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x28, 0x48, 0x5c, 0x7e])),
|
|
// In JIS mode ICU groups adjacent invalid payload bytes, but SO/SI begin
|
|
// standalone errors; a grouped CR/LF does not perform the line reset.
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x00, 0x00])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x21, 0x0e])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x00, 0x0a, 0x41])),
|
|
// Malformed escapes restore their payload in the active JIS state;
|
|
// consecutive designations toggle ICU's replacement flag.
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x1b, 0x24, 0x21])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x1b, 0x24, 0x42, 0x1b, 0x24, 0x42])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x26, 0x40, 0x1b, 0x24, 0x42, 0x46, 0x7c])),
|
|
// Four-byte designation prefixes stay pending until their final byte;
|
|
// unknown finals restore the payload while known finals are one error.
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x28, 0x00])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x28, 0x41])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x25, 0x2f, 0x42])),
|
|
// ICU's JIS-1990 announcer enters lead state even without a following
|
|
// designation; the next two payload bytes therefore form a JIS pair.
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x26, 0x40, 0x21, 0x5a])),
|
|
// ICU emits line separators in JIS/Katakana states and resumes in ASCII.
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x24, 0x42, 0x46, 0x7c, 0x0a, 0x41])),
|
|
new TextDecoder("iso-2022-jp").decode(new Uint8Array([0x1b, 0x28, 0x49, 0x21, 0x0d, 0x41])),
|
|
];
|
|
console.log(JSON.stringify(recoverySamples));
|
|
|
|
// A compact deterministic byte sweep exercises malformed-sequence recovery,
|
|
// truncated leads, state resets between decode() calls, and sparse table
|
|
// holes. JSON is the byte-for-byte oracle artifact.
|
|
const decoderVectors: number[][] = [];
|
|
let decoderSeed = 0x6d2b79f5;
|
|
for (let i = 0; i < 96; i++) {
|
|
const vector: number[] = [];
|
|
for (let j = 0; j < i % 7; j++) {
|
|
decoderSeed ^= decoderSeed << 13;
|
|
decoderSeed ^= decoderSeed >>> 17;
|
|
decoderSeed ^= decoderSeed << 5;
|
|
decoderSeed >>>= 0;
|
|
vector.push(decoderSeed >>> 24);
|
|
}
|
|
decoderVectors.push(vector);
|
|
}
|
|
|
|
const gbSweepDecoder = new TextDecoder("gb18030");
|
|
const gbSweep: string[] = [];
|
|
for (const vector of decoderVectors) gbSweep.push(gbSweepDecoder.decode(new Uint8Array(vector)));
|
|
console.log(JSON.stringify(gbSweep));
|
|
|
|
const big5SweepDecoder = new TextDecoder("big5");
|
|
const big5Sweep: string[] = [];
|
|
for (const vector of decoderVectors) big5Sweep.push(big5SweepDecoder.decode(new Uint8Array(vector)));
|
|
console.log(JSON.stringify(big5Sweep));
|
|
|
|
const eucJpSweepDecoder = new TextDecoder("euc-jp");
|
|
const eucJpSweep: string[] = [];
|
|
for (const vector of decoderVectors) eucJpSweep.push(eucJpSweepDecoder.decode(new Uint8Array(vector)));
|
|
console.log(JSON.stringify(eucJpSweep));
|
|
|
|
const isoJpSweepDecoder = new TextDecoder("iso-2022-jp");
|
|
const isoJpSweep: string[] = [];
|
|
for (const vector of decoderVectors) isoJpSweep.push(isoJpSweepDecoder.decode(new Uint8Array(vector)));
|
|
console.log(JSON.stringify(isoJpSweep));
|
|
|
|
const shiftJisSweepDecoder = new TextDecoder("shift_jis");
|
|
const shiftJisSweep: string[] = [];
|
|
for (const vector of decoderVectors) shiftJisSweep.push(shiftJisSweepDecoder.decode(new Uint8Array(vector)));
|
|
console.log(JSON.stringify(shiftJisSweep));
|
|
|
|
const eucKrSweepDecoder = new TextDecoder("euc-kr");
|
|
const eucKrSweep: string[] = [];
|
|
for (const vector of decoderVectors) eucKrSweep.push(eucKrSweepDecoder.decode(new Uint8Array(vector)));
|
|
console.log(JSON.stringify(eucKrSweep));
|
|
|
|
// A switch clause is still one straight-line execution region: the
|
|
// declaration dominates the call when both live in that clause.
|
|
const codecCase = 1;
|
|
switch (codecCase) {
|
|
case 1:
|
|
const caseEncoder = new TextEncoder();
|
|
console.log(caseEncoder.encode("same clause").length);
|
|
break;
|
|
}
|