• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3// Test that objects created by the TextEncoderStream and TextDecoderStream APIs
4// are created in the correct realm. The tests work by creating an iframe for
5// each realm and then posting Javascript to them to be evaluated. Inputs and
6// outputs are passed around via global variables in each realm's scope.
7
8// Async setup is required before creating any tests, so require done() to be
9// called.
10setup({explicit_done: true});
11
12function createRealm() {
13  let iframe = document.createElement('iframe');
14  const scriptEndTag = '<' + '/script>';
15  iframe.srcdoc = `<!doctype html>
16<script>
17onmessage = event => {
18  if (event.source !== window.parent) {
19    throw new Error('unexpected message with source ' + event.source);
20  }
21  eval(event.data);
22};
23${scriptEndTag}`;
24  iframe.style.display = 'none';
25  document.body.appendChild(iframe);
26  let realmPromiseResolve;
27  const realmPromise = new Promise(resolve => {
28    realmPromiseResolve = resolve;
29  });
30  iframe.onload = () => {
31    realmPromiseResolve(iframe.contentWindow);
32  };
33  return realmPromise;
34}
35
36async function createRealms() {
37  // All realms are visible on the global object so they can access each other.
38
39  // The realm that the constructor function comes from.
40  window.constructorRealm = await createRealm();
41
42  // The realm in which the constructor object is called.
43  window.constructedRealm = await createRealm();
44
45  // The realm in which reading happens.
46  window.readRealm = await createRealm();
47
48  // The realm in which writing happens.
49  window.writeRealm = await createRealm();
50
51  // The realm that provides the definitions of Readable and Writable methods.
52  window.methodRealm = await createRealm();
53
54  await evalInRealmAndWait(methodRealm, `
55  window.ReadableStreamDefaultReader =
56      new ReadableStream().getReader().constructor;
57  window.WritableStreamDefaultWriter =
58      new WritableStream().getWriter().constructor;
59`);
60  window.readMethod = methodRealm.ReadableStreamDefaultReader.prototype.read;
61  window.writeMethod = methodRealm.WritableStreamDefaultWriter.prototype.write;
62}
63
64// In order for values to be visible between realms, they need to be
65// global. To prevent interference between tests, variable names are generated
66// automatically.
67const id = (() => {
68  let nextId = 0;
69  return () => {
70    return `realmsId${nextId++}`;
71  };
72})();
73
74// Eval string "code" in the content of realm "realm". Evaluation happens
75// asynchronously, meaning it hasn't happened when the function returns.
76function evalInRealm(realm, code) {
77  realm.postMessage(code, window.origin);
78}
79
80// Same as evalInRealm() but returns a Promise which will resolve when the
81// function has actually.
82async function evalInRealmAndWait(realm, code) {
83  const resolve = id();
84  const waitOn = new Promise(r => {
85    realm[resolve] = r;
86  });
87  evalInRealm(realm, code);
88  evalInRealm(realm, `${resolve}();`);
89  await waitOn;
90}
91
92// The same as evalInRealmAndWait but returns the result of evaluating "code" as
93// an expression.
94async function evalInRealmAndReturn(realm, code) {
95  const myId = id();
96  await evalInRealmAndWait(realm, `window.${myId} = ${code};`);
97  return realm[myId];
98}
99
100// Constructs an object in constructedRealm and copies it into readRealm and
101// writeRealm. Returns the id that can be used to access the object in those
102// realms. |what| can contain constructor arguments.
103async function constructAndStore(what) {
104  const objId = id();
105  // Call |constructorRealm|'s constructor from inside |constructedRealm|.
106  writeRealm[objId] = await evalInRealmAndReturn(
107      constructedRealm, `new parent.constructorRealm.${what}`);
108  readRealm[objId] = writeRealm[objId];
109  return objId;
110}
111
112// Calls read() on the readable side of the TransformStream stored in
113// readRealm[objId]. Locks the readable side as a side-effect.
114function readInReadRealm(objId) {
115  return evalInRealmAndReturn(readRealm, `
116parent.readMethod.call(window.${objId}.readable.getReader())`);
117}
118
119// Calls write() on the writable side of the TransformStream stored in
120// writeRealm[objId], passing |value|. Locks the writable side as a
121// side-effect.
122function writeInWriteRealm(objId, value) {
123  const valueId = id();
124  writeRealm[valueId] = value;
125  return evalInRealmAndReturn(writeRealm, `
126parent.writeMethod.call(window.${objId}.writable.getWriter(),
127                        window.${valueId})`);
128}
129
130window.onload = () => {
131  createRealms().then(() => {
132    runGenericTests('TextEncoderStream');
133    runTextEncoderStreamTests();
134    runGenericTests('TextDecoderStream');
135    runTextDecoderStreamTests();
136    done();
137  });
138};
139
140function runGenericTests(classname) {
141  promise_test(async () => {
142    const obj = await evalInRealmAndReturn(
143        constructedRealm, `new parent.constructorRealm.${classname}()`);
144    assert_equals(obj.constructor, constructorRealm[classname],
145                  'obj should be in constructor realm');
146  }, `a ${classname} object should be associated with the realm the ` +
147     'constructor came from');
148
149  promise_test(async () => {
150    const objId = await constructAndStore(classname);
151    const readableGetterId = id();
152    readRealm[readableGetterId] = Object.getOwnPropertyDescriptor(
153        methodRealm[classname].prototype, 'readable').get;
154    const writableGetterId = id();
155    writeRealm[writableGetterId] = Object.getOwnPropertyDescriptor(
156        methodRealm[classname].prototype, 'writable').get;
157    const readable = await evalInRealmAndReturn(
158        readRealm, `${readableGetterId}.call(${objId})`);
159    const writable = await evalInRealmAndReturn(
160        writeRealm, `${writableGetterId}.call(${objId})`);
161    assert_equals(readable.constructor, constructorRealm.ReadableStream,
162                  'readable should be in constructor realm');
163    assert_equals(writable.constructor, constructorRealm.WritableStream,
164                  'writable should be in constructor realm');
165  }, `${classname}'s readable and writable attributes should come from the ` +
166     'same realm as the constructor definition');
167}
168
169function runTextEncoderStreamTests() {
170  promise_test(async () => {
171    const objId = await constructAndStore('TextEncoderStream');
172    const writePromise = writeInWriteRealm(objId, 'A');
173    const result = await readInReadRealm(objId);
174    await writePromise;
175    assert_equals(result.constructor, constructorRealm.Object,
176                  'result should be in constructor realm');
177    assert_equals(result.value.constructor, constructorRealm.Uint8Array,
178                  'chunk should be in constructor realm');
179  }, 'the output chunks when read is called after write should come from the ' +
180     'same realm as the constructor of TextEncoderStream');
181
182  promise_test(async () => {
183    const objId = await constructAndStore('TextEncoderStream');
184    const chunkPromise = readInReadRealm(objId);
185    writeInWriteRealm(objId, 'A');
186    // Now the read() should resolve.
187    const result = await chunkPromise;
188    assert_equals(result.constructor, constructorRealm.Object,
189                  'result should be in constructor realm');
190    assert_equals(result.value.constructor, constructorRealm.Uint8Array,
191                  'chunk should be in constructor realm');
192  }, 'the output chunks when write is called with a pending read should come ' +
193     'from the same realm as the constructor of TextEncoderStream');
194
195  // There is not absolute consensus regarding what realm exceptions should be
196  // created in. Implementations may vary. The expectations in exception-related
197  // tests may change in future once consensus is reached.
198  promise_test(async t => {
199    const objId = await constructAndStore('TextEncoderStream');
200    // Read first to relieve backpressure.
201    const readPromise = readInReadRealm(objId);
202    // promise_rejects() does not permit directly inspecting the rejection, so
203    // it's necessary to write it out long-hand.
204    let writeSucceeded = false;
205    try {
206      // Write an invalid chunk.
207      await writeInWriteRealm(objId, {
208        toString() { return {}; }
209      });
210      writeSucceeded = true;
211    } catch (err) {
212      assert_equals(err.constructor, constructorRealm.TypeError,
213                    'write TypeError should come from constructor realm');
214    }
215    assert_false(writeSucceeded, 'write should fail');
216
217    let readSucceeded = false;
218    try {
219      await readPromise;
220      readSucceeded = true;
221    } catch (err) {
222      assert_equals(err.constructor, constructorRealm.TypeError,
223                    'read TypeError should come from constructor realm');
224    }
225
226    assert_false(readSucceeded, 'read should fail');
227  }, 'TypeError for unconvertable chunk should come from constructor realm ' +
228     'of TextEncoderStream');
229}
230
231function runTextDecoderStreamTests() {
232  promise_test(async () => {
233    const objId = await constructAndStore('TextDecoderStream');
234    const writePromise = writeInWriteRealm(objId, new Uint8Array([65]));
235    const result = await readInReadRealm(objId);
236    await writePromise;
237    assert_equals(result.constructor, constructorRealm.Object,
238                  'result should be in constructor realm');
239    // A string is not an object, so doesn't have an associated realm. Accessing
240    // string properties will create a transient object wrapper belonging to the
241    // current realm. So checking the realm of result.value is not useful.
242  }, 'the result object when read is called after write should come from the ' +
243     'same realm as the constructor of TextDecoderStream');
244
245  promise_test(async () => {
246    const objId = await constructAndStore('TextDecoderStream');
247    const chunkPromise = readInReadRealm(objId);
248    writeInWriteRealm(objId, new Uint8Array([65]));
249    // Now the read() should resolve.
250    const result = await chunkPromise;
251    assert_equals(result.constructor, constructorRealm.Object,
252                  'result should be in constructor realm');
253    // A string is not an object, so doesn't have an associated realm. Accessing
254    // string properties will create a transient object wrapper belonging to the
255    // current realm. So checking the realm of result.value is not useful.
256  }, 'the result object when write is called with a pending ' +
257     'read should come from the same realm as the constructor of TextDecoderStream');
258
259  promise_test(async t => {
260    const objId = await constructAndStore('TextDecoderStream');
261    // Read first to relieve backpressure.
262    const readPromise = readInReadRealm(objId);
263    // promise_rejects() does not permit directly inspecting the rejection, so
264    // it's necessary to write it out long-hand.
265    let writeSucceeded = false;
266    try {
267      // Write an invalid chunk.
268      await writeInWriteRealm(objId, {});
269      writeSucceeded = true;
270    } catch (err) {
271      assert_equals(err.constructor, constructorRealm.TypeError,
272                    'write TypeError should come from constructor realm');
273    }
274    assert_false(writeSucceeded, 'write should fail');
275
276    let readSucceeded = false;
277    try {
278      await readPromise;
279      readSucceeded = true;
280    } catch (err) {
281      assert_equals(err.constructor, constructorRealm.TypeError,
282                    'read TypeError should come from constructor realm');
283    }
284    assert_false(readSucceeded, 'read should fail');
285  }, 'TypeError for chunk with the wrong type should come from constructor ' +
286     'realm of TextDecoderStream');
287
288  promise_test(async t => {
289    const objId =
290          await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`);
291    // Read first to relieve backpressure.
292    const readPromise = readInReadRealm(objId);
293    // promise_rejects() does not permit directly inspecting the rejection, so
294    // it's necessary to write it out long-hand.
295    let writeSucceeded = false;
296    try {
297      await writeInWriteRealm(objId, new Uint8Array([0xff]));
298      writeSucceeded = true;
299    } catch (err) {
300      assert_equals(err.constructor, constructorRealm.TypeError,
301                    'write TypeError should come from constructor realm');
302    }
303    assert_false(writeSucceeded, 'write should fail');
304
305    let readSucceeded = false;
306    try {
307      await readPromise;
308      readSucceeded = true;
309    } catch (err) {
310      assert_equals(err.constructor, constructorRealm.TypeError,
311                    'read TypeError should come from constructor realm');
312    }
313    assert_false(readSucceeded, 'read should fail');
314  }, 'TypeError for invalid chunk should come from constructor realm ' +
315     'of TextDecoderStream');
316
317  promise_test(async t => {
318    const objId =
319          await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`);
320    // Read first to relieve backpressure.
321    readInReadRealm(objId);
322    // Write an unfinished sequence of bytes.
323    const incompleteBytesId = id();
324    writeRealm[incompleteBytesId] = new Uint8Array([0xf0]);
325    // promise_rejects() does not permit directly inspecting the rejection, so
326    // it's necessary to write it out long-hand.
327    let closeSucceeded = false;
328    try {
329      // Can't use writeInWriteRealm() here because it doesn't make it possible
330      // to reuse the writer.
331      await evalInRealmAndReturn(writeRealm, `
332(() => {
333  const writer = window.${objId}.writable.getWriter();
334  parent.writeMethod.call(writer, window.${incompleteBytesId});
335  return parent.methodRealm.WritableStreamDefaultWriter.prototype
336    .close.call(writer);
337})();
338`);
339      closeSucceeded = true;
340    } catch (err) {
341      assert_equals(err.constructor, constructorRealm.TypeError,
342                    'close TypeError should come from constructor realm');
343    }
344    assert_false(closeSucceeded, 'close should fail');
345  }, 'TypeError for incomplete input should come from constructor realm ' +
346     'of TextDecoderStream');
347}
348