From 0950a50ec0a94f9797814584e3c78b5bf6652cea Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Wed, 11 Nov 2026 16:32:44 +0530 Subject: [PATCH 1/2] perf(web): skip oversized TAR preview payload copies --- .../files/binaryPreview.worker.test.ts | 99 +++++++++++++++++++ .../components/files/binaryPreview.worker.ts | 8 +- 2 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/files/binaryPreview.worker.test.ts diff --git a/apps/web/src/components/files/binaryPreview.worker.test.ts b/apps/web/src/components/files/binaryPreview.worker.test.ts new file mode 100644 index 0000000000000..e551891ec20be --- /dev/null +++ b/apps/web/src/components/files/binaryPreview.worker.test.ts @@ -0,0 +1,99 @@ +import { expect, it, vi } from "vite-plus/test"; + +import { + ARCHIVE_ENTRY_MAX_BYTES, + type BinaryWorkerRequest, + type BinaryWorkerResult, +} from "./binaryPreviewProtocol"; + +it.each([ARCHIVE_ENTRY_MAX_BYTES, ARCHIVE_ENTRY_MAX_BYTES + 1, 40 * 1024 * 1024])( + "only copies previewable TAR payloads with a %i-byte entry", + async (size) => { + const encode = (value: string) => new TextEncoder().encode(value); + const entries = [ + { path: "before.txt", bytes: encode("before") }, + { path: "large.txt", bytes: new Uint8Array(size).fill(97) }, + { path: "empty.txt", bytes: new Uint8Array() }, + { path: "after.txt", bytes: encode("after") }, + ]; + const bytes = new Uint8Array( + entries.reduce( + (length, entry) => length + 512 + Math.ceil(entry.bytes.length / 512) * 512, + 1024, + ), + ); + let offset = 0; + for (const entry of entries) { + const write = (position: number, value: string) => + bytes.set(encode(value), offset + position); + write(0, entry.path); + write(124, entry.bytes.length.toString(8).padStart(11, "0") + "\0"); + write(136, "00000000000\0"); + bytes.fill(32, offset + 148, offset + 156); + write(156, "0"); + write( + 148, + bytes + .subarray(offset, offset + 512) + .reduce((sum, byte) => sum + byte, 0) + .toString(8) + .padStart(6, "0") + "\0 ", + ); + bytes.set(entry.bytes, offset + 512); + offset += 512 + Math.ceil(entry.bytes.length / 512) * 512; + } + + const worker = new EventTarget(); + let receive = (_result: BinaryWorkerResult) => {}; + vi.stubGlobal( + "self", + Object.assign(worker, { postMessage: (result: BinaryWorkerResult) => receive(result) }), + ); + try { + vi.resetModules(); + await import("./binaryPreview.worker"); + const request = (input: BinaryWorkerRequest) => + new Promise((resolve) => { + receive = resolve; + worker.dispatchEvent(new MessageEvent("message", { data: input })); + }); + using copies = vi.spyOn(Uint8Array.prototype, "slice"); + expect( + await request({ type: "open", kind: "archive", name: "mixed.tar", bytes: bytes.buffer }), + ).toEqual({ + type: "archive", + entries: entries.map((entry) => ({ + path: entry.path, + size: entry.bytes.length, + directory: false, + modified: "1970-01-01T00:00:00.000Z", + })), + }); + const copiedBytes = copies.mock.results.reduce( + (total, result) => total + (result.type === "return" ? result.value.byteLength : 0), + 0, + ); + expect(copiedBytes).toBe(11 + (size <= ARCHIVE_ENTRY_MAX_BYTES ? size : 0)); + for (const entry of entries) { + const result = await request({ type: "entry", path: entry.path }); + if (entry.bytes.length > ARCHIVE_ENTRY_MAX_BYTES) { + expect(result).toEqual({ + type: "error", + message: "Entry previews are limited to 10 MB.", + }); + } else { + if (result.type !== "entry") throw new Error("Expected entry"); + expect(result.path).toBe(entry.path); + expect(result.bytes.length).toBe(entry.bytes.length); + expect(result.bytes.every((byte, index) => byte === entry.bytes[index])).toBe(true); + } + } + expect(await request({ type: "entry", path: "missing.txt" })).toEqual({ + type: "error", + message: "This archive entry is unavailable.", + }); + } finally { + vi.unstubAllGlobals(); + } + }, +); diff --git a/apps/web/src/components/files/binaryPreview.worker.ts b/apps/web/src/components/files/binaryPreview.worker.ts index c0f3c2a17410f..0dee9607ec2f4 100644 --- a/apps/web/src/components/files/binaryPreview.worker.ts +++ b/apps/web/src/components/files/binaryPreview.worker.ts @@ -14,7 +14,7 @@ import { let database: Database | null = null; let tables: string[] = []; -let files = new Map>(); +let files = new Map | null>(); const reply = (result: BinaryWorkerResult) => self.postMessage(result, { transfer: [] }); const quote = (name: string) => `"${name.replaceAll('"', '""')}"`; @@ -130,7 +130,7 @@ function tarEntries(bytes: Uint8Array) { directory: type === "5", modified: Number.isNaN(date.getTime()) ? null : date.toISOString(), }); - if (type !== "5") files.set(path, content.slice()); + if (type !== "5") files.set(path, size > ARCHIVE_ENTRY_MAX_BYTES ? null : content.slice()); } offset += 512 + Math.ceil(size / 512) * 512; } @@ -141,8 +141,8 @@ function tarEntries(bytes: Uint8Array) { async function handle(input: BinaryWorkerRequest) { if (input.type === "entry") { const bytes = files.get(input.path); - if (!bytes) throw new Error("This archive entry is unavailable."); - if (bytes.length > ARCHIVE_ENTRY_MAX_BYTES) + if (bytes === undefined) throw new Error("This archive entry is unavailable."); + if (bytes === null || bytes.length > ARCHIVE_ENTRY_MAX_BYTES) throw new Error("Entry previews are limited to 10 MB."); reply({ type: "entry", path: input.path, bytes }); return; From 8c492c457a186f61aeab3ad1c9c07799f65ff45f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 19:15:56 +0000 Subject: [PATCH 2/2] test(web): drop redundant 40 MiB TAR preview case The over-limit case already covers the skipped copy; the 40 MiB archive only added allocation time. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/files/binaryPreview.worker.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/files/binaryPreview.worker.test.ts b/apps/web/src/components/files/binaryPreview.worker.test.ts index e551891ec20be..7f03183be4bf2 100644 --- a/apps/web/src/components/files/binaryPreview.worker.test.ts +++ b/apps/web/src/components/files/binaryPreview.worker.test.ts @@ -6,7 +6,7 @@ import { type BinaryWorkerResult, } from "./binaryPreviewProtocol"; -it.each([ARCHIVE_ENTRY_MAX_BYTES, ARCHIVE_ENTRY_MAX_BYTES + 1, 40 * 1024 * 1024])( +it.each([ARCHIVE_ENTRY_MAX_BYTES, ARCHIVE_ENTRY_MAX_BYTES + 1])( "only copies previewable TAR payloads with a %i-byte entry", async (size) => { const encode = (value: string) => new TextEncoder().encode(value);