diff --git a/src/process/BoundedFileBytes.ts b/src/process/BoundedFileBytes.ts index a33d17c052..d42725b0ee 100644 --- a/src/process/BoundedFileBytes.ts +++ b/src/process/BoundedFileBytes.ts @@ -41,6 +41,7 @@ export const readBoundedFileBytes = async ( expectedSize > maxBytes) ) throw new RangeError("Expected file size must fit the byte limit"); + assertActive(); if (expectedSize === undefined) { const chunks: Buffer[] = []; const buffer = Buffer.allocUnsafe(Math.min(64 * 1_024, maxBytes + 1)); @@ -52,6 +53,7 @@ export const readBoundedFileBytes = async ( 0, Math.min(buffer.length, maxBytes - total + 1), ); + assertActive(); if (bytesRead === 0) return Buffer.concat(chunks, total); total += bytesRead; if (total > maxBytes) return undefined; @@ -64,6 +66,7 @@ export const readBoundedFileBytes = async ( for (;;) { assertActive(); const { bytesRead } = await read(buffer, total, expectedSize - total); + assertActive(); if (bytesRead === 0) return buffer.subarray(0, total); total += bytesRead; if (total > maxBytes) return undefined; diff --git a/tests/boundary/filesystem/boundedFileBytes.test.ts b/tests/boundary/filesystem/boundedFileBytes.test.ts index 8a753eb2b2..f2813adf19 100644 --- a/tests/boundary/filesystem/boundedFileBytes.test.ts +++ b/tests/boundary/filesystem/boundedFileBytes.test.ts @@ -26,7 +26,7 @@ describe("bounded file byte reads", () => { }, ); - it.each([undefined, 5])( + it.each([undefined, 0, 5])( "preserves raw reasons or caller-owned cancellation with expected size %s", async (expectedSize) => { const root = await createTestTempDirectory("rea-bounded-cancel-"); @@ -61,6 +61,48 @@ describe("bounded file byte reads", () => { }, ); + it.each([ + { contents: "", maxBytes: 0, expectedSize: undefined, outcome: "EOF" }, + { contents: "", maxBytes: 5, expectedSize: 5, outcome: "early EOF" }, + { + contents: "hello", + maxBytes: 5, + expectedSize: 5, + outcome: "expected size", + }, + { + contents: "hello", + maxBytes: 4, + expectedSize: undefined, + outcome: "overflow", + }, + ])( + "honors cancellation while the $outcome read is in flight", + async ({ contents, maxBytes, expectedSize }) => { + const root = await createTestTempDirectory("rea-bounded-cancel-read-"); + const path = join(root, "input"); + await writeFile(path, contents); + const handle = await open(path, "r"); + const controller = new AbortController(); + const reason = new Error("cancelled during read"); + const typed = new Error("typed cancellation", { cause: reason }); + try { + const result = readBoundedFileBytes(handle, maxBytes, { + signal: controller.signal, + expectedSize, + cancellationFailure: (observed) => { + expect(observed).toBe(reason); + return typed; + }, + }); + controller.abort(reason); + await expect(result).rejects.toBe(typed); + } finally { + await handle.close(); + } + }, + ); + it("stops after one overflow byte when a file grows beyond its admitted size", async () => { const root = await createTestTempDirectory("rea-growing-bytes-"); const path = join(root, "input");