• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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