Skip to content
File

Blob: src/workerd/api/streams/identitytransformstream-byob-test.js

javascript212 lines
1// Copyright (c) 2024 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import { strictEqual, deepStrictEqual } from 'node:assert';
6 
7// This is testing non-standard behavior that we support for historical reasons. While the spec
8// dictates that BYOB buffers should be detached when passed to a pending read, historically we've
9// not done so (there's a compat flag to enable that behavior). This means the read/write buffers
10// can be modified after being passed in, including being detached or resized. When a buffer is
11// detached or resized, we need to ensure that the BYOB read still resolves correctly and doesn't
12// end with a v8 assert or produce invalid data.
13 
14// Test that transferring the buffer during a pending BYOB read resolves with a zero-length result
15export const transferBuffer = {
16 async test() {
17 const ts = new IdentityTransformStream();
18 const reader = ts.readable.getReader({ mode: 'byob' });
19 const writer = ts.writable.getWriter();
20 
21 const buffer = new ArrayBuffer(4096);
22 const readPromise = reader.read(new Uint8Array(buffer));
23 buffer.transferToFixedLength(64);
24 
25 await writer.write(new Uint8Array(1337));
26 
27 // When the buffer is detached, the read resolves with a new zero-length ArrayBuffer
28 // (the data that was read is lost)
29 const result = await readPromise;
30 strictEqual(result.done, false);
31 strictEqual(result.value.byteLength, 0);
32 // The buffer should be a new ArrayBuffer, not the original detached one
33 strictEqual(result.value.buffer.byteLength, 0);
34 },
35};
36 
37// Test that resizing a resizable buffer smaller during a pending BYOB read returns truncated data
38export const resizeBufferSmaller = {
39 async test() {
40 const ts = new IdentityTransformStream();
41 const reader = ts.readable.getReader({ mode: 'byob' });
42 const writer = ts.writable.getWriter();
43 
44 const buffer = new ArrayBuffer(4096, { maxByteLength: 8192 });
45 const readPromise = reader.read(new Uint8Array(buffer));
46 buffer.resize(64);
47 
48 // Write data that would exceed the resized buffer
49 const writeData = new Uint8Array(1337);
50 for (let i = 0; i < writeData.length; i++) {
51 writeData[i] = i % 256;
52 }
53 await writer.write(writeData);
54 
55 // When the buffer is resized smaller, we get truncated data
56 // (only the bytes that fit in the new size)
57 const result = await readPromise;
58 strictEqual(result.done, false);
59 strictEqual(result.value.byteLength, 64);
60 // The underlying ArrayBuffer should be the resized buffer
61 strictEqual(result.value.buffer.byteLength, 64);
62 // Verify the truncated data matches the first 64 bytes written
63 deepStrictEqual(
64 Array.from(result.value),
65 Array.from(writeData.slice(0, 64))
66 );
67 },
68};
69 
70// Test that resizing a resizable buffer larger during a pending BYOB read works correctly
71export const resizeBufferLarger = {
72 async test() {
73 const ts = new IdentityTransformStream();
74 const reader = ts.readable.getReader({ mode: 'byob' });
75 const writer = ts.writable.getWriter();
76 
77 const buffer = new ArrayBuffer(4096, { maxByteLength: 8192 });
78 const readPromise = reader.read(new Uint8Array(buffer));
79 buffer.resize(8192);
80 
81 // Write some data
82 const writeData = new Uint8Array(100);
83 for (let i = 0; i < writeData.length; i++) {
84 writeData[i] = i;
85 }
86 await writer.write(writeData);
87 
88 const result = await readPromise;
89 strictEqual(result.done, false);
90 strictEqual(result.value.byteLength, 100);
91 // The underlying ArrayBuffer should be the resized buffer
92 strictEqual(result.value.buffer.byteLength, 8192);
93 
94 // Verify the data matches what was written
95 deepStrictEqual(Array.from(result.value), Array.from(writeData));
96 
97 // Verify the remaining bytes in the underlying ArrayBuffer are zeroes
98 const fullBuffer = new Uint8Array(result.value.buffer);
99 strictEqual(fullBuffer.byteLength, 8192);
100 for (let i = 100; i < fullBuffer.byteLength; i++) {
101 strictEqual(
102 fullBuffer[i],
103 0,
104 `Expected byte at index ${i} to be 0, got ${fullBuffer[i]}`
105 );
106 }
107 },
108};
109 
110// Test that resizing a write buffer smaller after queuing doesn't produce invalid data
111export const writeBufferResizedSmaller = {
112 async test() {
113 const { writable, readable } = new IdentityTransformStream();
114 const writer = writable.getWriter();
115 const reader = readable.getReader();
116 
117 const buf1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
118 const buf2 = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 }));
119 buf2.set([11, 12, 13, 14, 15, 16, 17, 18]);
120 
121 const p1 = writer.write(buf1);
122 const p2 = writer.write(buf2);
123 // Resize smaller after queuing. The write copies the BackingStore at
124 // time of write
125 buf2.buffer.resize(6);
126 
127 const r1 = await reader.read();
128 const r2 = await reader.read();
129 
130 await p1;
131 await p2;
132 
133 // First write should be unaffected
134 strictEqual(r1.done, false);
135 deepStrictEqual(Array.from(r1.value), [1, 2, 3, 4, 5, 6, 7, 8]);
136 
137 // Second write - uses original view size but bytes beyond the resized buffer are zeroed
138 strictEqual(r2.done, false);
139 deepStrictEqual(Array.from(r2.value), [11, 12, 13, 14, 15, 16, 17, 18]);
140 },
141};
142 
143// Test that resizing a write buffer larger after queuing doesn't produce invalid data
144export const writeBufferResizedLarger = {
145 async test() {
146 const { writable, readable } = new IdentityTransformStream();
147 const writer = writable.getWriter();
148 const reader = readable.getReader();
149 
150 const buf1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
151 const buf2 = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 }));
152 buf2.set([11, 12, 13, 14, 15, 16, 17, 18]);
153 
154 const p1 = writer.write(buf1);
155 const p2 = writer.write(buf2);
156 buf2.buffer.resize(12); // Resize larger after queuing
157 
158 const r1 = await reader.read();
159 const r2 = await reader.read();
160 
161 await p1;
162 await p2;
163 
164 // First write should be unaffected
165 strictEqual(r1.done, false);
166 deepStrictEqual(Array.from(r1.value), [1, 2, 3, 4, 5, 6, 7, 8]);
167 
168 // Second write - should contain original 8 bytes (view size at time of write)
169 strictEqual(r2.done, false);
170 deepStrictEqual(Array.from(r2.value), [11, 12, 13, 14, 15, 16, 17, 18]);
171 },
172};
173 
174// Test that transferring a write buffer after queuing still writes correctly (BackingStore
175// is captured at time of write, so transfer doesn't affect the queued write)
176export const writeBufferTransferred = {
177 async test() {
178 const { writable, readable } = new IdentityTransformStream();
179 const writer = writable.getWriter();
180 const reader = readable.getReader();
181 
182 const buf1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
183 const buf2 = new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]);
184 const buf3 = new Uint8Array([17, 18, 19, 20, 21, 22, 23, 24]);
185 const buf4 = new Uint8Array([25, 26, 27, 28, 29, 30, 31, 32]);
186 
187 // Do not await the writes so they are queued up before the transfer
188 writer.write(buf1);
189 writer.write(buf2);
190 writer.write(buf3);
191 writer.write(buf4);
192 
193 buf3.buffer.transfer();
194 
195 const r1 = await reader.read();
196 const r2 = await reader.read();
197 const r3 = await reader.read();
198 const r4 = await reader.read();
199 
200 // All reads should produce the original data since the transferred buffer's data is
201 // captured at time of write
202 strictEqual(r1.done, false);
203 deepStrictEqual(Array.from(r1.value), [1, 2, 3, 4, 5, 6, 7, 8]);
204 strictEqual(r2.done, false);
205 deepStrictEqual(Array.from(r2.value), [9, 10, 11, 12, 13, 14, 15, 16]);
206 strictEqual(r3.done, false);
207 deepStrictEqual(Array.from(r3.value), [17, 18, 19, 20, 21, 22, 23, 24]);
208 strictEqual(r4.done, false);
209 deepStrictEqual(Array.from(r4.value), [25, 26, 27, 28, 29, 30, 31, 32]);
210 },
211};