diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 2369175733c115..8d884c43c2f9c3 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -94,6 +94,7 @@ const { ArrayBufferViewGetByteLength, ArrayBufferViewGetByteOffset, AsyncIterator, + canCopyArrayBuffer, cloneAsUint8Array, copyArrayBuffer, createPromiseCallback, @@ -2552,6 +2553,15 @@ function readableByteStreamControllerCommitPullIntoDescriptor(stream, desc) { } } +function readableByteStreamControllerCommitPullIntoDescriptors(stream, descriptors) { + for (let i = 0; i < descriptors.length; ++i) { + readableByteStreamControllerCommitPullIntoDescriptor( + stream, + descriptors[i], + ); + } +} + function readableByteStreamControllerInvalidateBYOBRequest(controller) { if (controller[kState].byobRequest === null) return; @@ -2758,11 +2768,11 @@ function readableByteStreamControllerRespondInClosedState(controller, desc) { stream, } = controller[kState]; if (readableStreamHasBYOBReader(stream)) { - while (readableStreamGetNumReadIntoRequests(stream) > 0) { - readableByteStreamControllerCommitPullIntoDescriptor( - stream, - readableByteStreamControllerShiftPendingPullInto(controller)); + const filledPullIntos = []; + for (let i = 0; i < readableStreamGetNumReadIntoRequests(stream); ++i) { + ArrayPrototypePush(filledPullIntos, readableByteStreamControllerShiftPendingPullInto(controller)); } + readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos); } } @@ -2843,8 +2853,9 @@ function readableByteStreamControllerEnqueue(controller, chunk) { transferredBuffer, byteOffset, byteLength); - readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( + const filledPullIntos = readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( controller); + readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos); } else { assert(!isReadableStreamLocked(stream)); readableByteStreamControllerEnqueueChunkToQueue( @@ -2937,6 +2948,7 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue( const maxAlignedBytes = maxBytesFilled - (maxBytesFilled % elementSize); let totalBytesToCopyRemaining = maxBytesToCopy; let ready = false; + assert(!ArrayBufferPrototypeGetDetached(buffer)); assert(bytesFilled < minimumFill); if (maxAlignedBytes >= minimumFill) { totalBytesToCopyRemaining = maxAlignedBytes - bytesFilled; @@ -2952,12 +2964,12 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue( totalBytesToCopyRemaining, headOfQueue.byteLength); const destStart = byteOffset + desc.bytesFilled; - const arrayBufferByteLength = ArrayBufferPrototypeGetByteLength(buffer); - if (arrayBufferByteLength - destStart < bytesToCopy) { - throw new ERR_INVALID_STATE.RangeError( - 'view ArrayBuffer size is invalid'); - } - assert(arrayBufferByteLength - destStart >= bytesToCopy); + assert(canCopyArrayBuffer( + buffer, + destStart, + headOfQueue.buffer, + headOfQueue.byteOffset, + bytesToCopy)); copyArrayBuffer( buffer, destStart, @@ -2991,26 +3003,30 @@ function readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( const { closeRequested, pendingPullIntos, - stream, } = controller[kState]; assert(!closeRequested); + const filledPullIntos = []; while (pendingPullIntos.length) { if (!controller[kState].queueTotalSize) - return; + break; const desc = pendingPullIntos[0]; if (readableByteStreamControllerFillPullIntoDescriptorFromQueue( controller, desc)) { readableByteStreamControllerShiftPendingPullInto(controller); - readableByteStreamControllerCommitPullIntoDescriptor(stream, desc); + ArrayPrototypePush(filledPullIntos, desc); } } + return filledPullIntos; } function readableByteStreamControllerRespondInReadableState( controller, bytesWritten, desc) { + const { + stream, + } = controller[kState]; const { buffer, bytesFilled, @@ -3031,9 +3047,10 @@ function readableByteStreamControllerRespondInReadableState( controller, desc, ); - readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( + const filledPullIntos = readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( controller, ); + readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos); return; } @@ -3059,10 +3076,10 @@ function readableByteStreamControllerRespondInReadableState( ArrayBufferPrototypeGetByteLength(remainder)); } desc.bytesFilled -= remainderSize; - readableByteStreamControllerCommitPullIntoDescriptor( - controller[kState].stream, - desc); - readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + const filledPullIntos = readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + + readableByteStreamControllerCommitPullIntoDescriptor(stream, desc); + readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos); } function readableByteStreamControllerRespondWithNewView(controller, view) { diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index 2c70ef7acdfe66..5bf016f73b7af5 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -1,6 +1,8 @@ 'use strict'; const { + ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeGetDetached, ArrayBufferPrototypeSlice, ArrayPrototypePush, ArrayPrototypeShift, @@ -107,6 +109,14 @@ function cloneAsUint8Array(view) { ); } +function canCopyArrayBuffer(toBuffer, toIndex, fromBuffer, fromIndex, count) { + return toBuffer !== fromBuffer && + !ArrayBufferPrototypeGetDetached(toBuffer) && + !ArrayBufferPrototypeGetDetached(fromBuffer) && + toIndex + count <= ArrayBufferPrototypeGetByteLength(toBuffer) && + fromIndex + count <= ArrayBufferPrototypeGetByteLength(fromBuffer); +} + function isBrandCheck(brand) { return (value) => { return value != null && @@ -261,6 +271,7 @@ module.exports = { ArrayBufferViewGetByteLength, ArrayBufferViewGetByteOffset, AsyncIterator, + canCopyArrayBuffer, createPromiseCallback, cloneAsUint8Array, copyArrayBuffer, diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 86d83e913e6ed4..640129d187033b 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -27,7 +27,7 @@ Last update: - performance-timeline: https://github.com/web-platform-tests/wpt/tree/94caab7038/performance-timeline - resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing - resources: https://github.com/web-platform-tests/wpt/tree/1e140d63ec/resources -- streams: https://github.com/web-platform-tests/wpt/tree/2bd26e124c/streams +- streams: https://github.com/web-platform-tests/wpt/tree/bc9dcbbf1a/streams - url: https://github.com/web-platform-tests/wpt/tree/67880a4eb8/url - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi diff --git a/test/fixtures/wpt/streams/idlharness-shadowrealm.window.js b/test/fixtures/wpt/streams/idlharness-shadowrealm.window.js deleted file mode 100644 index 099b2475ca7e87..00000000000000 --- a/test/fixtures/wpt/streams/idlharness-shadowrealm.window.js +++ /dev/null @@ -1,2 +0,0 @@ -// META: script=/resources/idlharness-shadowrealm.js -idl_test_shadowrealm(["streams"], ["dom"]); diff --git a/test/fixtures/wpt/streams/idlharness.any.js b/test/fixtures/wpt/streams/idlharness.any.js index 42a17da58c5ae3..0be03b2078f9bc 100644 --- a/test/fixtures/wpt/streams/idlharness.any.js +++ b/test/fixtures/wpt/streams/idlharness.any.js @@ -1,4 +1,4 @@ -// META: global=window,worker +// META: global=window,worker,shadowrealm-in-window // META: script=/resources/WebIDLParser.js // META: script=/resources/idlharness.js // META: timeout=long diff --git a/test/fixtures/wpt/streams/readable-byte-streams/general.any.js b/test/fixtures/wpt/streams/readable-byte-streams/general.any.js index cdce2244c3c84b..4b0c73865f7cf9 100644 --- a/test/fixtures/wpt/streams/readable-byte-streams/general.any.js +++ b/test/fixtures/wpt/streams/readable-byte-streams/general.any.js @@ -870,11 +870,11 @@ promise_test(() => { start(c) { controller = c; }, - async pull() { + pull() { byobRequestDefined.push(controller.byobRequest !== null); const initialByobRequest = controller.byobRequest; - const transferredView = await transferArrayBufferView(controller.byobRequest.view); + const transferredView = transferArrayBufferView(controller.byobRequest.view); transferredView[0] = 0x01; controller.byobRequest.respondWithNewView(transferredView); @@ -2288,7 +2288,7 @@ promise_test(async t => { await pullCalledPromise; // Transfer the original BYOB request's buffer, and respond with a new view on that buffer - const transferredView = await transferArrayBufferView(controller.byobRequest.view); + const transferredView = transferArrayBufferView(controller.byobRequest.view); const newView = transferredView.subarray(0, 1); newView[0] = 42; @@ -2328,7 +2328,7 @@ promise_test(async t => { await pullCalledPromise; // Transfer the original BYOB request's buffer, and respond with an empty view on that buffer - const transferredView = await transferArrayBufferView(controller.byobRequest.view); + const transferredView = transferArrayBufferView(controller.byobRequest.view); const newView = transferredView.subarray(0, 0); controller.close(); diff --git a/test/fixtures/wpt/streams/readable-byte-streams/patched-global.any.js b/test/fixtures/wpt/streams/readable-byte-streams/patched-global.any.js new file mode 100644 index 00000000000000..ce2e9e9993ae57 --- /dev/null +++ b/test/fixtures/wpt/streams/readable-byte-streams/patched-global.any.js @@ -0,0 +1,54 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +'use strict'; + +// Tests which patch the global environment are kept separate to avoid +// interfering with other tests. + +promise_test(async (t) => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + const reader = rs.getReader({mode: 'byob'}); + + const length = 0x4000; + const buffer = new ArrayBuffer(length); + const bigArray = new BigUint64Array(buffer, length - 8, 1); + + const read1 = reader.read(new Uint8Array(new ArrayBuffer(0x100))); + const read2 = reader.read(bigArray); + + let flag = false; + Object.defineProperty(Object.prototype, 'then', { + get: t.step_func(() => { + if (!flag) { + flag = true; + assert_equals(controller.byobRequest, null, 'byobRequest should be null after filling both views'); + } + }), + configurable: true + }); + t.add_cleanup(() => { + delete Object.prototype.then; + }); + + controller.enqueue(new Uint8Array(0x110).fill(0x42)); + assert_true(flag, 'patched then() should be called'); + + // The first read() is filled entirely with 0x100 bytes + const result1 = await read1; + assert_false(result1.done, 'result1.done'); + assert_typed_array_equals(result1.value, new Uint8Array(0x100).fill(0x42), 'result1.value'); + + // The second read() is filled with the remaining 0x10 bytes + const result2 = await read2; + assert_false(result2.done, 'result2.done'); + assert_equals(result2.value.constructor, BigUint64Array, 'result2.value constructor'); + assert_equals(result2.value.byteOffset, length - 8, 'result2.value byteOffset'); + assert_equals(result2.value.length, 1, 'result2.value length'); + assert_array_equals([...result2.value], [0x42424242_42424242n], 'result2.value contents'); +}, 'Patched then() sees byobRequest after filling all pending pull-into descriptors'); diff --git a/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js b/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js index 7dd5ba3f3fb013..60d82b9cf6a1fd 100644 --- a/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js +++ b/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js @@ -934,3 +934,36 @@ promise_test(async () => { assert_typed_array_equals(result4.value, new Uint8Array([0]).subarray(0, 0), 'second chunk from branch2 should be correct'); }, 'ReadableStream teeing with byte source: respond() and close() while both branches are pulling'); + +promise_test(async t => { + let pullCount = 0; + const arrayBuffer = new Uint8Array([0x01, 0x02, 0x03]).buffer; + const enqueuedChunk = new Uint8Array(arrayBuffer, 2); + assert_equals(enqueuedChunk.length, 1); + assert_equals(enqueuedChunk.byteOffset, 2); + const rs = new ReadableStream({ + type: 'bytes', + pull(c) { + ++pullCount; + if (pullCount === 1) { + c.enqueue(enqueuedChunk); + } + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + const [result1, result2] = await Promise.all([reader1.read(), reader2.read()]); + assert_equals(result1.done, false, 'reader1 done'); + assert_equals(result2.done, false, 'reader2 done'); + + const view1 = result1.value; + const view2 = result2.value; + // The first stream has the transferred buffer, but the second stream has the + // cloned buffer. + const underlying = new Uint8Array([0x01, 0x02, 0x03]).buffer; + assert_typed_array_equals(view1, new Uint8Array(underlying, 2), 'reader1 value'); + assert_typed_array_equals(view2, new Uint8Array([0x03]), 'reader2 value'); +}, 'ReadableStream teeing with byte source: reading an array with a byte offset should clone correctly'); diff --git a/test/fixtures/wpt/streams/readable-streams/crashtests/from-cross-realm.https.html b/test/fixtures/wpt/streams/readable-streams/crashtests/from-cross-realm.https.html new file mode 100644 index 00000000000000..58a4371186ece7 --- /dev/null +++ b/test/fixtures/wpt/streams/readable-streams/crashtests/from-cross-realm.https.html @@ -0,0 +1,18 @@ + + + diff --git a/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js b/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js index b652f9c5fcb4b6..ec01fda0b3c737 100644 --- a/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js +++ b/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js @@ -1,4 +1,4 @@ -// META: global=window,worker,shadowrealm +// META: global=window,worker // META: script=../resources/test-utils.js // META: script=../resources/rs-utils.js 'use strict'; diff --git a/test/fixtures/wpt/streams/resources/rs-utils.js b/test/fixtures/wpt/streams/resources/rs-utils.js index f1a014275a2fbc..0f7742a5b3b190 100644 --- a/test/fixtures/wpt/streams/resources/rs-utils.js +++ b/test/fixtures/wpt/streams/resources/rs-utils.js @@ -1,5 +1,42 @@ 'use strict'; (function () { + // Fake setInterval-like functionality in environments that don't have it + class IntervalHandle { + constructor(callback, delayMs) { + this.callback = callback; + this.delayMs = delayMs; + this.cancelled = false; + Promise.resolve().then(() => this.check()); + } + + async check() { + while (true) { + await new Promise(resolve => step_timeout(resolve, this.delayMs)); + if (this.cancelled) { + return; + } + this.callback(); + } + } + + cancel() { + this.cancelled = true; + } + } + + let localSetInterval, localClearInterval; + if (typeof globalThis.setInterval !== "undefined" && + typeof globalThis.clearInterval !== "undefined") { + localSetInterval = globalThis.setInterval; + localClearInterval = globalThis.clearInterval; + } else { + localSetInterval = function setInterval(callback, delayMs) { + return new IntervalHandle(callback, delayMs); + } + localClearInterval = function clearInterval(handle) { + handle.cancel(); + } + } class RandomPushSource { constructor(toPush) { @@ -18,12 +55,12 @@ } if (!this.started) { - this._intervalHandle = setInterval(writeChunk, 2); + this._intervalHandle = localSetInterval(writeChunk, 2); this.started = true; } if (this.paused) { - this._intervalHandle = setInterval(writeChunk, 2); + this._intervalHandle = localSetInterval(writeChunk, 2); this.paused = false; } @@ -37,7 +74,7 @@ if (source.toPush > 0 && source.pushed > source.toPush) { if (source._intervalHandle) { - clearInterval(source._intervalHandle); + localClearInterval(source._intervalHandle); source._intervalHandle = undefined; } source.closed = true; @@ -55,7 +92,7 @@ if (this.started) { this.paused = true; - clearInterval(this._intervalHandle); + localClearInterval(this._intervalHandle); this._intervalHandle = undefined; } else { throw new Error('Can\'t pause reading an unstarted source.'); @@ -178,15 +215,7 @@ } function transferArrayBufferView(view) { - const noopByteStream = new ReadableStream({ - type: 'bytes', - pull(c) { - c.byobRequest.respond(c.byobRequest.view.byteLength); - c.close(); - } - }); - const reader = noopByteStream.getReader({ mode: 'byob' }); - return reader.read(view).then((result) => result.value); + return structuredClone(view, { transfer: [view.buffer] }); } self.RandomPushSource = RandomPushSource; diff --git a/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js b/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js index 37f8c9df169607..3bfe634a6e153d 100644 --- a/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js +++ b/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js @@ -105,7 +105,7 @@ async function transferMessagePortWith(constructor) { await transferMessagePortWithOrder3(new constructor()); } -async function advancedTransferMesagePortWith(constructor) { +async function advancedTransferMessagePortWith(constructor) { await transferMessagePortWithOrder4(new constructor()); await transferMessagePortWithOrder5(new constructor()); await transferMessagePortWithOrder6(new constructor()); @@ -166,7 +166,7 @@ async function mixedTransferMessagePortWithOrder3() { ); } -async function mixedTransferMesagePortWith() { +async function mixedTransferMessagePortWith() { await mixedTransferMessagePortWithOrder1(); await mixedTransferMessagePortWithOrder2(); await mixedTransferMessagePortWithOrder3(); @@ -185,19 +185,19 @@ promise_test(async t => { }, "Transferring a MessagePort with a TransformStream should set `.ports`"); promise_test(async t => { - await transferMessagePortWith(ReadableStream); + await advancedTransferMessagePortWith(ReadableStream); }, "Transferring a MessagePort with a ReadableStream should set `.ports`, advanced"); promise_test(async t => { - await transferMessagePortWith(WritableStream); + await advancedTransferMessagePortWith(WritableStream); }, "Transferring a MessagePort with a WritableStream should set `.ports`, advanced"); promise_test(async t => { - await transferMessagePortWith(TransformStream); + await advancedTransferMessagePortWith(TransformStream); }, "Transferring a MessagePort with a TransformStream should set `.ports`, advanced"); promise_test(async t => { - await mixedTransferMesagePortWith(); + await mixedTransferMessagePortWith(); }, "Transferring a MessagePort with multiple streams should set `.ports`"); test(() => { diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index e29d2eabde1f26..c2ff3c4ed07d43 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -68,7 +68,7 @@ "path": "resources" }, "streams": { - "commit": "2bd26e124cf17b2f0a25c150794d640b07b2a870", + "commit": "bc9dcbbf1a4c2c741ef47f47d6ede6458f40c4a4", "path": "streams" }, "url": {