| '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()); |