diff --git a/loopx/control_plane/collaboration/inbox_read.ts b/loopx/control_plane/collaboration/inbox_read.ts index cace31ba76..ada656aed9 100644 --- a/loopx/control_plane/collaboration/inbox_read.ts +++ b/loopx/control_plane/collaboration/inbox_read.ts @@ -55,11 +55,21 @@ function readRecord(path: string): JsonObject { try { const stat = fstatSync(fd); if (!stat.isFile()) fail("invalid manager context record"); - if (stat.size > 128_000) fail("manager context record too large"); - const buffer = Buffer.alloc(128_001); + const limit = 128_000; + if (stat.size > limit) fail("manager context record too large"); + // Most receipts are small. Read one byte past the observed size so an + // in-place append still gets the same bounded read through EOF. + let buffer = Buffer.alloc(stat.size + 1); let size = 0, count: number; - while (size < buffer.length && (count = readSync(fd, buffer, size, buffer.length - size, null)) > 0) size += count; - if (size > 128_000) fail("manager context record too large"); + while ((count = readSync(fd, buffer, size, buffer.length - size, null)) > 0) { + size += count; + if (size > limit) fail("manager context record too large"); + if (size === buffer.length) { + const expanded = Buffer.alloc(limit + 1); + buffer.copy(expanded); + buffer = expanded; + } + } const bytes = buffer.subarray(0, size); let parsed: unknown; try { diff --git a/tests/control_plane_ts/inbox_read_transaction.test.ts b/tests/control_plane_ts/inbox_read_transaction.test.ts index c9ea35acf2..64023d378e 100644 --- a/tests/control_plane_ts/inbox_read_transaction.test.ts +++ b/tests/control_plane_ts/inbox_read_transaction.test.ts @@ -141,6 +141,15 @@ for (const bytes of [Buffer.from("\ufeff{}"), Buffer.from([0xff]), Buffer.alloc( }); } +test("a valid record at the byte limit retains its complete content", async t => { + const f = fixture(t), request = f.publish(1); + const path = join(f.folder, request.request_id + ".json"); + const original = readFileSync(path); + writeFileSync(path, Buffer.concat([original, Buffer.alloc(128_000 - original.length, 32)])); + const result = (await f.call("pending", {request_id: request.request_id})).result as JsonObject; + assert.equal((result.items as JsonObject[])[0].message, request.message); +}); + test("malformed receipts retain the request and recovery advice", async t => { const f = fixture(t), request = f.publish(1); mkdirSync(join(f.base, "decisions"));