From fe422acbf4fbc2093ebd4805ee8667991ab8a58a Mon Sep 17 00:00:00 2001 From: Michael Vandesompel Date: Thu, 8 Oct 2026 21:47:52 +0200 Subject: [PATCH] Reject a binary file transfer whose byte count differs from FileBegin.size concatByteChunks allocated FileBegin.size bytes and copied the chunks in: a short transfer resolved as a zero-padded file, and a long one threw a RangeError inside the transport callback and left the request pending. Count the received bytes at FileEnd and reject the read when they differ from the announced size. --- packages/client/src/daemon-client.test.ts | 34 +++++++++++++++++++++++ packages/client/src/daemon-client.ts | 12 ++++++++ 2 files changed, 46 insertions(+) diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 85c9fee29f..c02e1720cb 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -2863,6 +2863,40 @@ test("readFile with timeout 0 outlives the default request timeout", async () => expect(new TextDecoder().decode(result.bytes)).toBe("hello"); }); +test("readFile rejects a binary transfer that ends before the announced size", async () => { + const { mock, client } = await connectFileClient("clsk_file_short"); + + const responsePromise = client.readFile("/tmp/project", "short.bin", "req-short"); + sendBinaryFile(mock, "req-short", 10, ["hello"]); + mock.triggerMessage( + encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-short" }), + ); + + await expect(responsePromise).rejects.toThrow( + "File transfer incomplete: expected 10 bytes, received 5.", + ); +}); + +test("readFile rejects a binary transfer that exceeds the announced size", async () => { + const { mock, client } = await connectFileClient("clsk_file_long"); + + const responsePromise = client.readFile("/tmp/project", "long.bin", "req-long"); + const settled = responsePromise.then( + () => null, + (error: unknown) => error, + ); + sendBinaryFile(mock, "req-long", 4, ["hello!"]); + + expect(() => + mock.triggerMessage( + encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-long" }), + ), + ).not.toThrow(); + expect(await settled).toMatchObject({ + message: "File transfer incomplete: expected 4 bytes, received 6.", + }); +}); + test("uploadFile sends metadata request and file bytes as binary chunks", async () => { const logger = createMockLogger(); const mock = createMockTransport(); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index e717b709de..1aa0d3e995 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -6549,6 +6549,18 @@ export class DaemonClient { return; } + const received = transfer.chunks.reduce((total, chunk) => total + chunk.byteLength, 0); + if (received !== transfer.size) { + this.activeBinaryFileTransfers.delete(frame.requestId); + this.rejectWaitersForRequestId( + frame.requestId, + new Error( + `File transfer incomplete: expected ${transfer.size} bytes, received ${received}.`, + ), + ); + return; + } + const bytes = concatByteChunks(transfer.chunks, transfer.size); this.activeBinaryFileTransfers.delete(frame.requestId); this.completedBinaryFileReads.set(frame.requestId, {