From 67547d0a9d0e27e4ecd8f8de4c22296124f1af09 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Mon, 12 Jan 2026 09:51:16 -0500 Subject: [PATCH 1/2] Fix the node zlib path. We were re-wrapping the Buffer into a Uint8Array but discarding the byteOffset and byteLength, so we expanded the returned array to include garbage data. We can just return the Buffer itself, it is a Uint8Array. --- src/utils/gz.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/gz.ts b/src/utils/gz.ts index 9dcc11c1f3..60c4a7202e 100644 --- a/src/utils/gz.ts +++ b/src/utils/gz.ts @@ -43,7 +43,7 @@ export async function compress( if (errorOrNull) { reject(errorOrNull); } else { - resolve(new Uint8Array(result.buffer as ArrayBuffer)); + resolve(result); } }); }); @@ -67,7 +67,7 @@ export async function decompress( if (errorOrNull) { reject(errorOrNull); } else { - resolve(new Uint8Array(result.buffer as ArrayBuffer)); + resolve(result); } }); }); From a0150a58596f0c8a4699cdc09b27a86be6fe2e32 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 11 Jan 2026 22:54:38 +0100 Subject: [PATCH 2/2] Stop blindly extracting uint8array.buffer after calling compress(). We were assuming that the Uint8Array returned from compress() is a view of the entirety of its underlying ArrayBuffer. But that's not necessarily true. Specifically it's not true if gz.ts takes the "node" code path where it uses the node zlib module. We haven't run into this before because our jest environment has browser-like Worker APIs available, so we test the browser code path and the node path doesn't get exercised. But if I change it so that we go down the node path in the tests, then multiple tests in receive-profile.test.ts fail because we pass around compressed buffers with extra garbage bytes at the end. So this fix replaces each of those `.buffer` accesses with a potential copy if that's needed to make sure that the ArrayBuffer doesn't contain any extra data. A cleaner solution might be to stop using ArrayBuffer so much in these tests, but in some of them we simulate the response from a fetch() which definitely has an arrayBuffer() accessor and not a byteArray() accessor. --- src/test/store/receive-profile.test.ts | 40 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/test/store/receive-profile.test.ts b/src/test/store/receive-profile.test.ts index 392fb2e833..ac8d174e2b 100644 --- a/src/test/store/receive-profile.test.ts +++ b/src/test/store/receive-profile.test.ts @@ -88,6 +88,22 @@ function simulateSymbolStoreHasNoCache() { })); } +// Returns an ArrayBuffer which contains only the bytes that +// are covered by the Uint8Array, making a copy if needed. +function extractArrayBuffer(bufferView: Uint8Array): ArrayBuffer { + if ( + bufferView.byteOffset === 0 && + bufferView.byteLength === bufferView.buffer.byteLength + ) { + return bufferView.buffer; + } + + // There was extra data at the start or at the end. Make a copy. + const copy = new Uint8Array(bufferView.byteLength); + copy.set(bufferView); + return copy.buffer; +} + describe('actions/receive-profile', function () { beforeEach(() => { // The SymbolStore requires the use of IndexedDB, ensure that it exists so that @@ -676,10 +692,11 @@ describe('actions/receive-profile', function () { case 'json': return profileJSON; case 'arraybuffer': - return toUint8Array(profileJSON).buffer as ArrayBuffer; + return extractArrayBuffer(toUint8Array(profileJSON)); case 'gzip': - return (await compress(toUint8Array(profileJSON))) - .buffer as ArrayBuffer; + return extractArrayBuffer( + await compress(toUint8Array(profileJSON)) + ); default: throw new Error('unknown profiler format'); } @@ -1314,7 +1331,7 @@ describe('actions/receive-profile', function () { const profileOrZip = await _fetchProfile(args); expect(profileOrZip).toEqual({ responseType: 'PROFILE', - profile: profile.buffer, + profile: extractArrayBuffer(profile), }); } catch (error) { userFacingError = error; @@ -1446,7 +1463,7 @@ describe('actions/receive-profile', function () { const { getState, view } = await setupTestWithFile({ type: '', - payload: (await compress(serializeProfile(profile))).buffer, + payload: extractArrayBuffer(await compress(serializeProfile(profile))), }); expect(view.phase).toBe('DATA_LOADED'); expect(ProfileViewSelectors.getProfile(getState()).meta.product).toEqual( @@ -1478,7 +1495,7 @@ describe('actions/receive-profile', function () { const { getState, view } = await setupTestWithFile({ type: 'application/gzip', - payload: (await compress(serializeProfile(profile))).buffer, + payload: extractArrayBuffer(await compress(serializeProfile(profile))), }); expect(view.phase).toBe('DATA_LOADED'); expect(ProfileViewSelectors.getProfile(getState()).meta.product).toEqual( @@ -1492,7 +1509,7 @@ describe('actions/receive-profile', function () { const { getState, view } = await setupTestWithFile({ type: 'application/json', - payload: (await compress(serializeProfile(profile))).buffer, + payload: extractArrayBuffer(await compress(serializeProfile(profile))), }); expect(view.phase).toBe('DATA_LOADED'); expect(ProfileViewSelectors.getProfile(getState()).meta.product).toEqual( @@ -1506,7 +1523,7 @@ describe('actions/receive-profile', function () { .mockImplementation(() => {}); const { view } = await setupTestWithFile({ type: 'application/gzip', - payload: (await compress('{}')).buffer, + payload: extractArrayBuffer(await compress('{}')), }); expect(view.phase).toBe('FATAL_ERROR'); @@ -1525,14 +1542,9 @@ describe('actions/receive-profile', function () { zip.file(fileName, serializedProfile); const array = await zip.generateAsync({ type: 'uint8array' }); - // Create a new ArrayBuffer instance and copy the data into it, in order - // to work around https://github.com/facebook/jest/issues/6248 - const bufferCopy = new ArrayBuffer(array.buffer.byteLength); - new Uint8Array(bufferCopy).set(new Uint8Array(array.buffer)); - return setupTestWithFile({ type: 'application/zip', - payload: bufferCopy, + payload: extractArrayBuffer(array as Uint8Array), }); }