mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 05:46:17 +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>
123 lines
4.4 KiB
TypeScript
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;
|
|
};
|