summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/encoding/streams/realms.window.js
blob: ca9ce21abc22eb261a0818a866ce1521f951bb60 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
'use strict';

// Test that objects created by the TextEncoderStream and TextDecoderStream APIs
// are created in the correct realm. The tests work by creating an iframe for
// each realm and then posting Javascript to them to be evaluated. Inputs and
// outputs are passed around via global variables in each realm's scope.

// Async setup is required before creating any tests, so require done() to be
// called.
setup({explicit_done: true});

function createRealm() {
  let iframe = document.createElement('iframe');
  const scriptEndTag = '<' + '/script>';
  iframe.srcdoc = `<!doctype html>
<script>
onmessage = event => {
  if (event.source !== window.parent) {
    throw new Error('unexpected message with source ' + event.source);
  }
  eval(event.data);
};
${scriptEndTag}`;
  iframe.style.display = 'none';
  document.body.appendChild(iframe);
  let realmPromiseResolve;
  const realmPromise = new Promise(resolve => {
    realmPromiseResolve = resolve;
  });
  iframe.onload = () => {
    realmPromiseResolve(iframe.contentWindow);
  };
  return realmPromise;
}

async function createRealms() {
  // All realms are visible on the global object so they can access each other.

  // The realm that the constructor function comes from.
  window.constructorRealm = await createRealm();

  // The realm in which the constructor object is called.
  window.constructedRealm = await createRealm();

  // The realm in which reading happens.
  window.readRealm = await createRealm();

  // The realm in which writing happens.
  window.writeRealm = await createRealm();

  // The realm that provides the definitions of Readable and Writable methods.
  window.methodRealm = await createRealm();

  await evalInRealmAndWait(methodRealm, `
  window.ReadableStreamDefaultReader =
      new ReadableStream().getReader().constructor;
  window.WritableStreamDefaultWriter =
      new WritableStream().getWriter().constructor;
`);
  window.readMethod = methodRealm.ReadableStreamDefaultReader.prototype.read;
  window.writeMethod = methodRealm.WritableStreamDefaultWriter.prototype.write;
}

// In order for values to be visible between realms, they need to be
// global. To prevent interference between tests, variable names are generated
// automatically.
const id = (() => {
  let nextId = 0;
  return () => {
    return `realmsId${nextId++}`;
  };
})();

// Eval string "code" in the content of realm "realm". Evaluation happens
// asynchronously, meaning it hasn't happened when the function returns.
function evalInRealm(realm, code) {
  realm.postMessage(code, window.origin);
}

// Same as evalInRealm() but returns a Promise which will resolve when the
// function has actually.
async function evalInRealmAndWait(realm, code) {
  const resolve = id();
  const waitOn = new Promise(r => {
    realm[resolve] = r;
  });
  evalInRealm(realm, code);
  evalInRealm(realm, `${resolve}();`);
  await waitOn;
}

// The same as evalInRealmAndWait but returns the result of evaluating "code" as
// an expression.
async function evalInRealmAndReturn(realm, code) {
  const myId = id();
  await evalInRealmAndWait(realm, `window.${myId} = ${code};`);
  return realm[myId];
}

// Constructs an object in constructedRealm and copies it into readRealm and
// writeRealm. Returns the id that can be used to access the object in those
// realms. |what| can contain constructor arguments.
async function constructAndStore(what) {
  const objId = id();
  // Call |constructorRealm|'s constructor from inside |constructedRealm|.
  writeRealm[objId] = await evalInRealmAndReturn(
      constructedRealm, `new parent.constructorRealm.${what}`);
  readRealm[objId] = writeRealm[objId];
  return objId;
}

// Calls read() on the readable side of the TransformStream stored in
// readRealm[objId]. Locks the readable side as a side-effect.
function readInReadRealm(objId) {
  return evalInRealmAndReturn(readRealm, `
parent.readMethod.call(window.${objId}.readable.getReader())`);
}

// Calls write() on the writable side of the TransformStream stored in
// writeRealm[objId], passing |value|. Locks the writable side as a
// side-effect.
function writeInWriteRealm(objId, value) {
  const valueId = id();
  writeRealm[valueId] = value;
  return evalInRealmAndReturn(writeRealm, `
parent.writeMethod.call(window.${objId}.writable.getWriter(),
                        window.${valueId})`);
}

window.onload = () => {
  createRealms().then(() => {
    runGenericTests('TextEncoderStream');
    runTextEncoderStreamTests();
    runGenericTests('TextDecoderStream');
    runTextDecoderStreamTests();
    done();
  });
};

function runGenericTests(classname) {
  promise_test(async () => {
    const obj = await evalInRealmAndReturn(
        constructedRealm, `new parent.constructorRealm.${classname}()`);
    assert_equals(obj.constructor, constructorRealm[classname],
                  'obj should be in constructor realm');
  }, `a ${classname} object should be associated with the realm the ` +
     'constructor came from');

  promise_test(async () => {
    const objId = await constructAndStore(classname);
    const readableGetterId = id();
    readRealm[readableGetterId] = Object.getOwnPropertyDescriptor(
        methodRealm[classname].prototype, 'readable').get;
    const writableGetterId = id();
    writeRealm[writableGetterId] = Object.getOwnPropertyDescriptor(
        methodRealm[classname].prototype, 'writable').get;
    const readable = await evalInRealmAndReturn(
        readRealm, `${readableGetterId}.call(${objId})`);
    const writable = await evalInRealmAndReturn(
        writeRealm, `${writableGetterId}.call(${objId})`);
    assert_equals(readable.constructor, constructorRealm.ReadableStream,
                  'readable should be in constructor realm');
    assert_equals(writable.constructor, constructorRealm.WritableStream,
                  'writable should be in constructor realm');
  }, `${classname}'s readable and writable attributes should come from the ` +
     'same realm as the constructor definition');
}

