Initial Commit
This commit is contained in:
455
packages/api-stack/api/api-sandbox-wasm-host-bridge/tests/fixtures/build-wasm.ts
vendored
Normal file
455
packages/api-stack/api/api-sandbox-wasm-host-bridge/tests/fixtures/build-wasm.ts
vendored
Normal file
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* Hand-encoded WASM fixture builder for the wasm host bridge's tests +
|
||||
* conformance harness. Produces deterministic, validator-clean WASM
|
||||
* modules implementing the `__suaveplan_*` ABI surface with configurable
|
||||
* behaviour per export so the test suite can exercise every bridge
|
||||
* production path WITHOUT pulling in `wabt` or any other external WASM
|
||||
* build tool.
|
||||
*
|
||||
* Memory layout (single 1-page memory):
|
||||
* [ 0, 1024) — reserved scratch (cleared on init).
|
||||
* [1024, 1024 + routesBytes.length) — routes JSON.
|
||||
* [2048, 2048 + responseBytes.length) — canned response JSON.
|
||||
* [3072, 3072 + WITNESS_CAPACITY) — request-byte witness.
|
||||
* [4096, 65536) — bump allocator arena.
|
||||
*
|
||||
* Global 0: mut i32 = 4096 (bump pointer).
|
||||
*/
|
||||
|
||||
import {
|
||||
concat,
|
||||
encodeI32,
|
||||
encodeName,
|
||||
encodeU32,
|
||||
section,
|
||||
vec,
|
||||
} from "./leb128.js";
|
||||
import {
|
||||
BLOCK_TYPE_EMPTY,
|
||||
funcBody,
|
||||
OP_BLOCK,
|
||||
OP_END,
|
||||
OP_I32_ADD,
|
||||
OP_I32_CONST,
|
||||
OP_I32_EQZ,
|
||||
OP_I32_LT_U,
|
||||
OP_I64_CONST,
|
||||
OP_LOOP,
|
||||
OP_UNREACHABLE,
|
||||
opBr,
|
||||
opBrIf,
|
||||
opCall,
|
||||
opGlobalGet,
|
||||
opGlobalSet,
|
||||
opI32Const,
|
||||
opI32Load8U,
|
||||
opI32Store8,
|
||||
opLocalGet,
|
||||
opLocalSet,
|
||||
VT_I32,
|
||||
VT_I64,
|
||||
} from "./wasm-opcodes.js";
|
||||
|
||||
const ROUTES_OFFSET = 1024;
|
||||
const RESPONSE_OFFSET = 2048;
|
||||
export const WITNESS_OFFSET = 3072;
|
||||
export const WITNESS_CAPACITY = 1024;
|
||||
const HEAP_START = 4096;
|
||||
|
||||
const MAGIC = new Uint8Array([0x00, 0x61, 0x73, 0x6d]);
|
||||
const VERSION = new Uint8Array([0x01, 0x00, 0x00, 0x00]);
|
||||
|
||||
/** Configurable behaviour per ABI export. */
|
||||
export interface BuildFixtureWasmOptions {
|
||||
/**
|
||||
* The JSON-encoded `SerializedRoute[]` the guest's
|
||||
* `__suaveplan_routes` returns. Defaults to a single-route catalog.
|
||||
*/
|
||||
readonly routesJson?: string;
|
||||
/**
|
||||
* The JSON-encoded `SerializedResponse` the guest's
|
||||
* `__suaveplan_handle_request` returns. Defaults to a 200 with an
|
||||
* empty body.
|
||||
*/
|
||||
readonly responseJson?: string;
|
||||
/** Force `__suaveplan_alloc` to return 0 (OOM). */
|
||||
readonly allocAlwaysZero?: boolean;
|
||||
/** Force `__suaveplan_handle_request` to execute `unreachable`. */
|
||||
readonly handlerTraps?: boolean;
|
||||
/** Force `__suaveplan_start` to execute `unreachable`. */
|
||||
readonly startTraps?: boolean;
|
||||
/** Force `__suaveplan_dispose` to execute `unreachable`. */
|
||||
readonly disposeTraps?: boolean;
|
||||
/** Force `__suaveplan_routes` to execute `unreachable`. */
|
||||
readonly routesTraps?: boolean;
|
||||
/** Force `__suaveplan_routes` to return `(0n, 0n)` packed. */
|
||||
readonly routesEmpty?: boolean;
|
||||
/** Force `__suaveplan_handle_request` to return `(0n, 0n)` packed. */
|
||||
readonly handlerEmptyResponse?: boolean;
|
||||
/**
|
||||
* Force `__suaveplan_handle_request` to return `(ptr, 0)` packed
|
||||
* with the supplied non-zero pointer. Used to test the
|
||||
* `(nonzero, 0)` empty-response branch where the bridge must free
|
||||
* the dangling pointer.
|
||||
*/
|
||||
readonly handlerZeroLengthResponse?: number;
|
||||
/** Drop one or more required exports — exercises the ABI validator. */
|
||||
readonly dropExports?: readonly (
|
||||
| "memory"
|
||||
| "__suaveplan_alloc"
|
||||
| "__suaveplan_free"
|
||||
| "__suaveplan_handle_request"
|
||||
| "__suaveplan_routes"
|
||||
| "__suaveplan_start"
|
||||
| "__suaveplan_dispose"
|
||||
)[];
|
||||
}
|
||||
|
||||
const ENCODER = new TextEncoder();
|
||||
const DEFAULT_ROUTES_JSON = JSON.stringify([{ method: "GET", path: "/hello" }]);
|
||||
const DEFAULT_RESPONSE_JSON = JSON.stringify({
|
||||
status: 200,
|
||||
headers: [["content-type", "text/plain"]],
|
||||
bodyBase64: btoa("ok")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, ""),
|
||||
});
|
||||
|
||||
/**
|
||||
* Build a validator-clean WASM module. The returned bytes are accepted
|
||||
* by `WebAssembly.compile` and instantiate against an empty imports
|
||||
* object (no WASI, no capability imports — the fixture is a pure
|
||||
* compute module).
|
||||
*/
|
||||
export function buildFixtureWasm(
|
||||
options: BuildFixtureWasmOptions = {}
|
||||
): Uint8Array {
|
||||
const routesBytes = ENCODER.encode(
|
||||
options.routesJson ?? DEFAULT_ROUTES_JSON
|
||||
);
|
||||
const responseBytes = ENCODER.encode(
|
||||
options.responseJson ?? DEFAULT_RESPONSE_JSON
|
||||
);
|
||||
if (routesBytes.length > RESPONSE_OFFSET - ROUTES_OFFSET) {
|
||||
throw new RangeError("routesJson too large for fixture memory layout");
|
||||
}
|
||||
if (responseBytes.length > WITNESS_OFFSET - RESPONSE_OFFSET) {
|
||||
throw new RangeError(
|
||||
"responseJson too large for fixture memory layout"
|
||||
);
|
||||
}
|
||||
const dropExports = new Set(options.dropExports ?? []);
|
||||
|
||||
// --- Type section ---------------------------------------------------
|
||||
// type 0: () -> ()
|
||||
// type 1: (i32) -> i32
|
||||
// type 2: (i32) -> ()
|
||||
// type 3: (i32, i32) -> i64
|
||||
// type 4: () -> i64
|
||||
// type 5: (i32, i32, i32) -> () [memcpy helper]
|
||||
const typeSection = section(
|
||||
0x01,
|
||||
vec([
|
||||
new Uint8Array([0x60, 0x00, 0x00]),
|
||||
new Uint8Array([0x60, 0x01, VT_I32, 0x01, VT_I32]),
|
||||
new Uint8Array([0x60, 0x01, VT_I32, 0x00]),
|
||||
new Uint8Array([0x60, 0x02, VT_I32, VT_I32, 0x01, VT_I64]),
|
||||
new Uint8Array([0x60, 0x00, 0x01, VT_I64]),
|
||||
new Uint8Array([0x60, 0x03, VT_I32, VT_I32, VT_I32, 0x00]),
|
||||
])
|
||||
);
|
||||
|
||||
// --- Function section (declares the type index of each defined fn) -
|
||||
// Function indices (no imports):
|
||||
// 0: __suaveplan_alloc — type 1
|
||||
// 1: __suaveplan_free — type 2
|
||||
// 2: __suaveplan_handle_request — type 3
|
||||
// 3: __suaveplan_routes — type 4
|
||||
// 4: __suaveplan_start — type 0
|
||||
// 5: __suaveplan_dispose — type 0
|
||||
// 6: memcpy_internal — type 5
|
||||
const funcSection = section(
|
||||
0x03,
|
||||
vec([
|
||||
encodeU32(1),
|
||||
encodeU32(2),
|
||||
encodeU32(3),
|
||||
encodeU32(4),
|
||||
encodeU32(0),
|
||||
encodeU32(0),
|
||||
encodeU32(5),
|
||||
])
|
||||
);
|
||||
|
||||
// --- Memory section -------------------------------------------------
|
||||
// One memory with initial=1 page, no max.
|
||||
const memorySection = section(0x05, vec([new Uint8Array([0x00, 0x01])]));
|
||||
|
||||
// --- Global section -------------------------------------------------
|
||||
// Global 0: (mut i32) = HEAP_START
|
||||
const globalSection = section(
|
||||
0x06,
|
||||
vec([
|
||||
concat(
|
||||
new Uint8Array([VT_I32, 0x01]),
|
||||
new Uint8Array([OP_I32_CONST]),
|
||||
encodeI32(HEAP_START),
|
||||
new Uint8Array([OP_END])
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
// --- Export section -------------------------------------------------
|
||||
const exportEntries: Uint8Array[] = [];
|
||||
const pushExport = (name: string, kind: number, index: number): void => {
|
||||
if (dropExports.has(name as never)) return;
|
||||
exportEntries.push(
|
||||
concat(encodeName(name), new Uint8Array([kind, index]))
|
||||
);
|
||||
};
|
||||
// 0x00 = function, 0x02 = memory.
|
||||
pushExport("memory", 0x02, 0);
|
||||
pushExport("__suaveplan_alloc", 0x00, 0);
|
||||
pushExport("__suaveplan_free", 0x00, 1);
|
||||
pushExport("__suaveplan_handle_request", 0x00, 2);
|
||||
pushExport("__suaveplan_routes", 0x00, 3);
|
||||
pushExport("__suaveplan_start", 0x00, 4);
|
||||
pushExport("__suaveplan_dispose", 0x00, 5);
|
||||
const exportSection = section(0x07, vec(exportEntries));
|
||||
|
||||
// --- Code section ---------------------------------------------------
|
||||
const codeSection = section(
|
||||
0x0a,
|
||||
vec([
|
||||
buildAllocBody(options.allocAlwaysZero === true),
|
||||
buildFreeBody(),
|
||||
buildHandleRequestBody({
|
||||
trap: options.handlerTraps === true,
|
||||
emptyResponse: options.handlerEmptyResponse === true,
|
||||
zeroLengthPtr: options.handlerZeroLengthResponse,
|
||||
responseBytesLen: responseBytes.length,
|
||||
}),
|
||||
buildRoutesBody({
|
||||
trap: options.routesTraps === true,
|
||||
empty: options.routesEmpty === true,
|
||||
routesLen: routesBytes.length,
|
||||
}),
|
||||
buildLifecycleBody(options.startTraps === true),
|
||||
buildLifecycleBody(options.disposeTraps === true),
|
||||
buildMemcpyBody(),
|
||||
])
|
||||
);
|
||||
|
||||
// --- Data section ---------------------------------------------------
|
||||
const dataSection = section(
|
||||
0x0b,
|
||||
vec([
|
||||
buildDataSegment(ROUTES_OFFSET, routesBytes),
|
||||
buildDataSegment(RESPONSE_OFFSET, responseBytes),
|
||||
])
|
||||
);
|
||||
|
||||
return concat(
|
||||
MAGIC,
|
||||
VERSION,
|
||||
typeSection,
|
||||
funcSection,
|
||||
memorySection,
|
||||
globalSection,
|
||||
exportSection,
|
||||
codeSection,
|
||||
dataSection
|
||||
);
|
||||
}
|
||||
|
||||
function buildDataSegment(offset: number, payload: Uint8Array): Uint8Array {
|
||||
// Active data segment, memory index 0, init expr (i32.const offset, end), bytes.
|
||||
return concat(
|
||||
encodeU32(0), // memory index
|
||||
new Uint8Array([OP_I32_CONST]),
|
||||
encodeI32(offset),
|
||||
new Uint8Array([OP_END]),
|
||||
encodeU32(payload.length),
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
function buildAllocBody(zero: boolean): Uint8Array {
|
||||
// (param i32) -> i32
|
||||
// local 0 = len
|
||||
if (zero) {
|
||||
return funcBody([], concat(opI32Const(0)));
|
||||
}
|
||||
// ptr = global; global = ptr + len; return ptr
|
||||
// locals: ptr (local 1)
|
||||
const body = concat(
|
||||
opGlobalGet(0),
|
||||
opLocalSet(1),
|
||||
opLocalGet(1),
|
||||
opLocalGet(0),
|
||||
new Uint8Array([OP_I32_ADD]),
|
||||
opGlobalSet(0),
|
||||
opLocalGet(1)
|
||||
);
|
||||
return funcBody([{ count: 1, type: VT_I32 }], body);
|
||||
}
|
||||
|
||||
function buildFreeBody(): Uint8Array {
|
||||
// (param i32) -> ()
|
||||
// No-op — emit just end (handled by funcBody).
|
||||
return funcBody([], new Uint8Array());
|
||||
}
|
||||
|
||||
function buildHandleRequestBody(options: {
|
||||
readonly trap: boolean;
|
||||
readonly emptyResponse: boolean;
|
||||
readonly zeroLengthPtr: number | undefined;
|
||||
readonly responseBytesLen: number;
|
||||
}): Uint8Array {
|
||||
// (param i32 i32) -> i64
|
||||
// local 0 = reqPtr, local 1 = reqLen
|
||||
if (options.trap) {
|
||||
return funcBody([], new Uint8Array([OP_UNREACHABLE]));
|
||||
}
|
||||
if (options.emptyResponse) {
|
||||
return funcBody(
|
||||
[],
|
||||
concat(new Uint8Array([OP_I64_CONST]), encodeI32(0))
|
||||
);
|
||||
}
|
||||
if (options.zeroLengthPtr !== undefined) {
|
||||
const packed = BigInt(options.zeroLengthPtr) << 32n;
|
||||
return funcBody(
|
||||
[],
|
||||
concat(new Uint8Array([OP_I64_CONST]), encodeI64(packed))
|
||||
);
|
||||
}
|
||||
// 1. Clamp reqLen to WITNESS_CAPACITY.
|
||||
// 2. Call memcpy_internal(WITNESS, reqPtr, reqLen).
|
||||
// 3. Return packed (RESPONSE_OFFSET << 32) | responseBytesLen as i64.
|
||||
const clipBlock = concat(
|
||||
new Uint8Array([OP_BLOCK, BLOCK_TYPE_EMPTY]),
|
||||
opLocalGet(1),
|
||||
opI32Const(WITNESS_CAPACITY),
|
||||
new Uint8Array([OP_I32_LT_U]),
|
||||
opBrIf(0),
|
||||
opI32Const(WITNESS_CAPACITY),
|
||||
opLocalSet(1),
|
||||
new Uint8Array([OP_END])
|
||||
);
|
||||
const callMemcpy = concat(
|
||||
opI32Const(WITNESS_OFFSET), // dst
|
||||
opLocalGet(0), // src = reqPtr
|
||||
opLocalGet(1), // len (possibly clamped)
|
||||
opCall(6) // memcpy_internal
|
||||
);
|
||||
const packed =
|
||||
(BigInt(RESPONSE_OFFSET) << 32n) | BigInt(options.responseBytesLen);
|
||||
const returnPacked = concat(
|
||||
new Uint8Array([OP_I64_CONST]),
|
||||
encodeI64(packed)
|
||||
);
|
||||
return funcBody([], concat(clipBlock, callMemcpy, returnPacked));
|
||||
}
|
||||
|
||||
function buildRoutesBody(options: {
|
||||
readonly trap: boolean;
|
||||
readonly empty: boolean;
|
||||
readonly routesLen: number;
|
||||
}): Uint8Array {
|
||||
// () -> i64
|
||||
if (options.trap) {
|
||||
return funcBody([], new Uint8Array([OP_UNREACHABLE]));
|
||||
}
|
||||
if (options.empty) {
|
||||
return funcBody(
|
||||
[],
|
||||
concat(new Uint8Array([OP_I64_CONST]), encodeI64(0n))
|
||||
);
|
||||
}
|
||||
const packed = (BigInt(ROUTES_OFFSET) << 32n) | BigInt(options.routesLen);
|
||||
return funcBody(
|
||||
[],
|
||||
concat(new Uint8Array([OP_I64_CONST]), encodeI64(packed))
|
||||
);
|
||||
}
|
||||
|
||||
function buildLifecycleBody(trap: boolean): Uint8Array {
|
||||
if (trap) {
|
||||
return funcBody([], new Uint8Array([OP_UNREACHABLE]));
|
||||
}
|
||||
return funcBody([], new Uint8Array());
|
||||
}
|
||||
|
||||
function buildMemcpyBody(): Uint8Array {
|
||||
// (param dst i32, src i32, len i32) -> ()
|
||||
// local 3 = i
|
||||
// block
|
||||
// loop
|
||||
// local.get i
|
||||
// local.get len
|
||||
// i32.lt_u
|
||||
// i32.eqz
|
||||
// br_if 1
|
||||
// ;; memory[dst+i] = memory[src+i]
|
||||
// local.get dst
|
||||
// local.get i
|
||||
// i32.add
|
||||
// local.get src
|
||||
// local.get i
|
||||
// i32.add
|
||||
// i32.load8_u
|
||||
// i32.store8
|
||||
// local.get i
|
||||
// i32.const 1
|
||||
// i32.add
|
||||
// local.set i
|
||||
// br 0
|
||||
// end
|
||||
// end
|
||||
const body = concat(
|
||||
opI32Const(0),
|
||||
opLocalSet(3),
|
||||
new Uint8Array([OP_BLOCK, BLOCK_TYPE_EMPTY]),
|
||||
new Uint8Array([OP_LOOP, BLOCK_TYPE_EMPTY]),
|
||||
opLocalGet(3),
|
||||
opLocalGet(2),
|
||||
new Uint8Array([OP_I32_LT_U]),
|
||||
new Uint8Array([OP_I32_EQZ]),
|
||||
opBrIf(1),
|
||||
opLocalGet(0),
|
||||
opLocalGet(3),
|
||||
new Uint8Array([OP_I32_ADD]),
|
||||
opLocalGet(1),
|
||||
opLocalGet(3),
|
||||
new Uint8Array([OP_I32_ADD]),
|
||||
opI32Load8U(),
|
||||
opI32Store8(),
|
||||
opLocalGet(3),
|
||||
opI32Const(1),
|
||||
new Uint8Array([OP_I32_ADD]),
|
||||
opLocalSet(3),
|
||||
opBr(0),
|
||||
new Uint8Array([OP_END]),
|
||||
new Uint8Array([OP_END])
|
||||
);
|
||||
return funcBody([{ count: 1, type: VT_I32 }], body);
|
||||
}
|
||||
|
||||
/** Encode a signed LEB128 i64 literal. */
|
||||
function encodeI64(value: bigint): Uint8Array {
|
||||
const out: number[] = [];
|
||||
let v = value;
|
||||
while (true) {
|
||||
const byte = Number(v & 0x7fn);
|
||||
v >>= 7n;
|
||||
const signBit = (byte & 0x40) !== 0;
|
||||
if ((v === 0n && !signBit) || (v === -1n && signBit)) {
|
||||
out.push(byte);
|
||||
break;
|
||||
}
|
||||
out.push(byte | 0x80);
|
||||
}
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
Reference in New Issue
Block a user