mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 13:56:06 +00:00
* fix: report provider admission failures as typed codes * fix(evm): keep limiter path on admission diagnostics * fix: assert WABT and CBOR admission codes * fix: keep admission details on returned failures * fix(evm): detect repeated CBOR keys in the decoder --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
567 lines
18 KiB
TypeScript
567 lines
18 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { expect, it } from "vitest";
|
|
|
|
const refused = (code: string) =>
|
|
expect.objectContaining({ _tag: "WabtAdmissionError", code });
|
|
import { parseWabtOutput } from "./WabtOutput.js";
|
|
import { parseWabtWat } from "./WabtWat.js";
|
|
import { associateWasmGlue } from "./WasmGlueAdapter.js";
|
|
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
|
const golden = () =>
|
|
readFile(
|
|
new URL("../../tests/fixtures/wasm/objdump.txt", import.meta.url),
|
|
"utf8",
|
|
);
|
|
it("corroborates a blank custom name with bounded invalid UTF-8 bytes", () => {
|
|
const bytes = Buffer.from("0061736d01000000000a02fffe7061796c6f6164", "hex");
|
|
const dump = [
|
|
"",
|
|
"module.wasm:\tfile format wasm 0x1",
|
|
"",
|
|
"Sections:",
|
|
"",
|
|
" Custom start=0x0000000a end=0x00000014 (size=0x0000000a) ",
|
|
"Section Details:",
|
|
"",
|
|
].join("\n");
|
|
const parsed = parseWabtOutput(dump, bytes.length, [], [], bytes);
|
|
expect(parsed.invalidNameEnds).toEqual([13]);
|
|
expect(parsed.sections).toMatchObject([{ kind: "Custom", description: "" }]);
|
|
const valid = Buffer.from(bytes);
|
|
valid[11] = 0x61;
|
|
valid[12] = 0x62;
|
|
expect(() => parseWabtOutput(dump, valid.length, [], [], valid)).toThrow(
|
|
refused("blank_custom_name"),
|
|
);
|
|
expect(() => parseWabtOutput(dump, bytes.length, [])).toThrow(
|
|
refused("blank_custom_name"),
|
|
);
|
|
const mixed = Buffer.concat([bytes, Buffer.from("00050174616263", "hex")]);
|
|
const mixedDump = dump.replace(
|
|
"\nSection Details:",
|
|
' Custom start=0x00000016 end=0x0000001b (size=0x00000005) "t"\n\nSection Details:',
|
|
);
|
|
expect(
|
|
parseWabtOutput(mixedDump, mixed.length, ["t"], [], mixed).sections,
|
|
).toMatchObject([
|
|
{ kind: "Custom", description: "" },
|
|
{ kind: "Custom", description: '"t"' },
|
|
]);
|
|
});
|
|
it("retains real WABT section ranges, custom section identity and original header output", async () => {
|
|
const text = await golden();
|
|
const headers = text.slice(0, text.indexOf("Section Details:\n"));
|
|
expect(parseWabtOutput(text, 76, ["test"]).sections).toHaveLength(7);
|
|
expect(parseWabtOutput(text, 76, ["test"]).headers).toBe(headers);
|
|
expect(parseWabtOutput(text, 76, ["test"]).sections.at(-1)).toEqual({
|
|
kind: "Custom",
|
|
start: 64,
|
|
end: 76,
|
|
bytes: 12,
|
|
description: '"test"',
|
|
});
|
|
expect(() => parseWabtOutput(text, 75, ["test"])).toThrow(
|
|
refused("section_ranges"),
|
|
);
|
|
expect(() =>
|
|
parseWabtOutput(text.replace("size=0x0000000c", "size=0x0000000d"), 76, [
|
|
"test",
|
|
]),
|
|
).toThrow(refused("section_ranges"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
text.replace("\nSection Details:\n", "unexpected\nSection Details:\n"),
|
|
76,
|
|
["test"],
|
|
),
|
|
).toThrow(refused("unrecognized_section_row"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
text.replace(
|
|
"\nSection Details:\n",
|
|
"\nDataCount start=0x00000040 end=0x0000004c (size=0x0000000c)\nSection Details:\n",
|
|
),
|
|
76,
|
|
["test"],
|
|
),
|
|
).toThrow(refused("unrecognized_section_row"));
|
|
});
|
|
const dataCountModuleBytes = 0x3d;
|
|
const unpaddedDataCountRow =
|
|
"DataCount start=0x00000023 end=0x00000024 (size=0x00000001) count: 1";
|
|
const dataCountDump = [
|
|
"",
|
|
"module.wasm:\tfile format wasm 0x1",
|
|
"",
|
|
"Sections:",
|
|
"",
|
|
" Type start=0x0000000a end=0x0000000e (size=0x00000004) count: 1",
|
|
" Function start=0x00000010 end=0x00000012 (size=0x00000002) count: 1",
|
|
" Memory start=0x00000014 end=0x00000017 (size=0x00000003) count: 1",
|
|
" Export start=0x00000019 end=0x00000021 (size=0x00000008) count: 1",
|
|
unpaddedDataCountRow,
|
|
" Code start=0x00000026 end=0x00000037 (size=0x00000011) count: 1",
|
|
" Data start=0x00000039 end=0x0000003d (size=0x00000004) count: 1",
|
|
"",
|
|
"Section Details:",
|
|
"",
|
|
].join("\n");
|
|
it("accepts an unpadded DataCount row and still rejects malformed output and bad ranges", () => {
|
|
const parsed = parseWabtOutput(dataCountDump, dataCountModuleBytes, []);
|
|
expect(parsed.sections.map(({ kind }) => kind)).toEqual([
|
|
"Type",
|
|
"Function",
|
|
"Memory",
|
|
"Export",
|
|
"DataCount",
|
|
"Code",
|
|
"Data",
|
|
]);
|
|
expect(parsed.sections.find(({ kind }) => kind === "DataCount")).toEqual({
|
|
kind: "DataCount",
|
|
start: 0x23,
|
|
end: 0x24,
|
|
bytes: 1,
|
|
description: "count: 1",
|
|
});
|
|
expect(parsed.headers).toContain(`\n${unpaddedDataCountRow}\n`);
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
dataCountDump.replace(unpaddedDataCountRow, "DataCount count: 1"),
|
|
dataCountModuleBytes,
|
|
[],
|
|
),
|
|
).toThrow(refused("unrecognized_section_row"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
dataCountDump.replace(unpaddedDataCountRow, "Sections:"),
|
|
dataCountModuleBytes,
|
|
[],
|
|
),
|
|
).toThrow(refused("unrecognized_section_row"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
dataCountDump.replace(
|
|
unpaddedDataCountRow,
|
|
"DataCount start=0x00000023 end=0x00000024 (size=0x00000001)count: 1",
|
|
),
|
|
dataCountModuleBytes,
|
|
[],
|
|
),
|
|
).toThrow(refused("unrecognized_section_row"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
dataCountDump.replace("(size=0x00000001)", "(size=0x00000002)"),
|
|
dataCountModuleBytes,
|
|
[],
|
|
),
|
|
).toThrow(refused("section_ranges"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
dataCountDump.replace(
|
|
"DataCount start=0x00000023 end=0x00000024 (size=0x00000001)",
|
|
"DataCount start=0x00000020 end=0x00000021 (size=0x00000001)",
|
|
),
|
|
dataCountModuleBytes,
|
|
[],
|
|
),
|
|
).toThrow(refused("section_ranges"));
|
|
expect(() =>
|
|
parseWabtOutput(dataCountDump, dataCountModuleBytes - 1, []),
|
|
).toThrow(refused("section_ranges"));
|
|
expect(() =>
|
|
parseWabtOutput(
|
|
dataCountDump.replace(
|
|
"\nmodule.wasm:\tfile format wasm 0x1\n\nSections:\n\n",
|
|
"",
|
|
),
|
|
dataCountModuleBytes,
|
|
[],
|
|
),
|
|
).toThrow(refused("unsupported_section_header"));
|
|
});
|
|
it("bounds section header bytes while accepting a five-byte size LEB", () => {
|
|
const paddedCustomDump = [
|
|
"",
|
|
"module.wasm:\tfile format wasm 0x1",
|
|
"",
|
|
"Sections:",
|
|
"",
|
|
' Custom start=0x0000000e end=0x0000000f (size=0x00000001) ""',
|
|
"",
|
|
"Section Details:",
|
|
"",
|
|
"Custom:",
|
|
' - name: ""',
|
|
"",
|
|
].join("\n");
|
|
expect(parseWabtOutput(paddedCustomDump, 15, [""]).sections).toMatchObject([
|
|
{ kind: "Custom", start: 14, end: 15, bytes: 1 },
|
|
]);
|
|
|
|
for (const gap of [0, 1, 7]) {
|
|
const start = 11 + gap;
|
|
const malformed = [
|
|
"",
|
|
"module.wasm:\tfile format wasm 0x1",
|
|
"",
|
|
"Sections:",
|
|
"",
|
|
" Type start=0x0000000a end=0x0000000b (size=0x00000001) count: 0",
|
|
` Custom start=0x${start.toString(16).padStart(8, "0")} end=0x${(start + 1).toString(16).padStart(8, "0")} (size=0x00000001) ""`,
|
|
"",
|
|
"Section Details:",
|
|
"",
|
|
].join("\n");
|
|
expect(() => parseWabtOutput(malformed, start + 1, [""])).toThrow(
|
|
refused("section_ranges"),
|
|
);
|
|
}
|
|
});
|
|
it.each([
|
|
{ displayed: [], decoded: ["named"] },
|
|
{ displayed: ["named"], decoded: [] },
|
|
{ displayed: ["other"], decoded: ["named"] },
|
|
])(
|
|
"rejects module-name headers inconsistent with decoded WAT: %j",
|
|
async ({ displayed, decoded }) => {
|
|
const text = (await golden()).replace(
|
|
"\n\nSections:",
|
|
`\n${displayed.map((name) => `module name: <${name}>\n`).join("")}\nSections:`,
|
|
);
|
|
expect(() => parseWabtOutput(text, 76, ["test"], decoded)).toThrow(
|
|
refused("unsupported_section_header"),
|
|
);
|
|
},
|
|
);
|
|
it.each([
|
|
'(module (@custom "name" "\\00\\06\\05name"))',
|
|
'(module (@custom "name" "\\00\\05\\05name"))',
|
|
])("retains an uninterpretable optional name section: %s", (text) => {
|
|
expect(parseWabtWat(text)).toMatchObject({
|
|
customSectionNames: ["name"],
|
|
debugModuleNames: [],
|
|
debugNameMetadata: "uninterpretable",
|
|
});
|
|
});
|
|
it.each([
|
|
String.raw`\00\06\05hello\01\02\01`,
|
|
String.raw`\00\07\05hello\00`,
|
|
String.raw`\00\06\05hello\00\06\05later`,
|
|
])(
|
|
"preserves a module name already observed before malformed metadata: %s",
|
|
(payload) => {
|
|
expect(
|
|
parseWabtWat(`(module (@custom "name" "${payload}"))`),
|
|
).toMatchObject({
|
|
debugModuleNames: ["hello"],
|
|
debugNameMetadata: "uninterpretable",
|
|
});
|
|
},
|
|
);
|
|
it("stops before a module name that WABT rejects as out of order", () => {
|
|
expect(
|
|
parseWabtWat(String.raw`(module (@custom "name" "\01\00\00\06\05hello"))`),
|
|
).toMatchObject({
|
|
debugModuleNames: [],
|
|
debugNameMetadata: "uninterpretable",
|
|
});
|
|
});
|
|
it("keeps observed names across custom sections while coverage stays uninterpretable", () => {
|
|
expect(
|
|
parseWabtWat(String.raw`(module
|
|
(@custom "name" "\00\06\05hello")
|
|
(@custom "name" "\00\06\05short\01\02\01")
|
|
(@custom "name" "\00\06\05later"))`),
|
|
).toMatchObject({
|
|
customSectionNames: ["name", "name", "name"],
|
|
debugModuleNames: ["hello", "short", "later"],
|
|
debugNameMetadata: "uninterpretable",
|
|
});
|
|
});
|
|
it("frames custom section names at their NUL-terminated objdump display", async () => {
|
|
const text = await golden();
|
|
expect(parseWabtOutput(text, 76, ["test\0hidden"]).sections.at(-1)).toEqual({
|
|
kind: "Custom",
|
|
start: 64,
|
|
end: 76,
|
|
bytes: 12,
|
|
description: '"test"',
|
|
});
|
|
expect(() => parseWabtOutput(text, 76, ["other\0test"])).toThrow(
|
|
refused("custom_header_mismatch"),
|
|
);
|
|
});
|
|
it("preserves leading U+FEFF as custom-name data, not a debug-name marker", () => {
|
|
const parsed = parseWabtWat(
|
|
'(module (@custom "\\ef\\bb\\bfsection" "payload") (@custom "\\ef\\bb\\bfname" "payload") (@custom "\\ef\\bb\\bf" "payload") (@custom "" "payload"))',
|
|
);
|
|
expect(parsed.customSectionNames).toEqual([
|
|
"\uFEFFsection",
|
|
"\uFEFFname",
|
|
"\uFEFF",
|
|
"",
|
|
]);
|
|
expect(parsed.debugModuleNames).toEqual([]);
|
|
expect(() => parseWabtWat('(module (@custom "\\ff" "payload"))')).toThrow();
|
|
});
|
|
it("extracts escaped WAT import/export forms while retaining multiline producer text", async () => {
|
|
const text = await readFile(
|
|
new URL("../../tests/fixtures/wasm/decoded.wat", import.meta.url),
|
|
"utf8",
|
|
);
|
|
expect(parseWabtWat(text)).toMatchObject({
|
|
imports: ['(import "env" "foo" (func (;0;) (type 0)))'],
|
|
exports: ['(export "memory" (memory 0))', '(export "run" (func 1))'],
|
|
customSectionNames: ["test"],
|
|
debugModuleNames: [],
|
|
});
|
|
expect(
|
|
parseWabtWat(
|
|
'(module (import "a.b" "c\\0aline" (func)) (export "fake\\22Export[1]:" (func 0)))',
|
|
),
|
|
).toMatchObject({
|
|
imports: ['(import "a.b" "c\\0aline" (func))'],
|
|
exports: ['(export "fake\\22Export[1]:" (func 0))'],
|
|
customSectionNames: [],
|
|
debugModuleNames: [],
|
|
});
|
|
expect(
|
|
parseWabtWat(
|
|
'(module (; nested (; comment ;) ;) ;; comment\n (export "x" (func 0)))',
|
|
).exports,
|
|
).toEqual(['(export "x" (func 0))']);
|
|
expect(() => parseWabtWat('(module (export "unfinished"')).toThrow(
|
|
refused("incomplete_output"),
|
|
);
|
|
expect(() => parseWabtWat("(module) unexpected")).toThrow(
|
|
refused("unexpected_text"),
|
|
);
|
|
});
|
|
it("keeps exact local, ambiguous URL, computed and unparseable glue distinct", () => {
|
|
const candidates = ["/app/module.wasm", "/other/module.wasm"];
|
|
const result = associateWasmGlue(
|
|
'fetch("./module.wasm"); new URL("https://example.com/module.wasm?x=1", import.meta.url); fetch(`module.wasm`); fetch(name + ".wasm");',
|
|
"/app/glue.js",
|
|
candidates,
|
|
);
|
|
expect(result.parse_status).toBe("complete");
|
|
expect(result.references).toMatchObject([
|
|
{
|
|
value: "./module.wasm",
|
|
association: "local-path-candidate",
|
|
candidate_paths: ["/app/module.wasm"],
|
|
},
|
|
{
|
|
value: "https://example.com/module.wasm?x=1",
|
|
association: "basename-candidates",
|
|
candidate_paths: candidates,
|
|
},
|
|
{ value: "module.wasm", association: "local-path-candidate" },
|
|
{ value: ".wasm", association: "unresolved" },
|
|
]);
|
|
expect(associateWasmGlue("function (", "/glue.js", candidates)).toEqual({
|
|
parse_status: "failed",
|
|
references: [],
|
|
});
|
|
});
|
|
|
|
it("retains .wasm references in glue whose conditional arrows only parse as JavaScript", () => {
|
|
const candidates = ["/app/module.wasm"];
|
|
// TypeScript grammar reads `n===0?H=>(x):H=>(y)` as a return type, so the
|
|
// unnamed-source fallback fails the file; the .js filename selects JavaScript.
|
|
const conditional = associateWasmGlue(
|
|
'const pick=n===0?H=>({start:H.startY}):H=>({start:H.startX});\nfetch("./module.wasm");\n',
|
|
"/app/glue.js",
|
|
candidates,
|
|
);
|
|
expect(conditional.parse_status).toBe("complete");
|
|
expect(conditional.references).toMatchObject([
|
|
{
|
|
value: "./module.wasm",
|
|
line: 2,
|
|
column: 6,
|
|
candidate_paths: candidates,
|
|
association: "local-path-candidate",
|
|
evidence_kind: "static-literal",
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
associateWasmGlue('fetch("./module.wasm");\n', "/app/glue.js", candidates)
|
|
.references,
|
|
).toMatchObject([
|
|
{
|
|
value: "./module.wasm",
|
|
line: 1,
|
|
column: 6,
|
|
association: "local-path-candidate",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("retains directive literals and matches their cooked escaped path", () => {
|
|
const candidates = ["/app/module.wasm"];
|
|
const directive = associateWasmGlue(
|
|
'"module.wasm";\n',
|
|
"/app/glue.js",
|
|
candidates,
|
|
);
|
|
expect(directive.references).toMatchObject([
|
|
{
|
|
value: "module.wasm",
|
|
line: 1,
|
|
column: 0,
|
|
candidate_paths: candidates,
|
|
association: "local-path-candidate",
|
|
evidence_kind: "static-literal",
|
|
},
|
|
]);
|
|
|
|
const escaped = String.raw`"mod\u0075le\x2ewasm";`;
|
|
expect(
|
|
associateWasmGlue(escaped, "/app/glue.js", candidates).references,
|
|
).toMatchObject([
|
|
{
|
|
value: "module.wasm",
|
|
line: 1,
|
|
column: 0,
|
|
candidate_paths: candidates,
|
|
association: "local-path-candidate",
|
|
evidence_kind: "static-literal",
|
|
},
|
|
]);
|
|
expect(
|
|
associateWasmGlue(`;${escaped}`, "/app/glue.js", candidates).references,
|
|
).toMatchObject([
|
|
{
|
|
value: "module.wasm",
|
|
line: 1,
|
|
column: 1,
|
|
candidate_paths: candidates,
|
|
association: "local-path-candidate",
|
|
evidence_kind: "static-literal",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("matches a CLI-selected ./ module exactly against its glue reference", () => {
|
|
// The CLI makes `./module.wasm` absolute from /app with absoluteCallerPath.
|
|
const selected = absoluteCallerPath("./module.wasm", {
|
|
cwd: "/app",
|
|
platform: "linux",
|
|
});
|
|
expect(
|
|
associateWasmGlue('fetch("./module.wasm");', "/app/glue.js", [
|
|
selected,
|
|
"/other/module.wasm",
|
|
]).references,
|
|
).toMatchObject([
|
|
{
|
|
association: "local-path-candidate",
|
|
candidate_paths: ["/app/module.wasm"],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it.each(["\n", "\r", "\r\n", "\u2028", "\u2029"])(
|
|
"retains complete .wasm query and fragment literals containing %j",
|
|
(separator) => {
|
|
const candidates = ["/artifacts/module.wasm", "/other/module.wasm"];
|
|
for (const qualifier of ["?query=", "#fragment="]) {
|
|
const value = `https://example.invalid/module.wasm${qualifier}a${separator}b`;
|
|
const result = associateWasmGlue(
|
|
`fetch(${JSON.stringify(value)});`,
|
|
"/artifacts/glue.js",
|
|
candidates,
|
|
);
|
|
expect(result).toEqual({
|
|
parse_status: "complete",
|
|
references: [
|
|
{
|
|
value,
|
|
line: 1,
|
|
column: 6,
|
|
candidate_paths: candidates,
|
|
association: "basename-candidates",
|
|
evidence_kind: "static-literal",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(["\n", "\r", "\r\n", "\u2028", "\u2029"])(
|
|
"does not accept a path-ending separator without a query or fragment: %j",
|
|
(separator) => {
|
|
const value = `https://example.invalid/module.wasm${separator}`;
|
|
expect(
|
|
associateWasmGlue(`fetch(${JSON.stringify(value)});`, "/glue.js", []),
|
|
).toEqual({ parse_status: "complete", references: [] });
|
|
},
|
|
);
|
|
|
|
it("tests long multi-line glue literals for a .wasm suffix in linear time", () => {
|
|
const value = `${".wasm#".repeat(32_000)}\nend`;
|
|
const started = performance.now();
|
|
const result = associateWasmGlue(
|
|
`const s = ${JSON.stringify(value)};`,
|
|
"/glue.js",
|
|
[],
|
|
);
|
|
expect(result.parse_status).toBe("complete");
|
|
expect(result.references).toHaveLength(1);
|
|
expect(result.references[0]?.value).toBe(value);
|
|
const negative = `${".wasm!".repeat(32_000)}\nend`;
|
|
expect(
|
|
associateWasmGlue(`const s = ${JSON.stringify(negative)};`, "/glue.js", []),
|
|
).toEqual({ parse_status: "complete", references: [] });
|
|
expect(performance.now() - started).toBeLessThan(1_000);
|
|
expect(
|
|
associateWasmGlue("const s = `first\nmodule.wasm?v=1`;", "/glue.js", [])
|
|
.references,
|
|
).toMatchObject([{ value: "first\nmodule.wasm?v=1", line: 1 }]);
|
|
});
|
|
|
|
it("retains earlier module names for objdump framing when a later optional subsection is malformed", async () => {
|
|
const parsed = parseWabtWat(
|
|
'(module $hello (@custom "name" "\\00\\06\\05hello\\01\\02\\01"))',
|
|
);
|
|
expect(parsed).toMatchObject({
|
|
debugModuleNames: ["hello"],
|
|
debugNameMetadata: "uninterpretable",
|
|
});
|
|
const text = (await golden()).replace(
|
|
"\n\nSections:",
|
|
"\nmodule name: <hello>\n\nSections:",
|
|
);
|
|
expect(
|
|
parseWabtOutput(text, 76, ["test"], parsed.debugModuleNames).sections,
|
|
).toHaveLength(7);
|
|
});
|
|
|
|
it.each([
|
|
'import source module from "./module.wasm";',
|
|
'const module = await import.source("./module.wasm");',
|
|
'import * as module from "./module.wasm";',
|
|
'const module = await import("./module.wasm");',
|
|
])("retains source-phase and ordinary WASM import literals: %s", (source) => {
|
|
const offset = source.indexOf('"./module.wasm"');
|
|
expect(
|
|
associateWasmGlue(source, "/app/glue.mjs", ["/app/module.wasm"]),
|
|
).toEqual({
|
|
parse_status: "complete",
|
|
references: [
|
|
{
|
|
value: "./module.wasm",
|
|
line: 1,
|
|
column: offset,
|
|
candidate_paths: ["/app/module.wasm"],
|
|
association: "local-path-candidate",
|
|
evidence_kind: "static-literal",
|
|
},
|
|
],
|
|
});
|
|
});
|