function runTextEncoderStreamTests() {
  promise_test(async () => {
    const objId = await constructAndStore('TextEncoderStream');
    const writePromise = writeInWriteRealm(objId, 'A');
    const result = await readInReadRealm(objId);
    await writePromise;
    assert_equals(result.constructor, constructorRealm.Object,
                  'result should be in constructor realm');
    assert_equals(result.value.constructor, constructorRealm.Uint8Array,
                  'chunk should be in constructor realm');
  }, 'the output chunks when read is called after write should come from the ' +
     'same realm as the constructor of TextEncoderStream');

  promise_test(async () => {
    const objId = await constructAndStore('TextEncoderStream');
    const chunkPromise = readInReadRealm(objId);
    writeInWriteRealm(objId, 'A');
    // Now the read() should resolve.
    const result = await chunkPromise;
    assert_equals(result.constructor, constructorRealm.Object,
                  'result should be in constructor realm');
    assert_equals(result.value.constructor, constructorRealm.Uint8Array,
                  'chunk should be in constructor realm');
  }, 'the output chunks when write is called with a pending read should come ' +
     'from the same realm as the constructor of TextEncoderStream');

  // There is not absolute consensus regarding what realm exceptions should be
  // created in. Implementations may vary. The expectations in exception-related
  // tests may change in future once consensus is reached.
  promise_test(async t => {
    const objId = await constructAndStore('TextEncoderStream');
    // Read first to relieve backpressure.
    const readPromise = readInReadRealm(objId);

    await promise_rejects_js(t, constructorRealm.TypeError,
                             writeInWriteRealm(objId, {
                               toString() { return {}; }
                             }),
                             'write TypeError should come from constructor realm');

    return promise_rejects_js(t, constructorRealm.TypeError, readPromise,
                              'read TypeError should come from constructor realm');
  }, 'TypeError for unconvertable chunk should come from constructor realm ' +
     'of TextEncoderStream');
}

function runTextDecoderStreamTests() {
  promise_test(async () => {
    const objId = await constructAndStore('TextDecoderStream');
    const writePromise = writeInWriteRealm(objId, new Uint8Array([65]));
    const result = await readInReadRealm(objId);
    await writePromise;
    assert_equals(result.constructor, constructorRealm.Object,
                  'result should be in constructor realm');
    // A string is not an object, so doesn't have an associated realm. Accessing
    // string properties will create a transient object wrapper belonging to the
    // current realm. So checking the realm of result.value is not useful.
  }, 'the result object when read is called after write should come from the ' +
     'same realm as the constructor of TextDecoderStream');

  promise_test(async () => {
    const objId = await constructAndStore('TextDecoderStream');
    const chunkPromise = readInReadRealm(objId);
    writeInWriteRealm(objId, new Uint8Array([65]));
    // Now the read() should resolve.
    const result = await chunkPromise;
    assert_equals(result.constructor, constructorRealm.Object,
                  'result should be in constructor realm');
    // A string is not an object, so doesn't have an associated realm. Accessing
    // string properties will create a transient object wrapper belonging to the
    // current realm. So checking the realm of result.value is not useful.
  }, 'the result object when write is called with a pending ' +
     'read should come from the same realm as the constructor of TextDecoderStream');

  promise_test(async t => {
    const objId = await constructAndStore('TextDecoderStream');
    // Read first to relieve backpressure.
    const readPromise = readInReadRealm(objId);
    await promise_rejects_js(
      t, constructorRealm.TypeError,
      writeInWriteRealm(objId, {}),
      'write TypeError should come from constructor realm'
    );

    return promise_rejects_js(
      t, constructorRealm.TypeError, readPromise,
      'read TypeError should come from constructor realm'
    );
  }, 'TypeError for chunk with the wrong type should come from constructor ' +
     'realm of TextDecoderStream');

  promise_test(async t => {
    const objId =
          await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`);
    // Read first to relieve backpressure.
    const readPromise = readInReadRealm(objId);

    await promise_rejects_js(
      t, constructorRealm.TypeError,
      writeInWriteRealm(objId, new Uint8Array([0xff])),
      'write TypeError should come from constructor realm'
    );

    return promise_rejects_js(
      t, constructorRealm.TypeError, readPromise,
      'read TypeError should come from constructor realm'
    );
  }, 'TypeError for invalid chunk should come from constructor realm ' +
     'of TextDecoderStream');

  promise_test(async t => {
    const objId =
          await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`);
    // Read first to relieve backpressure.
    readInReadRealm(objId);
    // Write an unfinished sequence of bytes.
    const incompleteBytesId = id();
    writeRealm[incompleteBytesId] = new Uint8Array([0xf0]);

    return promise_rejects_js(
      t, constructorRealm.TypeError,
      // Can't use writeInWriteRealm() here because it doesn't make it possible
      // to reuse the writer.
      evalInRealmAndReturn(writeRealm, `
(() => {
  const writer = window.${objId}.writable.getWriter();
  parent.writeMethod.call(writer, window.${incompleteBytesId});
  return parent.methodRealm.WritableStreamDefaultWriter.prototype
    .close.call(writer);
})();
`),
      'close TypeError should come from constructor realm'
    );
  }, 'TypeError for incomplete input should come from constructor realm ' +
     'of TextDecoderStream');
}