From 9a5d60c083051862e086f5e9a37b41b4f422f533 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 4e5c88844d..d67a0ae0f9 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 f2a7b57d43..dd181cd38c 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -6532,6 +6532,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, {