Files
reverse-engineering-anything/src/wasm/WabtOutput.ts
T
morlutoandCursor Agent 317bf3fe23 fix: report provider admission failures as typed codes (#8066)
* 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>
2026-10-30 06:48:01 +08:00

123 lines
4.4 KiB
TypeScript

import { WabtAdmissionError } from "./WabtAdmissionError.js";
/** WABT prints names with %.*s, so an embedded NUL ends their display. */
const wabtDisplayedName = (name: string | undefined): string | undefined =>
name?.split("\0", 1)[0];
/** Frame WABT's raw custom names using the ordered names from its escaped WAT output. */
export const parseWabtOutput = (
text: string,
artifactBytes: number,
customSectionNames: readonly string[],
debugModuleNames: readonly string[] = [],
artifact?: Uint8Array,
) => {
const preamble =
"\nmodule.wasm:\tfile format wasm 0x1\n" +
debugModuleNames
.map((name) => `module name: <${wabtDisplayedName(name)}>\n`)
.join("");
const prefix = `${preamble}\nSections:\n\n`;
if (!text.startsWith(prefix))
throw new WabtAdmissionError("unsupported_section_header");
let offset = prefix.length;
let customIndex = 0;
const sections = [];
const invalidNameEnds: number[] = [];
while (!text.startsWith("\nSection Details:\n", offset)) {
const match =
/^[ \t]*(\w+) start=0x([\da-f]+) end=0x([\da-f]+) \(size=0x([\da-f]+)\) /u.exec(
text.slice(offset),
);
if (match === null)
throw new WabtAdmissionError("unrecognized_section_row");
offset += match[0].length;
let description: string;
if (match[1] === "Custom" && !text.startsWith('"', offset)) {
// WABT leaves this field blank when the binary custom name is invalid
// UTF-8. Confirm the bytes rather than inventing a name from its output.
const start = Number.parseInt(match[2] ?? "", 16);
const end = Number.parseInt(match[3] ?? "", 16);
const invalidEnd = invalidCustomNameEnd(artifact, start, end);
if (invalidEnd === undefined)
throw new WabtAdmissionError("blank_custom_name");
invalidNameEnds.push(invalidEnd);
description = "";
// WABT does not terminate a blank-name row when another section follows.
if (
text.startsWith("\n", offset) &&
!text.startsWith("\nSection Details:\n", offset)
)
offset += 1;
} else if (match[1] === "Custom") {
const name = wabtDisplayedName(customSectionNames[customIndex++]);
if (name === undefined || !text.startsWith(`"${name}"\n`, offset))
throw new WabtAdmissionError("custom_header_mismatch");
description = `"${name}"`;
offset += description.length + 1;
} else {
const end = text.indexOf("\n", offset);
if (end < 0) throw new WabtAdmissionError("incomplete_section_row");
description = text.slice(offset, end).trim();
offset = end + 1;
}
const section = {
kind: match[1] ?? "",
start: Number.parseInt(match[2] ?? "", 16),
end: Number.parseInt(match[3] ?? "", 16),
bytes: Number.parseInt(match[4] ?? "", 16),
description,
};
const sectionHeaderBytes = section.start - (sections.at(-1)?.end ?? 8);
if (
section.end > artifactBytes ||
section.end - section.start !== section.bytes ||
sectionHeaderBytes < 2 ||
sectionHeaderBytes > 6
)
throw new WabtAdmissionError("section_ranges");
sections.push(section);
}
if (customIndex !== customSectionNames.length)
throw new WabtAdmissionError("custom_inventory_mismatch");
if ((sections.at(-1)?.end ?? 8) !== artifactBytes)
throw new WabtAdmissionError("section_coverage");
const separator = offset + 1;
return {
sections,
headers: text.slice(0, separator),
details: `${preamble.slice(1)}\n${text.slice(separator)}`,
invalidNameEnds,
};
};
/** Return the end of a bounded invalid UTF-8 name, or undefined if unproved. */
const invalidCustomNameEnd = (
bytes: Uint8Array | undefined,
start: number,
end: number,
): number | undefined => {
if (bytes === undefined || end > bytes.length || start >= end)
return undefined;
let length = 0;
let position = start;
for (let shift = 0; shift <= 28; shift += 7) {
const byte = bytes[position++];
if (byte === undefined || position > end || (shift === 28 && byte > 15))
return undefined;
length += (byte & 0x7f) * 2 ** shift;
if ((byte & 0x80) !== 0) continue;
const nameEnd = position + length;
if (nameEnd > end) return undefined;
try {
new TextDecoder("utf-8", { fatal: true }).decode(
bytes.subarray(position, nameEnd),
);
return undefined;
} catch {
return nameEnd;
}
}
return undefined;
};