1'use strict'; 2 3const common = require('../common'); 4const fixtures = require('../common/fixtures'); 5 6const assert = require('assert'); 7const events = require('events'); 8const fs = require('fs/promises'); 9const { createServer } = require('http'); 10 11assert.strictEqual(typeof WebAssembly.compileStreaming, 'function'); 12assert.strictEqual(typeof WebAssembly.instantiateStreaming, 'function'); 13 14const simpleWasmBytes = fixtures.readSync('simple.wasm'); 15 16// Sets up an HTTP server with the given response handler and calls fetch() to 17// obtain a Response from the newly created server. 18async function testRequest(handler) { 19 const server = createServer((_, res) => handler(res)).unref().listen(0); 20 await events.once(server, 'listening'); 21 const { port } = server.address(); 22 return fetch(`http://127.0.0.1:${port}/foo.wasm`); 23} 24 25// Runs the given function both with the promise itself and as a continuation 26// of the promise. We use this to test that the API accepts not just a Response 27// but also a Promise that resolves to a Response. 28function withPromiseAndResolved(makePromise, consume) { 29 return Promise.all([ 30 consume(makePromise()), 31 makePromise().then(consume), 32 ]); 33} 34 35// The makeResponsePromise function must return a Promise that resolves to a 36// Response. The checkResult function receives the Promise returned by 37// WebAssembly.compileStreaming and must return a Promise itself. 38function testCompileStreaming(makeResponsePromise, checkResult) { 39 return withPromiseAndResolved( 40 common.mustCall(makeResponsePromise, 2), 41 common.mustCall((response) => { 42 return checkResult(WebAssembly.compileStreaming(response)); 43 }, 2) 44 ); 45} 46 47function testCompileStreamingSuccess(makeResponsePromise) { 48 return testCompileStreaming(makeResponsePromise, async (modPromise) => { 49 const mod = await modPromise; 50 assert.strictEqual(mod.constructor, WebAssembly.Module); 51 }); 52} 53 54function testCompileStreamingRejection(makeResponsePromise, rejection) { 55 return testCompileStreaming(makeResponsePromise, (modPromise) => { 56 assert.strictEqual(modPromise.constructor, Promise); 57 return assert.rejects(modPromise, rejection); 58 }); 59} 60 61function testCompileStreamingSuccessUsingFetch(responseCallback) { 62 return testCompileStreamingSuccess(() => testRequest(responseCallback)); 63} 64 65function testCompileStreamingRejectionUsingFetch(responseCallback, rejection) { 66 return testCompileStreamingRejection(() => testRequest(responseCallback), 67 rejection); 68} 69 70(async () => { 71 // A non-Response should cause a TypeError. 72 for (const invalid of [undefined, null, 0, true, 'foo', {}, [], Symbol()]) { 73 await withPromiseAndResolved(() => Promise.resolve(invalid), (arg) => { 74 return assert.rejects(() => WebAssembly.compileStreaming(arg), { 75 name: 'TypeError', 76 code: 'ERR_INVALID_ARG_TYPE', 77 message: /^The "source" argument .*$/ 78 }); 79 }); 80 } 81 82 // When given a Promise, any rejection should be propagated as-is. 83 { 84 const err = new RangeError('foo'); 85 await assert.rejects(() => { 86 return WebAssembly.compileStreaming(Promise.reject(err)); 87 }, (actualError) => actualError === err); 88 } 89 90 // A valid WebAssembly file with the correct MIME type. 91 await testCompileStreamingSuccessUsingFetch((res) => { 92 res.setHeader('Content-Type', 'application/wasm'); 93 res.end(simpleWasmBytes); 94 }); 95 96 // The same valid WebAssembly file with the same MIME type, but using a 97 // Response whose body is a Buffer instead of calling fetch(). 98 await testCompileStreamingSuccess(() => { 99 return Promise.resolve(new Response(simpleWasmBytes, { 100 status: 200, 101 headers: { 'Content-Type': 'application/wasm' } 102 })); 103 }); 104 105 // The same valid WebAssembly file with the same MIME type, but using a 106 // Response whose body is a ReadableStream instead of calling fetch(). 107 await testCompileStreamingSuccess(async () => { 108 const handle = await fs.open(fixtures.path('simple.wasm')); 109 const stream = handle.readableWebStream(); 110 return Promise.resolve(new Response(stream, { 111 status: 200, 112 headers: { 'Content-Type': 'application/wasm' } 113 })); 114 }); 115 116 // A larger valid WebAssembly file with the correct MIME type that causes the 117 // client to pass it to the compiler in many separate chunks. For this, we use 118 // the same WebAssembly file as in the previous test but insert useless custom 119 // sections into the WebAssembly module to increase the file size without 120 // changing the relevant contents. 121 await testCompileStreamingSuccessUsingFetch((res) => { 122 res.setHeader('Content-Type', 'application/wasm'); 123 124 // Send the WebAssembly magic and version first. 125 res.write(simpleWasmBytes.slice(0, 8), common.mustCall()); 126 127 // Construct a 4KiB custom section. 128 const customSection = Buffer.concat([ 129 Buffer.from([ 130 0, // Custom section. 131 134, 32, // (134 & 0x7f) + 0x80 * 32 = 6 + 4096 bytes in this section. 132 5, // The length of the following section name. 133 ]), 134 Buffer.from('?'.repeat(5)), // The section name 135 Buffer.from('\0'.repeat(4096)), // The actual section data 136 ]); 137 138 // Now repeatedly send useless custom sections. These have no use for the 139 // WebAssembly compiler but they are syntactically valid. The client has to 140 // keep reading the stream until the very end to obtain the relevant 141 // sections within the module. This adds up to a few hundred kibibytes. 142 (function next(i) { 143 if (i < 100) { 144 while (res.write(customSection)); 145 res.once('drain', () => next(i + 1)); 146 } else { 147 // End the response body with the actual module contents. 148 res.end(simpleWasmBytes.slice(8)); 149 } 150 })(0); 151 }); 152 153 // A valid WebAssembly file with an empty parameter in the (otherwise valid) 154 // MIME type. 155 await testCompileStreamingRejectionUsingFetch((res) => { 156 res.setHeader('Content-Type', 'application/wasm;'); 157 res.end(simpleWasmBytes); 158 }, { 159 name: 'TypeError', 160 code: 'ERR_WEBASSEMBLY_RESPONSE', 161 message: 'WebAssembly response has unsupported MIME type ' + 162 "'application/wasm;'" 163 }); 164 165 // A valid WebAssembly file with an invalid MIME type. 166 await testCompileStreamingRejectionUsingFetch((res) => { 167 res.setHeader('Content-Type', 'application/octet-stream'); 168 res.end(simpleWasmBytes); 169 }, { 170 name: 'TypeError', 171 code: 'ERR_WEBASSEMBLY_RESPONSE', 172 message: 'WebAssembly response has unsupported MIME type ' + 173 "'application/octet-stream'" 174 }); 175 176 // HTTP status code indiciating an error. 177 await testCompileStreamingRejectionUsingFetch((res) => { 178 res.statusCode = 418; 179 res.setHeader('Content-Type', 'application/wasm'); 180 res.end(simpleWasmBytes); 181 }, { 182 name: 'TypeError', 183 code: 'ERR_WEBASSEMBLY_RESPONSE', 184 message: /^WebAssembly response has status code 418$/ 185 }); 186 187 // HTTP status code indiciating an error, but using a Response whose body is 188 // a Buffer instead of calling fetch(). 189 await testCompileStreamingSuccess(() => { 190 return Promise.resolve(new Response(simpleWasmBytes, { 191 status: 200, 192 headers: { 'Content-Type': 'application/wasm' } 193 })); 194 }); 195 196 // Extra bytes after the WebAssembly file. 197 await testCompileStreamingRejectionUsingFetch((res) => { 198 res.setHeader('Content-Type', 'application/wasm'); 199 res.end(Buffer.concat([simpleWasmBytes, Buffer.from('foo')])); 200 }, { 201 name: 'CompileError', 202 message: /^WebAssembly\.compileStreaming\(\): .*$/ 203 }); 204 205 // Missing bytes at the end of the WebAssembly file. 206 await testCompileStreamingRejectionUsingFetch((res) => { 207 res.setHeader('Content-Type', 'application/wasm'); 208 res.end(simpleWasmBytes.subarray(0, simpleWasmBytes.length - 3)); 209 }, { 210 name: 'CompileError', 211 message: /^WebAssembly\.compileStreaming\(\): .*$/ 212 }); 213 214 // Incomplete HTTP response body. The TypeError might come as a surprise, but 215 // it originates from within fetch(). 216 await testCompileStreamingRejectionUsingFetch((res) => { 217 res.setHeader('Content-Length', simpleWasmBytes.length); 218 res.setHeader('Content-Type', 'application/wasm'); 219 res.write(simpleWasmBytes.slice(0, 5), common.mustSucceed(() => { 220 res.destroy(); 221 })); 222 }, { 223 name: 'TypeError', 224 message: /terminated/ 225 }); 226 227 // Test "Developer-Facing Display Conventions" described in the WebAssembly 228 // Web API specification. 229 await testCompileStreaming(() => testRequest((res) => { 230 // Respond with a WebAssembly module that only exports a single function, 231 // which only contains an 'unreachable' instruction. 232 res.setHeader('Content-Type', 'application/wasm'); 233 res.end(fixtures.readSync('crash.wasm')); 234 }), async (modPromise) => { 235 // Call the WebAssembly function and check that the error stack contains the 236 // correct "WebAssembly location" as per the specification. 237 const mod = await modPromise; 238 const instance = new WebAssembly.Instance(mod); 239 assert.throws(() => instance.exports.crash(), (err) => { 240 const stack = err.stack.split(/\n/g); 241 assert.strictEqual(stack[0], 'RuntimeError: unreachable'); 242 assert.match(stack[1], 243 /^\s*at http:\/\/127\.0\.0\.1:\d+\/foo\.wasm:wasm-function\[0\]:0x22$/); 244 return true; 245 }); 246 }); 247})().then(common.mustCall()); 248