Skip to content

Commit 033c6d9

Browse files
Various fixes for readable byte streams
* respondWithNewView(newView) can now be called with a newView that is *smaller than* the BYOB request's view. This aligns it with respond(bytesWritten), which also allows bytesWritten <= byobRequest.view.byteLength. * respondWithNewView() must now be called with an empty view when the stream is closed. This aligns it with respond(bytesWritten), which requires bytesWritten to be 0 when closed. * respondWithNewView(newView) must now be called with a view whose view.buffer.byteLength matches that of the BYOB request. Ideally, we would like to assert that the new view's buffer is the "transferred version" of the BYOB request's buffer, but that's not possible yet with the tools currently provided by the ECMAScript specification. * enqueue(chunk) and respondWithNewView(newView) now check that the given view's buffer is actually *transferable*. Previously, we only checked whether the buffer is *not yet detached*, but this is insufficient: a WebAssembly.Memory's buffer is *never* transferable. We also make sure to not transfer the given buffer until *after* we've checked all other preconditions, so the buffer is still intact if these methods were to throw an error. * enqueue() and respond() now check that the BYOB request's view has *not* been transferred, since otherwise it's not possible to copy bytes into its buffer and/or transfer the buffer when committing. * enqueue(), respond(), and respondWithNewView() immediately invalidate the BYOB request. Previously, we only did this if we actually filled the first pull-into descriptor, which doesn't *always* happen. (For example: if the pull-into descriptor's element size is 4, but we only have filled 1 or 2 bytes.) * We now always transfer the pull-into descriptor's buffer when committing it (to fulfill a read request or read-into request). This is mainly a sanity check: the stream should never use this buffer after it has been committed.
1 parent 0ff6d45 commit 033c6d9

File tree

8 files changed

+198
-66
lines changed

8 files changed

+198
-66
lines changed

index.bs

Lines changed: 101 additions & 35 deletions
Large diffs are not rendered by default.

reference-implementation/lib/ReadableByteStreamController-impl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ exports.implementation = class ReadableByteStreamControllerImpl {
108108

109109
const pullIntoDescriptor = {
110110
buffer,
111+
bufferByteLength: autoAllocateChunkSize,
111112
byteOffset: 0,
112113
byteLength: autoAllocateChunkSize,
113114
bytesFilled: 0,

reference-implementation/lib/ReadableStreamBYOBReader-impl.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const { newPromise, resolvePromise, rejectPromise, promiseRejectedWith } = require('./helpers/webidl.js');
4+
const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js');
45
const aos = require('./abstract-ops/readable-streams.js');
56
const { mixin } = require('./helpers/miscellaneous.js');
67
const ReadableStreamGenericReaderImpl = require('./ReadableStreamGenericReader-impl.js').implementation;
@@ -17,6 +18,9 @@ class ReadableStreamBYOBReaderImpl {
1718
if (view.buffer.byteLength === 0) {
1819
return promiseRejectedWith(new TypeError('view\'s buffer must have non-zero byteLength'));
1920
}
21+
if (IsDetachedBuffer(view.buffer) === true) {
22+
return promiseRejectedWith(new TypeError('view\'s buffer has been detached'));
23+
}
2024

2125
if (this._stream === undefined) {
2226
return promiseRejectedWith(readerLockException('read'));

reference-implementation/lib/ReadableStreamBYOBRequest-impl.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,14 @@ exports.implementation = class ReadableStreamBYOBRequestImpl {
2525
}
2626

2727
respondWithNewView(view) {
28-
if (view.byteLength === 0) {
29-
throw new TypeError('chunk must have non-zero byteLength');
30-
}
31-
if (view.buffer.byteLength === 0) {
32-
throw new TypeError('chunk\'s buffer must have non-zero byteLength');
33-
}
34-
3528
if (this._controller === undefined) {
3629
throw new TypeError('This BYOB request has been invalidated');
3730
}
3831

32+
if (IsDetachedBuffer(view.buffer) === true) {
33+
throw new TypeError('The given view\'s buffer has been detached and so cannot be used as a response');
34+
}
35+
3936
aos.ReadableByteStreamControllerRespondWithNewView(this._controller, view);
4037
}
4138
};

reference-implementation/lib/abstract-ops/ecmascript.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ exports.TransferArrayBuffer = O => {
3030
return transferredIshVersion;
3131
};
3232

33+
// Not implemented correctly
34+
exports.CanTransferArrayBuffer = O => {
35+
return !exports.IsDetachedBuffer(O);
36+
};
37+
3338
// Not implemented correctly
3439
exports.IsDetachedBuffer = O => {
3540
return isFakeDetached in O;

reference-implementation/lib/abstract-ops/readable-streams.js

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ const assert = require('assert');
44
const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, uponPromise,
55
setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, uponRejection } =
66
require('../helpers/webidl.js');
7-
const { CopyDataBlockBytes, CreateArrayFromList, TransferArrayBuffer } = require('./ecmascript.js');
7+
const { CanTransferArrayBuffer, CopyDataBlockBytes, CreateArrayFromList, IsDetachedBuffer, TransferArrayBuffer } =
8+
require('./ecmascript.js');
89
const { IsNonNegativeNumber } = require('./miscellaneous.js');
910
const { EnqueueValueWithSize, ResetQueue } = require('./queue-with-sizes.js');
1011
const { AcquireWritableStreamDefaultWriter, IsWritableStreamLocked, WritableStreamAbort,
@@ -983,8 +984,8 @@ function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescripto
983984
assert(bytesFilled <= pullIntoDescriptor.byteLength);
984985
assert(bytesFilled % elementSize === 0);
985986

986-
return new pullIntoDescriptor.viewConstructor(
987-
pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize);
987+
const buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
988+
return new pullIntoDescriptor.viewConstructor(buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize);
988989
}
989990

990991
function ReadableByteStreamControllerEnqueue(controller, chunk) {
@@ -997,8 +998,23 @@ function ReadableByteStreamControllerEnqueue(controller, chunk) {
997998
const buffer = chunk.buffer;
998999
const byteOffset = chunk.byteOffset;
9991000
const byteLength = chunk.byteLength;
1001+
if (IsDetachedBuffer(buffer) === true) {
1002+
throw new TypeError('chunk\'s buffer is detached and so cannot be enqueued');
1003+
}
10001004
const transferredBuffer = TransferArrayBuffer(buffer);
10011005

1006+
if (controller._pendingPullIntos.length > 0) {
1007+
const firstPendingPullInto = controller._pendingPullIntos[0];
1008+
if (IsDetachedBuffer(firstPendingPullInto.buffer) === true) {
1009+
throw new TypeError(
1010+
'The BYOB request\'s buffer has been detached and so cannot be filled with an enqueued chunk'
1011+
);
1012+
}
1013+
firstPendingPullInto.buffer = TransferArrayBuffer(firstPendingPullInto.buffer);
1014+
}
1015+
1016+
ReadableByteStreamControllerInvalidateBYOBRequest(controller);
1017+
10021018
if (ReadableStreamHasDefaultReader(stream) === true) {
10031019
if (ReadableStreamGetNumReadRequests(stream) === 0) {
10041020
ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
@@ -1041,8 +1057,7 @@ function ReadableByteStreamControllerError(controller, e) {
10411057

10421058
function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) {
10431059
assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos[0] === pullIntoDescriptor);
1044-
1045-
ReadableByteStreamControllerInvalidateBYOBRequest(controller);
1060+
assert(controller._byobRequest === null);
10461061
pullIntoDescriptor.bytesFilled += size;
10471062
}
10481063

@@ -1160,9 +1175,17 @@ function ReadableByteStreamControllerPullInto(controller, view, readIntoRequest)
11601175

11611176
const ctor = view.constructor;
11621177

1163-
const buffer = TransferArrayBuffer(view.buffer);
1178+
let buffer;
1179+
try {
1180+
buffer = TransferArrayBuffer(view.buffer);
1181+
} catch (e) {
1182+
readIntoRequest.errorSteps(e);
1183+
return;
1184+
}
1185+
11641186
const pullIntoDescriptor = {
11651187
buffer,
1188+
bufferByteLength: buffer.byteLength,
11661189
byteOffset: view.byteOffset,
11671190
byteLength: view.byteLength,
11681191
bytesFilled: 0,
@@ -1216,12 +1239,29 @@ function ReadableByteStreamControllerPullInto(controller, view, readIntoRequest)
12161239
function ReadableByteStreamControllerRespond(controller, bytesWritten) {
12171240
assert(controller._pendingPullIntos.length > 0);
12181241

1242+
const firstDescriptor = controller._pendingPullIntos[0];
1243+
const state = controller._stream._state;
1244+
1245+
if (state === 'closed') {
1246+
if (bytesWritten !== 0) {
1247+
throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream');
1248+
}
1249+
} else {
1250+
assert(state === 'readable');
1251+
if (bytesWritten === 0) {
1252+
throw new TypeError('bytesWritten must be greater than 0 when calling respond() on a readable stream');
1253+
}
1254+
if (firstDescriptor.bytesFilled + bytesWritten > firstDescriptor.byteLength) {
1255+
throw new RangeError('bytesWritten out of range');
1256+
}
1257+
}
1258+
1259+
firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer);
1260+
12191261
ReadableByteStreamControllerRespondInternal(controller, bytesWritten);
12201262
}
12211263

12221264
function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) {
1223-
firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer);
1224-
12251265
assert(firstDescriptor.bytesFilled === 0);
12261266

12271267
const stream = controller._stream;
@@ -1234,14 +1274,11 @@ function ReadableByteStreamControllerRespondInClosedState(controller, firstDescr
12341274
}
12351275

12361276
function ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) {
1237-
if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) {
1238-
throw new RangeError('bytesWritten out of range');
1239-
}
1277+
assert(pullIntoDescriptor.bytesFilled + bytesWritten <= pullIntoDescriptor.byteLength);
12401278

12411279
ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor);
12421280

12431281
if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) {
1244-
// TODO: Figure out whether we should detach the buffer or not here.
12451282
return;
12461283
}
12471284

@@ -1254,7 +1291,6 @@ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWri
12541291
ReadableByteStreamControllerEnqueueChunkToQueue(controller, remainder, 0, remainder.byteLength);
12551292
}
12561293

1257-
pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
12581294
pullIntoDescriptor.bytesFilled -= remainderSize;
12591295
ReadableByteStreamControllerCommitPullIntoDescriptor(controller._stream, pullIntoDescriptor);
12601296

@@ -1263,18 +1299,17 @@ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWri
12631299

12641300
function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) {
12651301
const firstDescriptor = controller._pendingPullIntos[0];
1302+
assert(CanTransferArrayBuffer(firstDescriptor.buffer) === true);
12661303

1267-
const state = controller._stream._state;
1304+
ReadableByteStreamControllerInvalidateBYOBRequest(controller);
12681305

1306+
const state = controller._stream._state;
12691307
if (state === 'closed') {
1270-
if (bytesWritten !== 0) {
1271-
throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream');
1272-
}
1273-
1308+
assert(bytesWritten === 0);
12741309
ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor);
12751310
} else {
12761311
assert(state === 'readable');
1277-
1312+
assert(bytesWritten > 0);
12781313
ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor);
12791314
}
12801315

