'use strict'; const common = require('../common'); const fixtures = require('../common/fixtures'); const assert = require('assert'); const events = require('events'); const fs = require('fs/promises'); const { createServer } = require('http'); assert.strictEqual(typeof WebAssembly.compileStreaming, 'function'); assert.strictEqual(typeof WebAssembly.instantiateStreaming, 'function'); const simpleWasmBytes = fixtures.readSync('simple.wasm'); // Sets up an HTTP server with the given response handler and calls fetch() to // obtain a Response from the newly created server. async function testRequest(handler) { const server = createServer((_, res) => handler(res)).unref().listen(0); await events.once(server, 'listening'); const { port } = server.address(); return fetch(`http://127.0.0.1:${port}/foo.wasm`); } // Runs the given function both with the promise itself and as a continuation // of the promise. We use this to test that the API accepts not just a Response // but also a Promise that resolves to a Response. function withPromiseAndResolved(makePromise, consume) { return Promise.all([ consume(makePromise()), makePromise().then(consume), ]); } // The makeResponsePromise function must return a Promise that resolves to a // Response. The checkResult function receives the Promise returned by // WebAssembly.compileStreaming and must return a Promise itself. function testCompileStreaming(makeResponsePromise, checkResult) { return withPromiseAndResolved( common.mustCall(makeResponsePromise, 2), common.mustCall((response) => { return checkResult(WebAssembly.compileStreaming(response)); }, 2) ); } function testCompileStreamingSuccess(makeResponsePromise) { return testCompileStreaming(makeResponsePromise, async (modPromise) => { const mod = await modPromise; assert.strictEqual(mod.constructor, WebAssembly.Module); }); } function testCompileStreamingRejection(makeResponsePromise, rejection) { return testCompileStreaming(makeResponsePromise, (modPromise) => { assert.strictEqual(modPromise.constructor, Promise); return assert.rejects(modPromise, rejection); }); } function testCompileStreamingSuccessUsingFetch(responseCallback) { return testCompileStreamingSuccess(() => testRequest(responseCallback)); } function testCompileStreamingRejectionUsingFetch(responseCallback, rejection) { return testCompileStreamingRejection(() => testRequest(responseCallback), rejection); } (async () => { // A non-Response should cause a TypeError. for (const invalid of [undefined, null, 0, true, 'foo', {}, [], Symbol()]) { await withPromiseAndResolved(() => Promise.resolve(invalid), (arg) => { return assert.rejects(() => WebAssembly.compileStreaming(arg), { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE', message: /^The "source" argument .*$/ }); }); } // When given a Promise, any rejection should be propagated as-is. { const err = new RangeError('foo'); await assert.rejects(() => { return WebAssembly.compileStreaming(Promise.reject(err)); }, (actualError) => actualError === err); } // A valid WebAssembly file with the correct MIME type. await testCompileStreamingSuccessUsingFetch((res) => { res.setHeader('Content-Type', 'application/wasm'); res.end(simpleWasmBytes); }); // The same valid WebAssembly file with the same MIME type, but using a // Response whose body is a Buffer instead of calling fetch(). await testCompileStreamingSuccess(() => { return Promise.resolve(new Response(simpleWasmBytes, { status: 200, headers: { 'Content-Type': 'application/wasm' } })); }); // The same valid WebAssembly file with the same MIME type, but using a // Response whose body is a ReadableStream instead of calling fetch(). await testCompileStreamingSuccess(async () => { const handle = await fs.open(fixtures.path('simple.wasm')); const stream = handle.readableWebStream(); return Promise.resolve(new Response(stream, { status: 200, headers: { 'Content-Type': 'application/wasm' } })); }); // A larger valid WebAssembly file with the correct MIME type that causes the // client to pass it to the compiler in many separate chunks. For this, we use // the same WebAssembly file as in the previous test but insert useless custom // sections into the WebAssembly module to increase the file size without // changing the relevant contents. await testCompileStreamingSuccessUsingFetch((res) => { res.setHeader('Content-Type', 'application/wasm'); // Send the WebAssembly magic and version first. res.write(simpleWasmBytes.slice(0, 8), common.mustCall()); // Construct a 4KiB custom section. const customSection = Buffer.concat([ Buffer.from([ 0, // Custom section. 134, 32, // (134 & 0x7f) + 0x80 * 32 = 6 + 4096 bytes in this section. 5, // The length of the following section name. ]), Buffer.from('?'.repeat(5)), // The section name Buffer.from('\0'.repeat(4096)), // The actual section data ]); // Now repeatedly send useless custom sections. These have no use for the // WebAssembly compiler but they are syntactically valid. The client has to // keep reading the stream until the very end to obtain the relevant // sections within the module. This adds up to a few hundred kibibytes. (function next(i) { if (i < 100) { while (res.write(customSection)); res.once('drain', () => next(i + 1)); } else { // End the response body with the actual module contents. res.end(simpleWasmBytes.slice(8)); } })(0); }); // A valid WebAssembly file with an empty parameter in the (otherwise valid) // MIME type. await testCompileStreamingRejectionUsingFetch((res) => { res.setHeader('Content-Type', 'application/wasm;'); res.end(simpleWasmBytes); }, { name: 'TypeError', code: 'ERR_WEBASSEMBLY_RESPONSE', message: 'WebAssembly response has unsupported MIME type ' + "'application/wasm;'" }); // A valid WebAssembly file with an invalid MIME type. await testCompileStreamingRejectionUsingFetch((res) => { res.setHeader('Content-Type', 'application/octet-stream'); res.end(simpleWasmBytes); }, { name: 'TypeError', code: 'ERR_WEBASSEMBLY_RESPONSE', message: 'WebAssembly response has unsupported MIME type ' + "'application/octet-stream'" }); // HTTP status code indiciating an error. await testCompileStreamingRejectionUsingFetch((res) => { res.statusCode = 418; res.setHeader('Content-Type', 'application/wasm'); res.end(simpleWasmBytes); }, { name: 'TypeError', code: 'ERR_WEBASSEMBLY_RESPONSE', message: /^WebAssembly response has status code 418$/ }); // HTTP status code indiciating an error, but using a Response whose body is // a Buffer instead of calling fetch(). await testCompileStreamingSuccess(() => { return Promise.resolve(new Response(simpleWasmBytes, { status: 200, headers: { 'Content-Type': 'application/wasm' } })); }); // Extra bytes after the WebAssembly file. await testCompileStreamingRejectionUsingFetch((res) => { res.setHeader('Content-Type', 'application/wasm'); res.end(Buffer.concat([simpleWasmBytes, Buffer.from('foo')])); }, { name: 'CompileError', message: /^WebAssembly\.compileStreaming\(\): .*$/ }); // Missing bytes at the end of the WebAssembly file. await testCompileStreamingRejectionUsingFetch((res) => { res.setHeader('Content-Type', 'application/wasm'); res.end(simpleWasmBytes.subarray(0, simpleWasmBytes.length - 3)); }, { name: 'CompileError', message: /^WebAssembly\.compileStreaming\(\): .*$/ }); // Incomplete HTTP response body. The TypeError might come as a surprise, but // it originates from within fetch(). await testCompileStreamingRejectionUsingFetch((res) => { res.setHeader('Content-Length', simpleWasmBytes.length); res.setHeader('Content-Type', 'application/wasm'); res.write(simpleWasmBytes.slice(0, 5), common.mustSucceed(() => { res.destroy(); })); }, { name: 'TypeError', message: /terminated/ }); // Test "Developer-Facing Display Conventions" described in the WebAssembly // Web API specification. await testCompileStreaming(() => testRequest((res) => { // Respond with a WebAssembly module that only exports a single function, // which only contains an 'unreachable' instruction. res.setHeader('Content-Type', 'application/wasm'); res.end(fixtures.readSync('crash.wasm')); }), async (modPromise) => { // Call the WebAssembly function and check that the error stack contains the // correct "WebAssembly location" as per the specification. const mod = await modPromise; const instance = new WebAssembly.Instance(mod); assert.throws(() => instance.exports.crash(), (err) => { const stack = err.stack.split(/\n/g); assert.strictEqual(stack[0], 'RuntimeError: unreachable'); assert.match(stack[1], /^\s*at http:\/\/127\.0\.0\.1:\d+\/foo\.wasm:wasm-function\[0\]:0x22$/); return true; }); }); })().then(common.mustCall());