diff --git a/src/logger.ts b/src/logger.ts index d4be5d70..515fa18e 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -163,7 +163,15 @@ function processData(data: Uint8Array | string, encoding?: BufferEncoding) { let decodedData; try { if (data instanceof Uint8Array) { - decodedData = Buffer.from(data.buffer).toString(); + // Decode only the bytes the view actually spans. A Uint8Array (e.g. a + // Buffer carved from Node's shared pool) is often a view into a larger + // ArrayBuffer, so reading `data.buffer` directly would include unrelated + // bytes outside the view's byteOffset/byteLength range. + decodedData = Buffer.from( + data.buffer, + data.byteOffset, + data.byteLength, + ).toString(); } else { decodedData = Buffer.from(data, encoding).toString(); } diff --git a/test/logger.ts b/test/logger.ts index 86a7d0ed..4b9f3f06 100644 --- a/test/logger.ts +++ b/test/logger.ts @@ -147,6 +147,18 @@ describe('getModifiedData', () => { assert.equal(modifiedData, expectedTextOutput); }); + it('uint8Array that is a view into a larger (pooled) buffer', () => { + // Node.js allocates most Buffers from a shared internal pool, so a Buffer + // written to stdout/stderr is frequently a view with a non-zero byteOffset + // into a much larger ArrayBuffer. The logger must honor the view's + // byteOffset/byteLength rather than decoding the entire backing buffer. + const pool = Buffer.alloc(64, 0x2e /* '.' */); + const view = pool.subarray(8, 8 + sampleText.length); + view.write(sampleText); + const modifiedData = getModifiedData(view); + assert.equal(modifiedData, expectedTextOutput); + }); + it('simple text with error', () => { const modifiedData = getModifiedData(sampleText, undefined, true); const expectedOutput =