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 203 await promise_rejects_js(t, constructorRealm.TypeError, 204 writeInWriteRealm(objId, { 205 toString() { return {}; } 206 }), 207 'write TypeError should come from constructor realm'); 208 209 return promise_rejects_js(t, constructorRealm.TypeError, readPromise, 210 'read TypeError should come from constructor realm'); 211 }, 'TypeError for unconvertable chunk should come from constructor realm ' + 212 'of TextEncoderStream'); 213} 214 215function runTextDecoderStreamTests() { 216 promise_test(async () => { 217 const objId = await constructAndStore('TextDecoderStream'); 218 const writePromise = writeInWriteRealm(objId, new Uint8Array([65])); 219 const result = await readInReadRealm(objId); 220 await writePromise; 221 assert_equals(result.constructor, constructorRealm.Object, 222 'result should be in constructor realm'); 223 // A string is not an object, so doesn't have an associated realm. Accessing 224 // string properties will create a transient object wrapper belonging to the 225 // current realm. So checking the realm of result.value is not useful. 226 }, 'the result object when read is called after write should come from the ' + 227 'same realm as the constructor of TextDecoderStream'); 228 229 promise_test(async () => { 230 const objId = await constructAndStore('TextDecoderStream'); 231 const chunkPromise = readInReadRealm(objId); 232 writeInWriteRealm(objId, new Uint8Array([65])); 233 // Now the read() should resolve. 234 const result = await chunkPromise; 235 assert_equals(result.constructor, constructorRealm.Object, 236 'result should be in constructor realm'); 237 // A string is not an object, so doesn't have an associated realm. Accessing 238 // string properties will create a transient object wrapper belonging to the 239 // current realm. So checking the realm of result.value is not useful. 240 }, 'the result object when write is called with a pending ' + 241 'read should come from the same realm as the constructor of TextDecoderStream'); 242 243 promise_test(async t => { 244 const objId = await constructAndStore('TextDecoderStream'); 245 // Read first to relieve backpressure. 246 const readPromise = readInReadRealm(objId); 247 await promise_rejects_js( 248 t, constructorRealm.TypeError, 249 writeInWriteRealm(objId, {}), 250 'write TypeError should come from constructor realm' 251 ); 252 253 return promise_rejects_js( 254 t, constructorRealm.TypeError, readPromise, 255 'read TypeError should come from constructor realm' 256 ); 257 }, 'TypeError for chunk with the wrong type should come from constructor ' + 258 'realm of TextDecoderStream'); 259 260 promise_test(async t => { 261 const objId = 262 await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`); 263 // Read first to relieve backpressure. 264 const readPromise = readInReadRealm(objId); 265 266 await promise_rejects_js( 267 t, constructorRealm.TypeError, 268 writeInWriteRealm(objId, new Uint8Array([0xff])), 269 'write TypeError should come from constructor realm' 270 ); 271 272 return promise_rejects_js( 273 t, constructorRealm.TypeError, readPromise, 274 'read TypeError should come from constructor realm' 275 ); 276 }, 'TypeError for invalid chunk should come from constructor realm ' + 277 'of TextDecoderStream'); 278 279 promise_test(async t => { 280 const objId = 281 await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`); 282 // Read first to relieve backpressure. 283 readInReadRealm(objId); 284 // Write an unfinished sequence of bytes. 285 const incompleteBytesId = id(); 286 writeRealm[incompleteBytesId] = new Uint8Array([0xf0]); 287 288 return promise_rejects_js( 289 t, constructorRealm.TypeError, 290 // Can't use writeInWriteRealm() here because it doesn't make it possible 291 // to reuse the writer. 292 evalInRealmAndReturn(writeRealm, ` 293(() => { 294 const writer = window.${objId}.writable.getWriter(); 295 parent.writeMethod.call(writer, window.${incompleteBytesId}); 296 return parent.methodRealm.WritableStreamDefaultWriter.prototype 297 .close.call(writer); 298})(); 299`), 300 'close TypeError should come from constructor realm' 301 ); 302 }, 'TypeError for incomplete input should come from constructor realm ' + 303 'of TextDecoderStream'); 304} 305