@@ -1283,24 +1318,42 @@ function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) {
12831318

12841319
function ReadableByteStreamControllerRespondWithNewView(controller, view) {
12851320
assert(controller._pendingPullIntos.length > 0);
1321+
assert(IsDetachedBuffer(view.buffer) === false);
12861322

12871323
const firstDescriptor = controller._pendingPullIntos[0];
1324+
const state = controller._stream._state;
1325+
1326+
if (state === 'closed') {
1327+
if (view.byteLength !== 0) {
1328+
throw new TypeError('The view\'s length must be 0 when calling respondWithNewView() on a closed stream');
1329+
}
1330+
} else {
1331+
assert(state === 'readable');
1332+
if (view.byteLength === 0) {
1333+
throw new TypeError(
1334+
'The view\'s length must be greater than 0 when calling respondWithNewView() on a readable stream'
1335+
);
1336+
}
1337+
}
12881338

12891339
if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) {
12901340
throw new RangeError('The region specified by view does not match byobRequest');
12911341
}
1292-
if (firstDescriptor.byteLength !== view.byteLength) {
1342+
if (firstDescriptor.bufferByteLength !== view.buffer.byteLength) {
12931343
throw new RangeError('The buffer of view has different capacity than byobRequest');
12941344
}
1345+
if (firstDescriptor.bytesFilled + view.byteLength > firstDescriptor.byteLength) {
1346+
throw new RangeError('The region specified by view is larger than byobRequest');
1347+
}
12951348

1296-
firstDescriptor.buffer = view.buffer;
1349+
firstDescriptor.buffer = TransferArrayBuffer(view.buffer);
12971350

12981351
ReadableByteStreamControllerRespondInternal(controller, view.byteLength);
12991352
}
13001353

13011354
function ReadableByteStreamControllerShiftPendingPullInto(controller) {
1355+
assert(controller._byobRequest === null);
13021356
const descriptor = controller._pendingPullIntos.shift();
1303-
ReadableByteStreamControllerInvalidateBYOBRequest(controller);
13041357
return descriptor;
13051358
}
13061359

reference-implementation/run-web-platform-tests.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ async function main() {
3333
const testsPath = path.resolve(wptPath, 'streams');
3434

3535
const filterGlobs = process.argv.length >= 3 ? process.argv.slice(2) : ['**/*.html'];
36+
const excludeGlobs = [
37+
// These tests use ArrayBuffers backed by WebAssembly.Memory objects, which *should* be non-transferable.
38+
// However, our TransferArrayBuffer implementation cannot detect these, and will incorrectly "transfer" them anyway.
39+
'readable-byte-streams/non-transferable-buffers.any.html'
40+
];
3641
const anyTestPattern = /\.any\.html$/;
3742

3843
const bundledJS = await bundle(entryPath);
@@ -61,7 +66,8 @@ async function main() {
6166
return false;
6267
}
6368

64-
return filterGlobs.some(glob => minimatch(testPath, glob));
69+
return filterGlobs.some(glob => minimatch(testPath, glob)) &&
70+
!excludeGlobs.some(glob => minimatch(testPath, glob));
6571
}
6672
});
6773

0 commit comments

Comments
 (0)