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