Skip to content

Commit e00e11a

Browse files
authored
Merge pull request #7516 from cloudflare/jasnell/ts-streams-detached-dataview
Read detached and out-of-bounds DataView chunks as empty
2 parents bf52ccf + 68d99dd commit e00e11a

20 files changed

Lines changed: 252 additions & 112 deletions

‎src/per_isolate/crypto/digest-stream.ts‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,16 @@ import type {
5555
UnderlyingSink,
5656
WritableStream as WritableStreamType,
5757
} from '../webstreams/types';
58+
import type { ViewExtentHelpers } from '../webstreams/view-extent';
5859

5960
const {
6061
ArrayBufferPrototypeByteLengthGet,
6162
BigInt,
62-
DataViewPrototypeGetByteLength,
6363
ObjectDefineProperties,
6464
PromiseWithResolvers,
6565
SymbolDispose,
6666
SymbolToStringTag,
6767
TypeError,
68-
TypedArrayPrototypeGetByteLength,
69-
TypedArrayPrototypeGetSymbolToStringTag,
7068
} = primordials;
7169

7270
const {
@@ -79,6 +77,8 @@ const {
7977
const { WritableStream } = require('webstreams/writable') as {
8078
WritableStream: typeof WritableStreamType;
8179
};
80+
const { viewByteLength } =
81+
require('webstreams/view-extent') as ViewExtentHelpers;
8282

8383
type Chunk = ArrayBuffer | ArrayBufferView | string;
8484

@@ -104,12 +104,9 @@ function byteLengthOf(chunk: ArrayBuffer | ArrayBufferView): number {
104104
if (isArrayBuffer(chunk)) {
105105
return ArrayBufferPrototypeByteLengthGet(chunk);
106106
}
107-
// A view: either a TypedArray or a DataView. The tag getter returns the
108-
// internal [[TypedArrayName]], and undefined for a DataView.
109-
if (TypedArrayPrototypeGetSymbolToStringTag(chunk) !== undefined) {
110-
return TypedArrayPrototypeGetByteLength(chunk as Uint8Array);
111-
}
112-
return DataViewPrototypeGetByteLength(chunk as DataView);
107+
// A detached or out-of-bounds view, DataViews included, is empty (see
108+
// webstreams/view-extent.ts).
109+
return viewByteLength(chunk);
113110
}
114111

115112
// Collapses the WebCrypto-style `string | { name }` algorithm parameter to a

‎src/per_isolate/webstreams/AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ private-brand dispatch, no `instanceof`) apply here — see
1313
| File | Role |
1414
| ------------- | -------------------------------------------------------------------------------------------- |
1515
| `ring-buffer.ts` | O(1) FIFO with indexed access backing every internal queue below; leaf module |
16+
| `view-extent.ts` | A caller-supplied view's byte extent through captured getters; detached/out-of-bounds views (DataViews included) read as empty; leaf module |
1617
| `queue.ts` | QUEUED backend: single-queue/multi-cursor, JS sources; fence interfaces; invariant list |
1718
| `native.ts` | NATIVE backend: C++-backed pull conduit; **the C++/JS contract** + invariants |
1819
| `readable.ts` | Reader layer + queued controllers + the BACKEND-DISPATCH points (constructor, tee, chains, byte-capable gate, JS-to-C++ extraction) |

‎src/per_isolate/webstreams/compression.ts‎

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,16 @@ import type {
3737
RingBuffer as RingBufferType,
3838
RingBufferConstructor,
3939
} from './ring-buffer';
40+
import type { ViewExtentHelpers } from './view-extent';
4041

4142
const {
4243
ArrayBufferPrototypeByteLengthGet,
4344
DataViewPrototypeGetBuffer,
44-
DataViewPrototypeGetByteLength,
45-
DataViewPrototypeGetByteOffset,
4645
MathMin,
4746
ObjectDefineProperties,
4847
SymbolToStringTag,
4948
TypeError,
5049
TypedArrayPrototypeGetBuffer,
51-
TypedArrayPrototypeGetByteLength,
52-
TypedArrayPrototypeGetByteOffset,
5350
TypedArrayPrototypeSet,
5451
Uint8Array,
5552
uncurryThis,
@@ -71,6 +68,8 @@ const {
7168
WritableStreamDefaultController,
7269
internalsForPipe: writableInternals,
7370
} = require('webstreams/writable');
71+
const { viewByteExtent } =
72+
require('webstreams/view-extent') as ViewExtentHelpers;
7473
const { RingBuffer } = require('webstreams/ring-buffer') as {
7574
RingBuffer: RingBufferConstructor;
7675
};
@@ -142,8 +141,8 @@ function isValidChunk(chunk: unknown): boolean {
142141
// detaching, or mutating the buffer after write() returns cannot change
143142
// what the codec consumes — matching the C++ implementation, whose adapter
144143
// copies inside write() for exactly these hazards. Detached or
145-
// out-of-bounds inputs report zero length through the captured getters and
146-
// copy as empty (a codec no-op).
144+
// out-of-bounds inputs, DataViews included, copy as empty (a codec no-op;
145+
// see view-extent.ts).
147146
function snapshotChunk(chunk: unknown): Uint8Array {
148147
if (!isValidChunk(chunk)) {
149148
throw new TypeError(
@@ -157,15 +156,11 @@ function snapshotChunk(chunk: unknown): Uint8Array {
157156
buffer = chunk as ArrayBuffer;
158157
byteOffset = 0;
159158
byteLength = ArrayBufferPrototypeByteLengthGet(chunk) as number;
160-
} else if (isDataView(chunk)) {
161-
buffer = DataViewPrototypeGetBuffer(chunk) as ArrayBuffer;
162-
byteOffset = DataViewPrototypeGetByteOffset(chunk) as number;
163-
byteLength = DataViewPrototypeGetByteLength(chunk) as number;
164159
} else {
165-
const view = chunk as ArrayBufferView;
166-
buffer = TypedArrayPrototypeGetBuffer(view) as ArrayBuffer;
167-
byteOffset = TypedArrayPrototypeGetByteOffset(view) as number;
168-
byteLength = TypedArrayPrototypeGetByteLength(view) as number;
160+
const extent = viewByteExtent(chunk as ArrayBufferView);
161+
buffer = extent.buffer as ArrayBuffer;
162+
byteOffset = extent.byteOffset;
163+
byteLength = extent.byteLength;
169164
}
170165
const copy = new Uint8Array(byteLength);
171166
if (byteLength > 0) {

‎src/per_isolate/webstreams/identity.ts‎

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
// processChunk copies inside write() for exactly these hazards).
2121
// - Zero-length writes are accepted as NO-OPS: the write resolves
2222
// immediately without touching the readable queue (no zero-length
23-
// chunk enqueued, no backpressure interaction, no pull).
23+
// chunk enqueued, no backpressure interaction, no pull). A view whose
24+
// buffer is detached or out of bounds is zero-length, as in C++.
2425
//
2526
// See /src/tests/streams/identity/AGENTS.md for the IdentityTransformStream
2627
// and FixedLengthStream specification.
@@ -90,15 +91,13 @@ import type {
9091
RingBuffer as RingBufferType,
9192
RingBufferConstructor,
9293
} from './ring-buffer';
94+
import type { ViewExtentHelpers } from './view-extent';
9395

9496
const {
9597
ArrayBuffer,
9698
ArrayPrototypePush,
9799
ArrayBufferPrototypeByteLengthGet,
98100
BigInt,
99-
DataViewPrototypeGetBuffer,
100-
DataViewPrototypeGetByteLength,
101-
DataViewPrototypeGetByteOffset,
102101
Number,
103102
ObjectDefineProperties,
104103
ObjectFreeze,
@@ -115,7 +114,6 @@ const {
115114
TypedArrayPrototypeGetBuffer,
116115
TypedArrayPrototypeGetByteLength,
117116
TypedArrayPrototypeGetByteOffset,
118-
TypedArrayPrototypeGetSymbolToStringTag,
119117
TypedArrayPrototypeSet,
120118
Uint8Array,
121119
uncurryThis,
@@ -134,6 +132,8 @@ const {
134132
WritableStreamDefaultController,
135133
internalsForPipe: writableInternals,
136134
} = require('webstreams/writable');
135+
const { viewByteExtent, viewByteLength } =
136+
require('webstreams/view-extent') as ViewExtentHelpers;
137137
const { RingBuffer } = require('webstreams/ring-buffer') as {
138138
RingBuffer: RingBufferConstructor;
139139
};
@@ -205,28 +205,13 @@ function validateAndCopyChunk(chunk: unknown): Uint8Array | undefined {
205205
return copy;
206206
}
207207
if (isArrayBufferView(chunk)) {
208-
const isDataView =
209-
TypedArrayPrototypeGetSymbolToStringTag(chunk) === undefined;
210-
const byteOffset = (
211-
isDataView
212-
? DataViewPrototypeGetByteOffset(chunk)
213-
: TypedArrayPrototypeGetByteOffset(chunk)
214-
) as number;
215-
const byteLength = (
216-
isDataView
217-
? DataViewPrototypeGetByteLength(chunk)
218-
: TypedArrayPrototypeGetByteLength(chunk)
219-
) as number;
208+
// A detached or out-of-bounds view is empty (see view-extent.ts).
209+
const { buffer, byteOffset, byteLength } = viewByteExtent(chunk);
220210
if (byteLength === 0) return undefined;
221-
const buffer = (
222-
isDataView
223-
? DataViewPrototypeGetBuffer(chunk)
224-
: TypedArrayPrototypeGetBuffer(chunk)
225-
) as ArrayBuffer;
226211
const copy = new Uint8Array(new ArrayBuffer(byteLength));
227212
TypedArrayPrototypeSet(
228213
copy,
229-
new Uint8Array(buffer, byteOffset, byteLength)
214+
new Uint8Array(buffer as ArrayBuffer, byteOffset, byteLength)
230215
);
231216
return copy;
232217
}
@@ -271,13 +256,7 @@ function byteSize(chunk: unknown): number {
271256
// byteSize runs only on chunks validateAndCopyChunk has already
272257
// accepted (sizeAndSnapshot validates before sizing), so anything that
273258
// is not a string or (Shared)ArrayBuffer is an ArrayBufferView.
274-
const isDataView =
275-
TypedArrayPrototypeGetSymbolToStringTag(chunk) === undefined;
276-
return (
277-
isDataView
278-
? DataViewPrototypeGetByteLength(chunk)
279-
: TypedArrayPrototypeGetByteLength(chunk)
280-
) as number;
259+
return viewByteLength(chunk as ArrayBufferView);
281260
}
282261

283262
let assertIsIdentityTransformStream: (self: IdentityTransformStream) => void;

‎src/per_isolate/webstreams/readable.ts‎

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ const {
110110
internalsForPipe: writableInternals,
111111
} = require('webstreams/writable');
112112

113+
import type { ViewExtentHelpers } from './view-extent';
114+
const { viewByteExtent } =
115+
require('webstreams/view-extent') as ViewExtentHelpers;
116+
113117
// The native backend (see the fence conventions in native.ts and
114118
// queue.ts). The cast restores the real shape the untyped loader erases,
115119
// so the brand predicates keep their type-guard narrowing.
@@ -4838,27 +4842,16 @@ async function collectChunks<R>(
48384842
// Drained chunks are untrusted values: any BufferSource contributes
48394843
// its bytes, with the extent pinned at drain time; anything else
48404844
// fails with the same TypeError the C++ bridge pump uses. Detached
4841-
// inputs are skipped with the other empties.
4845+
// or out-of-bounds inputs are skipped with the other empties (see
4846+
// view-extent.ts).
48424847
let buffer: ArrayBufferLike;
48434848
let byteOffset: number;
48444849
let byteLength: number;
48454850
if (isArrayBufferView(chunk)) {
4846-
// Probe detachment through the buffer before getViewInfo: a
4847-
// detached DataView's byteLength getter throws (typed arrays and
4848-
// raw buffers just report 0). A SharedArrayBuffer cannot be
4849-
// detached, and the probe rejects it as a receiver.
4850-
buffer =
4851-
TypedArrayPrototypeGetSymbolToStringTag(chunk) !== undefined
4852-
? TypedArrayPrototypeGetBuffer(chunk)
4853-
: DataViewPrototypeGetBuffer(chunk as DataView);
4854-
if (
4855-
!isSharedArrayBuffer(buffer) &&
4856-
ArrayBufferPrototypeDetachedGet(buffer)
4857-
)
4858-
continue;
4859-
const info = getViewInfo(chunk);
4860-
byteOffset = info.byteOffset;
4861-
byteLength = info.byteLength;
4851+
const extent = viewByteExtent(chunk);
4852+
buffer = extent.buffer;
4853+
byteOffset = extent.byteOffset;
4854+
byteLength = extent.byteLength;
48624855
} else if (isArrayBuffer(chunk)) {
48634856
buffer = chunk;
48644857
byteOffset = 0;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
// The byte extent of an ArrayBufferView, read through captured getters, for
4+
// code that consumes caller-supplied views (the internal transform pairs'
5+
// write paths, the digest stream, byte-counting strategies, body
6+
// collection).
7+
//
8+
// A view whose buffer has been detached, or that a resizable buffer has
9+
// shrunk out from under, has no bytes: the typed-array getters report
10+
// byteOffset and byteLength 0 for it, but the DataView getters throw a
11+
// TypeError. These helpers give a DataView the typed arrays' answer, so
12+
// such a view reads as empty whichever kind it is — as it does in the C++
13+
// implementation, which treats it as zero-length. Leaf module: requires
14+
// nothing.
15+
16+
const {
17+
DataViewPrototypeGetBuffer,
18+
DataViewPrototypeGetByteLength,
19+
DataViewPrototypeGetByteOffset,
20+
TypedArrayPrototypeGetBuffer,
21+
TypedArrayPrototypeGetByteLength,
22+
TypedArrayPrototypeGetByteOffset,
23+
TypedArrayPrototypeGetSymbolToStringTag,
24+
} = primordials;
25+
26+
interface ViewExtent {
27+
buffer: ArrayBufferLike;
28+
byteOffset: number;
29+
byteLength: number;
30+
}
31+
32+
// PRECONDITION: isArrayBufferView(view). A view without a [[TypedArrayName]]
33+
// is a DataView, whose getters brand-check the receiver; they throw only for
34+
// a detached or out-of-bounds view.
35+
function viewByteLength(view: ArrayBufferView): number {
36+
if (TypedArrayPrototypeGetSymbolToStringTag(view) !== undefined) {
37+
return TypedArrayPrototypeGetByteLength(view) as number;
38+
}
39+
try {
40+
return DataViewPrototypeGetByteLength(view as DataView) as number;
41+
} catch {
42+
return 0;
43+
}
44+
}
45+
46+
// PRECONDITION: isArrayBufferView(view). The buffer getters never throw.
47+
function viewByteExtent(view: ArrayBufferView): ViewExtent {
48+
if (TypedArrayPrototypeGetSymbolToStringTag(view) !== undefined) {
49+
return {
50+
__proto__: null,
51+
buffer: TypedArrayPrototypeGetBuffer(view),
52+
byteOffset: TypedArrayPrototypeGetByteOffset(view),
53+
byteLength: TypedArrayPrototypeGetByteLength(view),
54+
} as ViewExtent;
55+
}
56+
const dataView = view as DataView;
57+
const buffer = DataViewPrototypeGetBuffer(dataView) as ArrayBufferLike;
58+
let byteOffset = 0;
59+
let byteLength = 0;
60+
try {
61+
byteOffset = DataViewPrototypeGetByteOffset(dataView) as number;
62+
byteLength = DataViewPrototypeGetByteLength(dataView) as number;
63+
} catch {
64+
byteOffset = 0;
65+
byteLength = 0;
66+
}
67+
return { __proto__: null, buffer, byteOffset, byteLength } as ViewExtent;
68+
}
69+
70+
export type ViewExtentHelpers = {
71+
viewByteLength: typeof viewByteLength;
72+
viewByteExtent: typeof viewByteExtent;
73+
};
74+
75+
module.exports = { viewByteLength, viewByteExtent };

‎src/per_isolate/webstreams/writable.ts‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const {
2828
AbortControllerAbort,
2929
AbortControllerSignalGet,
3030
ArrayBufferPrototypeByteLengthGet,
31-
DataViewPrototypeGetByteLength,
3231
NumberIsNaN,
3332
ObjectDefineProperties,
3433
ObjectDefineProperty,
@@ -42,17 +41,11 @@ const {
4241
SymbolFor,
4342
SymbolToStringTag,
4443
TypeError,
45-
TypedArrayPrototypeGetByteLength,
4644
uncurryThis,
4745
} = primordials;
4846

49-
const {
50-
isArrayBuffer,
51-
isArrayBufferView,
52-
isDataView,
53-
isPromise,
54-
markPromiseHandled,
55-
} = utils;
47+
const { isArrayBuffer, isArrayBufferView, isPromise, markPromiseHandled } =
48+
utils;
5649

5750
// The native backend (see the fence conventions in native.ts). The cast
5851
// restores the real shape.
@@ -62,6 +55,10 @@ const { nativeStreamInternals } = require('webstreams/native') as {
6255
};
6356
const { kExtractNativeSink, isNativeUnderlyingSink } = nativeStreamInternals;
6457

58+
import type { ViewExtentHelpers } from './view-extent';
59+
const { viewByteLength } =
60+
require('webstreams/view-extent') as ViewExtentHelpers;
61+
6562
const { RingBuffer } = require('webstreams/ring-buffer') as {
6663
RingBuffer: RingBufferConstructor;
6764
};
@@ -1843,9 +1840,8 @@ function writableStreamFlush<W>(stream: WritableStream<W>): Promise<void> {
18431840
// heuristic).
18441841
function byteSizeOf(chunk: unknown): number {
18451842
if (isArrayBufferView(chunk)) {
1846-
return isDataView(chunk)
1847-
? DataViewPrototypeGetByteLength(chunk)
1848-
: TypedArrayPrototypeGetByteLength(chunk);
1843+
// A detached or out-of-bounds view counts 0 (see view-extent.ts).
1844+
return viewByteLength(chunk);
18491845
}
18501846
if (isArrayBuffer(chunk)) {
18511847
return ArrayBufferPrototypeByteLengthGet(chunk);

‎src/tests/streams/compression/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ pedantic branches shifting anything the suite pins.
178178
| `corrupt-input.js` | write-time rejection with "Decompression failed."; both-sides error; iteration rejection; bad magic bytes |
179179
| `strict-checks.js` | trailing-data write rejection; close-with-no-data rejection; truncated-member close rejection |
180180
| `chunk-types.js` | BufferSource acceptance incl. offsets; string (#1), SAB (#2), invalid-chunk message+aftermath (#3, #4) |
181-
| `buffer-lifecycle.js` | snapshot-at-write: post-write mutation/detach/shrink invisible; already-detached no-op; lying metadata getters never consulted |
181+
| `buffer-lifecycle.js` | snapshot-at-write: post-write mutation/detach/shrink invisible; already-detached no-op; detached/out-of-bounds typed-array and DataView views are no-ops; lying metadata getters never consulted |
182182
| `byob.js` | BYOB reader fills a 2-byte destination with the gzip magic |
183183
| `backpressure.js` | eager write settlement without reads; desiredSize accounting (#8) |
184184
| `propagation.js` | abort rejects pending read (reason per #9), errors both sides; cancel settles parked read (#12); write-after-abort (#10); non-Error reasons (#11); writes after a queued close reject (message per impl) without disturbing the close or output; cancel→writable aftermath (#13) |

0 commit comments

Comments
 (0)