From fff66e1ea21e977c60e51269337d795b9bffe9f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:11:07 +0000 Subject: [PATCH 1/4] fix: apply audit fixes --- dist/index.cjs | 450 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 292 insertions(+), 158 deletions(-) diff --git a/dist/index.cjs b/dist/index.cjs index 883c8c4..c0c721e 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -100121,7 +100121,7 @@ async function connectH1$1 (client, socket) { function clearIdleSocketValidation (socket) { if (socket[kIdleSocketValidationTimeout]) { - clearTimeout(socket[kIdleSocketValidationTimeout]); + clearImmediate(socket[kIdleSocketValidationTimeout]); socket[kIdleSocketValidationTimeout] = null; } @@ -100130,15 +100130,23 @@ function clearIdleSocketValidation (socket) { function scheduleIdleSocketValidation (client, socket) { socket[kIdleSocketValidation] = 1; - socket[kIdleSocketValidationTimeout] = setTimeout(() => { + // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST + // already pending on this idle keep-alive socket are processed before the + // next request is written (GHSA-35p6-xmwp-9g52). + // + // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse + // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll + // block for ~500ms when the event loop is otherwise idle (#5600 / #5606). + // A ref'd Immediate both keeps the pending request alive and makes poll + // return immediately — the hybrid those issues asked for. + socket[kIdleSocketValidationTimeout] = setImmediate(() => { socket[kIdleSocketValidationTimeout] = null; socket[kIdleSocketValidation] = 2; if (client[kSocket$1] === socket && !socket.destroyed) { client[kResume$3](); } - }, 0); - socket[kIdleSocketValidationTimeout].unref?.(); + }); } /** @@ -103643,6 +103651,7 @@ let RetryHandler$3 = class RetryHandler { this.end = null; this.etag = null; this.resume = null; + this.headersSent = false; // Handle possible onConnect duplication this.handler.onConnect(reason => { @@ -103655,6 +103664,20 @@ let RetryHandler$3 = class RetryHandler { }); } + checkpointResponseEnd (headers, resume) { + if (this.end == null && this.opts.method !== 'HEAD') { + const contentLength = headers['content-length']; + this.end = contentLength != null ? Number(contentLength) - 1 : null; + + assert$7( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ); + } + + this.resume = this.end != null ? resume : null; + } + onRequestSent () { if (this.handler.onRequestSent) { this.handler.onRequestSent(); @@ -103744,6 +103767,8 @@ let RetryHandler$3 = class RetryHandler { if (statusCode >= 300) { if (this.retryOpts.statusCodes.includes(statusCode) === false) { + this.headersSent = true; + this.checkpointResponseEnd(headers, resume); return this.handler.onHeaders( statusCode, rawHeaders, @@ -103812,8 +103837,15 @@ let RetryHandler$3 = class RetryHandler { const { start, size, end = size - 1 } = contentRange; - assert$7(this.start === start, 'content-range mismatch'); - assert$7(this.end == null || this.end === end, 'content-range mismatch'); + if (this.start !== start || (this.end != null && this.end !== end)) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false + } this.resume = resume; return true @@ -103825,6 +103857,7 @@ let RetryHandler$3 = class RetryHandler { const range = parseRangeHeader(headers['content-range']); if (range == null) { + this.headersSent = true; return this.handler.onHeaders( statusCode, rawHeaders, @@ -103863,6 +103896,7 @@ let RetryHandler$3 = class RetryHandler { ); this.resume = resume; + this.headersSent = true; this.etag = headers.etag != null ? headers.etag : null; // Weak etags are not useful for comparison nor cache @@ -103902,7 +103936,7 @@ let RetryHandler$3 = class RetryHandler { } onError (err) { - if (this.aborted || isDisturbed(this.opts.body)) { + if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) { return this.handler.onError(err) } @@ -115834,7 +115868,7 @@ function requireConnection () { // is specified, the server needs to include the same field and one of // the selected subprotocol values in its response for the connection to // be established. - if (!requestProtocols.includes(secProtocol)) { + if (requestProtocols === null || !requestProtocols.includes(secProtocol)) { failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.'); return } @@ -116081,7 +116115,12 @@ function requirePermessageDeflate () { if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { callback(new MessageSizeExceededError()); + // The inflater may still hold buffered input that can emit a late + // zlib error. Remove the data listener, then deterministically stop + // the stream so a subsequent 'error' cannot fire without a listener + // (which would terminate the process as an unhandled error event). this.#inflate.removeAllListeners(); + this.#inflate.destroy(); this.#inflate = null; return } @@ -117430,6 +117469,49 @@ function requireEventsourceStream () { */ const SPACE = 0x20; + const DATA = Buffer.from('data'); + const EVENT = Buffer.from('event'); + const ID = Buffer.from('id'); + const RETRY = Buffer.from('retry'); + + function isASCIINumberBytes (buffer, start) { + if (start >= buffer.length) { + return false + } + + for (let i = start; i < buffer.length; i++) { + if (buffer[i] < 0x30 || buffer[i] > 0x39) { + return false + } + } + + return true + } + + function isValidLastEventIdBytes (buffer, start) { + for (let i = start; i < buffer.length; i++) { + if (buffer[i] === 0x00) { + return false + } + } + + return true + } + + function isFieldName (line, length, field) { + if (length !== field.length) { + return false + } + + for (let i = 0; i < length; i++) { + if (line[i] !== field[i]) { + return false + } + } + + return true + } + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -117470,11 +117552,14 @@ function requireEventsourceStream () { eventEndCheck = false /** - * @type {Buffer} + * @type {Buffer[]} */ - buffer = null + chunks = [] + chunkIndex = 0 pos = 0 + lineChunkIndex = 0 + linePos = 0 event = { data: undefined, @@ -117513,92 +117598,20 @@ function requireEventsourceStream () { return } - // Cache the chunk in the buffer, as the data might not be complete while - // processing it - // TODO: Investigate if there is a more performant way to handle - // incoming chunks - // see: https://github.com/nodejs/undici/issues/2630 - if (this.buffer) { - this.buffer = Buffer.concat([this.buffer, chunk]); - } else { - this.buffer = chunk; - } + this.chunks.push(chunk); // Strip leading byte-order-mark if we opened the stream and started // the processing of the incoming data if (this.checkBOM) { - switch (this.buffer.length) { - case 1: - // Check if the first byte is the same as the first byte of the BOM - if (this.buffer[0] === BOM[0]) { - // If it is, we need to wait for more data - callback(); - return - } - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - - // The buffer only contains one byte so we need to wait for more data - callback(); - return - case 2: - // Check if the first two bytes are the same as the first two bytes - // of the BOM - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] - ) { - // If it is, we need to wait for more data, because the third byte - // is needed to determine if it is the BOM or not - callback(); - return - } - - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - break - case 3: - // Check if the first three bytes are the same as the first three - // bytes of the BOM - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] && - this.buffer[2] === BOM[2] - ) { - // If it is, we can drop the buffered data, as it is only the BOM - this.buffer = Buffer.alloc(0); - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - - // Await more data - callback(); - return - } - // If it is not the BOM, we can start processing the data - this.checkBOM = false; - break - default: - // The buffer is longer than 3 bytes, so we can drop the BOM if it is - // present - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] && - this.buffer[2] === BOM[2] - ) { - // Remove the BOM from the buffer - this.buffer = this.buffer.subarray(3); - } - - // Set the checkBOM flag to false as we don't need to check for the - this.checkBOM = false; - break + if (this.handleBOM()) { + callback(); + return } } - while (this.pos < this.buffer.length) { + while (this.hasCurrentByte()) { + const byte = this.currentByte(); + // If the previous line ended with an end-of-line, we need to check // if the next character is also an end-of-line. if (this.eventEndCheck) { @@ -117611,10 +117624,9 @@ function requireEventsourceStream () { if (this.crlfCheck) { // If the current character is a line feed, we can remove it // from the buffer and reset the crlfCheck flag - if (this.buffer[this.pos] === LF) { - this.buffer = this.buffer.subarray(this.pos + 1); - this.pos = 0; + if (byte === LF) { this.crlfCheck = false; + this.consumeCurrentByte(); // It is possible that the line feed is not the end of the // event. We need to check if the next character is an @@ -117630,19 +117642,17 @@ function requireEventsourceStream () { this.crlfCheck = false; } - if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (byte === LF || byte === CR) { // If the current character is a carriage return, we need to // set the crlfCheck flag to true, as we need to check if the // next character is a line feed so we can remove it from the // buffer - if (this.buffer[this.pos] === CR) { + if (byte === CR) { this.crlfCheck = true; } - this.buffer = this.buffer.subarray(this.pos + 1); - this.pos = 0; - if ( - this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) { + this.consumeCurrentByte(); + if (this.hasPendingEvent()) { this.processEvent(this.event); } this.clearEvent(); @@ -117656,22 +117666,18 @@ function requireEventsourceStream () { // If the current character is an end-of-line, we can process the // line - if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (byte === LF || byte === CR) { // If the current character is a carriage return, we need to // set the crlfCheck flag to true, as we need to check if the // next character is a line feed - if (this.buffer[this.pos] === CR) { + if (byte === CR) { this.crlfCheck = true; } // In any case, we can process the line as we reached an // end-of-line character - this.parseLine(this.buffer.subarray(0, this.pos), this.event); - - // Remove the processed line from the buffer - this.buffer = this.buffer.subarray(this.pos + 1); - // Reset the position as we removed the processed line from the buffer - this.pos = 0; + this.parseLine(this.readLine(), this.event); + this.consumeCurrentByte(); // A line was processed and this could be the end of the event. We need // to check if the next line is empty to determine if the event is // finished. @@ -117679,7 +117685,7 @@ function requireEventsourceStream () { continue } - this.pos++; + this.advanceCursor(); } callback(); @@ -117704,64 +117710,53 @@ function requireEventsourceStream () { return } - let field = ''; - let value = ''; + let fieldLength = line.length; + let valueStart = line.length; // If the line contains a U+003A COLON character (:) if (colonPosition !== -1) { - // Collect the characters on the line before the first U+003A COLON - // character (:), and let field be that string. - // TODO: Investigate if there is a more performant way to extract the - // field - // see: https://github.com/nodejs/undici/issues/2630 - field = line.subarray(0, colonPosition).toString('utf8'); + fieldLength = colonPosition; // Collect the characters on the line after the first U+003A COLON // character (:), and let value be that string. // If value starts with a U+0020 SPACE character, remove it from value. - let valueStart = colonPosition + 1; + valueStart = colonPosition + 1; if (line[valueStart] === SPACE) { ++valueStart; } - // TODO: Investigate if there is a more performant way to extract the - // value - // see: https://github.com/nodejs/undici/issues/2630 - value = line.subarray(valueStart).toString('utf8'); + } - // Otherwise, the string is not empty but does not contain a U+003A COLON - // character (:) - } else { - // Process the field using the steps described below, using the whole - // line as the field name, and the empty string as the field value. - field = line.toString('utf8'); - value = ''; + if (isFieldName(line, fieldLength, DATA)) { + const value = line.toString('utf8', valueStart); + + if (event.data === undefined) { + event.data = value; + } else { + event.data += `\n${value}`; + } + return } - // Modify the event with the field name and value. The value is also - // decoded as UTF-8 - switch (field) { - case 'data': - if (event[field] === undefined) { - event[field] = value; - } else { - event[field] += `\n${value}`; - } - break - case 'retry': - if (isASCIINumber(value)) { - event[field] = value; - } - break - case 'id': - if (isValidLastEventId(value)) { - event[field] = value; - } - break - case 'event': - if (value.length > 0) { - event[field] = value; - } - break + if (isFieldName(line, fieldLength, RETRY)) { + if (isASCIINumberBytes(line, valueStart)) { + event.retry = line.toString('utf8', valueStart); + } + return + } + + if (isFieldName(line, fieldLength, ID)) { + if (isValidLastEventIdBytes(line, valueStart)) { + event.id = line.toString('utf8', valueStart); + } + return + } + + if (isFieldName(line, fieldLength, EVENT)) { + const value = line.toString('utf8', valueStart); + + if (value.length > 0) { + event.event = value; + } } } @@ -117791,12 +117786,151 @@ function requireEventsourceStream () { } clearEvent () { - this.event = { - data: undefined, - event: undefined, - id: undefined, - retry: undefined - }; + this.event.data = undefined; + this.event.event = undefined; + this.event.id = undefined; + this.event.retry = undefined; + } + + hasPendingEvent () { + return this.event.data !== undefined || + this.event.event !== undefined || + this.event.id !== undefined || + this.event.retry !== undefined + } + + hasCurrentByte () { + return this.chunkIndex < this.chunks.length && + this.pos < this.chunks[this.chunkIndex].length + } + + currentByte () { + return this.chunks[this.chunkIndex][this.pos] + } + + consumeCurrentByte () { + this.advanceCursor(); + this.syncLineStartToCursor(); + } + + advanceCursor () { + this.pos++; + + while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) { + this.chunkIndex++; + this.pos = 0; + } + } + + syncLineStartToCursor () { + this.lineChunkIndex = this.chunkIndex; + this.linePos = this.pos; + this.dropConsumedChunks(); + } + + dropConsumedChunks () { + while (this.lineChunkIndex > 0) { + this.chunks.shift(); + this.lineChunkIndex--; + this.chunkIndex--; + } + + if (this.chunkIndex === this.chunks.length) { + this.chunks.length = 0; + this.chunkIndex = 0; + this.pos = 0; + this.lineChunkIndex = 0; + this.linePos = 0; + } + } + + readLine () { + if (this.lineChunkIndex === this.chunkIndex) { + return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos) + } + + const chunks = []; + let length = 0; + + for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) { + const chunk = this.chunks[i]; + const start = i === this.lineChunkIndex ? this.linePos : 0; + const end = i === this.chunkIndex ? this.pos : chunk.length; + const slice = chunk.subarray(start, end); + length += slice.length; + chunks.push(slice); + } + + return Buffer.concat(chunks, length) + } + + peekBufferedByte (offset) { + let chunkIndex = this.lineChunkIndex; + let pos = this.linePos; + + while (chunkIndex < this.chunks.length) { + const chunk = this.chunks[chunkIndex]; + const remaining = chunk.length - pos; + + if (offset < remaining) { + return chunk[pos + offset] + } + + offset -= remaining; + chunkIndex++; + pos = 0; + } + } + + discardLeadingBytes (count) { + while (count > 0 && this.lineChunkIndex < this.chunks.length) { + const chunk = this.chunks[this.lineChunkIndex]; + const remaining = chunk.length - this.linePos; + + if (count < remaining) { + this.linePos += count; + count = 0; + } else { + count -= remaining; + this.lineChunkIndex++; + this.linePos = 0; + } + } + + this.chunkIndex = this.lineChunkIndex; + this.pos = this.linePos; + this.dropConsumedChunks(); + } + + handleBOM () { + const first = this.peekBufferedByte(0); + const second = this.peekBufferedByte(1); + const third = this.peekBufferedByte(2); + + if (second === undefined) { + if (first === BOM[0]) { + return true + } + + this.checkBOM = false; + return true + } + + if (third === undefined) { + if (first === BOM[0] && second === BOM[1]) { + return true + } + + this.checkBOM = false; + return false + } + + if (first === BOM[0] && second === BOM[1] && third === BOM[2]) { + this.discardLeadingBytes(3); + } + + this.checkBOM = false; + return !this.hasCurrentByte() } } From 0bd93385cb1b44a0ce5087b2bd6f2364c6d905a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:11:10 +0000 Subject: [PATCH 2/4] fix: apply audit fixes --- dist/index.mjs | 450 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 292 insertions(+), 158 deletions(-) diff --git a/dist/index.mjs b/dist/index.mjs index faf5619..95756b5 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -100119,7 +100119,7 @@ async function connectH1$1 (client, socket) { function clearIdleSocketValidation (socket) { if (socket[kIdleSocketValidationTimeout]) { - clearTimeout(socket[kIdleSocketValidationTimeout]); + clearImmediate(socket[kIdleSocketValidationTimeout]); socket[kIdleSocketValidationTimeout] = null; } @@ -100128,15 +100128,23 @@ function clearIdleSocketValidation (socket) { function scheduleIdleSocketValidation (client, socket) { socket[kIdleSocketValidation] = 1; - socket[kIdleSocketValidationTimeout] = setTimeout(() => { + // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST + // already pending on this idle keep-alive socket are processed before the + // next request is written (GHSA-35p6-xmwp-9g52). + // + // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse + // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll + // block for ~500ms when the event loop is otherwise idle (#5600 / #5606). + // A ref'd Immediate both keeps the pending request alive and makes poll + // return immediately — the hybrid those issues asked for. + socket[kIdleSocketValidationTimeout] = setImmediate(() => { socket[kIdleSocketValidationTimeout] = null; socket[kIdleSocketValidation] = 2; if (client[kSocket$1] === socket && !socket.destroyed) { client[kResume$3](); } - }, 0); - socket[kIdleSocketValidationTimeout].unref?.(); + }); } /** @@ -103641,6 +103649,7 @@ let RetryHandler$3 = class RetryHandler { this.end = null; this.etag = null; this.resume = null; + this.headersSent = false; // Handle possible onConnect duplication this.handler.onConnect(reason => { @@ -103653,6 +103662,20 @@ let RetryHandler$3 = class RetryHandler { }); } + checkpointResponseEnd (headers, resume) { + if (this.end == null && this.opts.method !== 'HEAD') { + const contentLength = headers['content-length']; + this.end = contentLength != null ? Number(contentLength) - 1 : null; + + assert$7( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ); + } + + this.resume = this.end != null ? resume : null; + } + onRequestSent () { if (this.handler.onRequestSent) { this.handler.onRequestSent(); @@ -103742,6 +103765,8 @@ let RetryHandler$3 = class RetryHandler { if (statusCode >= 300) { if (this.retryOpts.statusCodes.includes(statusCode) === false) { + this.headersSent = true; + this.checkpointResponseEnd(headers, resume); return this.handler.onHeaders( statusCode, rawHeaders, @@ -103810,8 +103835,15 @@ let RetryHandler$3 = class RetryHandler { const { start, size, end = size - 1 } = contentRange; - assert$7(this.start === start, 'content-range mismatch'); - assert$7(this.end == null || this.end === end, 'content-range mismatch'); + if (this.start !== start || (this.end != null && this.end !== end)) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false + } this.resume = resume; return true @@ -103823,6 +103855,7 @@ let RetryHandler$3 = class RetryHandler { const range = parseRangeHeader(headers['content-range']); if (range == null) { + this.headersSent = true; return this.handler.onHeaders( statusCode, rawHeaders, @@ -103861,6 +103894,7 @@ let RetryHandler$3 = class RetryHandler { ); this.resume = resume; + this.headersSent = true; this.etag = headers.etag != null ? headers.etag : null; // Weak etags are not useful for comparison nor cache @@ -103900,7 +103934,7 @@ let RetryHandler$3 = class RetryHandler { } onError (err) { - if (this.aborted || isDisturbed(this.opts.body)) { + if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) { return this.handler.onError(err) } @@ -115832,7 +115866,7 @@ function requireConnection () { // is specified, the server needs to include the same field and one of // the selected subprotocol values in its response for the connection to // be established. - if (!requestProtocols.includes(secProtocol)) { + if (requestProtocols === null || !requestProtocols.includes(secProtocol)) { failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.'); return } @@ -116079,7 +116113,12 @@ function requirePermessageDeflate () { if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { callback(new MessageSizeExceededError()); + // The inflater may still hold buffered input that can emit a late + // zlib error. Remove the data listener, then deterministically stop + // the stream so a subsequent 'error' cannot fire without a listener + // (which would terminate the process as an unhandled error event). this.#inflate.removeAllListeners(); + this.#inflate.destroy(); this.#inflate = null; return } @@ -117428,6 +117467,49 @@ function requireEventsourceStream () { */ const SPACE = 0x20; + const DATA = Buffer.from('data'); + const EVENT = Buffer.from('event'); + const ID = Buffer.from('id'); + const RETRY = Buffer.from('retry'); + + function isASCIINumberBytes (buffer, start) { + if (start >= buffer.length) { + return false + } + + for (let i = start; i < buffer.length; i++) { + if (buffer[i] < 0x30 || buffer[i] > 0x39) { + return false + } + } + + return true + } + + function isValidLastEventIdBytes (buffer, start) { + for (let i = start; i < buffer.length; i++) { + if (buffer[i] === 0x00) { + return false + } + } + + return true + } + + function isFieldName (line, length, field) { + if (length !== field.length) { + return false + } + + for (let i = 0; i < length; i++) { + if (line[i] !== field[i]) { + return false + } + } + + return true + } + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -117468,11 +117550,14 @@ function requireEventsourceStream () { eventEndCheck = false /** - * @type {Buffer} + * @type {Buffer[]} */ - buffer = null + chunks = [] + chunkIndex = 0 pos = 0 + lineChunkIndex = 0 + linePos = 0 event = { data: undefined, @@ -117511,92 +117596,20 @@ function requireEventsourceStream () { return } - // Cache the chunk in the buffer, as the data might not be complete while - // processing it - // TODO: Investigate if there is a more performant way to handle - // incoming chunks - // see: https://github.com/nodejs/undici/issues/2630 - if (this.buffer) { - this.buffer = Buffer.concat([this.buffer, chunk]); - } else { - this.buffer = chunk; - } + this.chunks.push(chunk); // Strip leading byte-order-mark if we opened the stream and started // the processing of the incoming data if (this.checkBOM) { - switch (this.buffer.length) { - case 1: - // Check if the first byte is the same as the first byte of the BOM - if (this.buffer[0] === BOM[0]) { - // If it is, we need to wait for more data - callback(); - return - } - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - - // The buffer only contains one byte so we need to wait for more data - callback(); - return - case 2: - // Check if the first two bytes are the same as the first two bytes - // of the BOM - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] - ) { - // If it is, we need to wait for more data, because the third byte - // is needed to determine if it is the BOM or not - callback(); - return - } - - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - break - case 3: - // Check if the first three bytes are the same as the first three - // bytes of the BOM - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] && - this.buffer[2] === BOM[2] - ) { - // If it is, we can drop the buffered data, as it is only the BOM - this.buffer = Buffer.alloc(0); - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - - // Await more data - callback(); - return - } - // If it is not the BOM, we can start processing the data - this.checkBOM = false; - break - default: - // The buffer is longer than 3 bytes, so we can drop the BOM if it is - // present - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] && - this.buffer[2] === BOM[2] - ) { - // Remove the BOM from the buffer - this.buffer = this.buffer.subarray(3); - } - - // Set the checkBOM flag to false as we don't need to check for the - this.checkBOM = false; - break + if (this.handleBOM()) { + callback(); + return } } - while (this.pos < this.buffer.length) { + while (this.hasCurrentByte()) { + const byte = this.currentByte(); + // If the previous line ended with an end-of-line, we need to check // if the next character is also an end-of-line. if (this.eventEndCheck) { @@ -117609,10 +117622,9 @@ function requireEventsourceStream () { if (this.crlfCheck) { // If the current character is a line feed, we can remove it // from the buffer and reset the crlfCheck flag - if (this.buffer[this.pos] === LF) { - this.buffer = this.buffer.subarray(this.pos + 1); - this.pos = 0; + if (byte === LF) { this.crlfCheck = false; + this.consumeCurrentByte(); // It is possible that the line feed is not the end of the // event. We need to check if the next character is an @@ -117628,19 +117640,17 @@ function requireEventsourceStream () { this.crlfCheck = false; } - if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (byte === LF || byte === CR) { // If the current character is a carriage return, we need to // set the crlfCheck flag to true, as we need to check if the // next character is a line feed so we can remove it from the // buffer - if (this.buffer[this.pos] === CR) { + if (byte === CR) { this.crlfCheck = true; } - this.buffer = this.buffer.subarray(this.pos + 1); - this.pos = 0; - if ( - this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) { + this.consumeCurrentByte(); + if (this.hasPendingEvent()) { this.processEvent(this.event); } this.clearEvent(); @@ -117654,22 +117664,18 @@ function requireEventsourceStream () { // If the current character is an end-of-line, we can process the // line - if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (byte === LF || byte === CR) { // If the current character is a carriage return, we need to // set the crlfCheck flag to true, as we need to check if the // next character is a line feed - if (this.buffer[this.pos] === CR) { + if (byte === CR) { this.crlfCheck = true; } // In any case, we can process the line as we reached an // end-of-line character - this.parseLine(this.buffer.subarray(0, this.pos), this.event); - - // Remove the processed line from the buffer - this.buffer = this.buffer.subarray(this.pos + 1); - // Reset the position as we removed the processed line from the buffer - this.pos = 0; + this.parseLine(this.readLine(), this.event); + this.consumeCurrentByte(); // A line was processed and this could be the end of the event. We need // to check if the next line is empty to determine if the event is // finished. @@ -117677,7 +117683,7 @@ function requireEventsourceStream () { continue } - this.pos++; + this.advanceCursor(); } callback(); @@ -117702,64 +117708,53 @@ function requireEventsourceStream () { return } - let field = ''; - let value = ''; + let fieldLength = line.length; + let valueStart = line.length; // If the line contains a U+003A COLON character (:) if (colonPosition !== -1) { - // Collect the characters on the line before the first U+003A COLON - // character (:), and let field be that string. - // TODO: Investigate if there is a more performant way to extract the - // field - // see: https://github.com/nodejs/undici/issues/2630 - field = line.subarray(0, colonPosition).toString('utf8'); + fieldLength = colonPosition; // Collect the characters on the line after the first U+003A COLON // character (:), and let value be that string. // If value starts with a U+0020 SPACE character, remove it from value. - let valueStart = colonPosition + 1; + valueStart = colonPosition + 1; if (line[valueStart] === SPACE) { ++valueStart; } - // TODO: Investigate if there is a more performant way to extract the - // value - // see: https://github.com/nodejs/undici/issues/2630 - value = line.subarray(valueStart).toString('utf8'); + } - // Otherwise, the string is not empty but does not contain a U+003A COLON - // character (:) - } else { - // Process the field using the steps described below, using the whole - // line as the field name, and the empty string as the field value. - field = line.toString('utf8'); - value = ''; + if (isFieldName(line, fieldLength, DATA)) { + const value = line.toString('utf8', valueStart); + + if (event.data === undefined) { + event.data = value; + } else { + event.data += `\n${value}`; + } + return } - // Modify the event with the field name and value. The value is also - // decoded as UTF-8 - switch (field) { - case 'data': - if (event[field] === undefined) { - event[field] = value; - } else { - event[field] += `\n${value}`; - } - break - case 'retry': - if (isASCIINumber(value)) { - event[field] = value; - } - break - case 'id': - if (isValidLastEventId(value)) { - event[field] = value; - } - break - case 'event': - if (value.length > 0) { - event[field] = value; - } - break + if (isFieldName(line, fieldLength, RETRY)) { + if (isASCIINumberBytes(line, valueStart)) { + event.retry = line.toString('utf8', valueStart); + } + return + } + + if (isFieldName(line, fieldLength, ID)) { + if (isValidLastEventIdBytes(line, valueStart)) { + event.id = line.toString('utf8', valueStart); + } + return + } + + if (isFieldName(line, fieldLength, EVENT)) { + const value = line.toString('utf8', valueStart); + + if (value.length > 0) { + event.event = value; + } } } @@ -117789,12 +117784,151 @@ function requireEventsourceStream () { } clearEvent () { - this.event = { - data: undefined, - event: undefined, - id: undefined, - retry: undefined - }; + this.event.data = undefined; + this.event.event = undefined; + this.event.id = undefined; + this.event.retry = undefined; + } + + hasPendingEvent () { + return this.event.data !== undefined || + this.event.event !== undefined || + this.event.id !== undefined || + this.event.retry !== undefined + } + + hasCurrentByte () { + return this.chunkIndex < this.chunks.length && + this.pos < this.chunks[this.chunkIndex].length + } + + currentByte () { + return this.chunks[this.chunkIndex][this.pos] + } + + consumeCurrentByte () { + this.advanceCursor(); + this.syncLineStartToCursor(); + } + + advanceCursor () { + this.pos++; + + while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) { + this.chunkIndex++; + this.pos = 0; + } + } + + syncLineStartToCursor () { + this.lineChunkIndex = this.chunkIndex; + this.linePos = this.pos; + this.dropConsumedChunks(); + } + + dropConsumedChunks () { + while (this.lineChunkIndex > 0) { + this.chunks.shift(); + this.lineChunkIndex--; + this.chunkIndex--; + } + + if (this.chunkIndex === this.chunks.length) { + this.chunks.length = 0; + this.chunkIndex = 0; + this.pos = 0; + this.lineChunkIndex = 0; + this.linePos = 0; + } + } + + readLine () { + if (this.lineChunkIndex === this.chunkIndex) { + return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos) + } + + const chunks = []; + let length = 0; + + for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) { + const chunk = this.chunks[i]; + const start = i === this.lineChunkIndex ? this.linePos : 0; + const end = i === this.chunkIndex ? this.pos : chunk.length; + const slice = chunk.subarray(start, end); + length += slice.length; + chunks.push(slice); + } + + return Buffer.concat(chunks, length) + } + + peekBufferedByte (offset) { + let chunkIndex = this.lineChunkIndex; + let pos = this.linePos; + + while (chunkIndex < this.chunks.length) { + const chunk = this.chunks[chunkIndex]; + const remaining = chunk.length - pos; + + if (offset < remaining) { + return chunk[pos + offset] + } + + offset -= remaining; + chunkIndex++; + pos = 0; + } + } + + discardLeadingBytes (count) { + while (count > 0 && this.lineChunkIndex < this.chunks.length) { + const chunk = this.chunks[this.lineChunkIndex]; + const remaining = chunk.length - this.linePos; + + if (count < remaining) { + this.linePos += count; + count = 0; + } else { + count -= remaining; + this.lineChunkIndex++; + this.linePos = 0; + } + } + + this.chunkIndex = this.lineChunkIndex; + this.pos = this.linePos; + this.dropConsumedChunks(); + } + + handleBOM () { + const first = this.peekBufferedByte(0); + const second = this.peekBufferedByte(1); + const third = this.peekBufferedByte(2); + + if (second === undefined) { + if (first === BOM[0]) { + return true + } + + this.checkBOM = false; + return true + } + + if (third === undefined) { + if (first === BOM[0] && second === BOM[1]) { + return true + } + + this.checkBOM = false; + return false + } + + if (first === BOM[0] && second === BOM[1] && third === BOM[2]) { + this.discardLeadingBytes(3); + } + + this.checkBOM = false; + return !this.hasCurrentByte() } } From e3400f33e4834db4229ebccf7c6a3889c6bd488b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:11:13 +0000 Subject: [PATCH 3/4] fix: apply audit fixes --- dist/main.js | 450 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 292 insertions(+), 158 deletions(-) diff --git a/dist/main.js b/dist/main.js index 078721b..97da531 100644 --- a/dist/main.js +++ b/dist/main.js @@ -9599,7 +9599,7 @@ async function connectH1$1 (client, socket) { function clearIdleSocketValidation (socket) { if (socket[kIdleSocketValidationTimeout]) { - clearTimeout(socket[kIdleSocketValidationTimeout]); + clearImmediate(socket[kIdleSocketValidationTimeout]); socket[kIdleSocketValidationTimeout] = null; } @@ -9608,15 +9608,23 @@ function clearIdleSocketValidation (socket) { function scheduleIdleSocketValidation (client, socket) { socket[kIdleSocketValidation] = 1; - socket[kIdleSocketValidationTimeout] = setTimeout(() => { + // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST + // already pending on this idle keep-alive socket are processed before the + // next request is written (GHSA-35p6-xmwp-9g52). + // + // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse + // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll + // block for ~500ms when the event loop is otherwise idle (#5600 / #5606). + // A ref'd Immediate both keeps the pending request alive and makes poll + // return immediately — the hybrid those issues asked for. + socket[kIdleSocketValidationTimeout] = setImmediate(() => { socket[kIdleSocketValidationTimeout] = null; socket[kIdleSocketValidation] = 2; if (client[kSocket$1] === socket && !socket.destroyed) { client[kResume$3](); } - }, 0); - socket[kIdleSocketValidationTimeout].unref?.(); + }); } /** @@ -13121,6 +13129,7 @@ let RetryHandler$3 = class RetryHandler { this.end = null; this.etag = null; this.resume = null; + this.headersSent = false; // Handle possible onConnect duplication this.handler.onConnect(reason => { @@ -13133,6 +13142,20 @@ let RetryHandler$3 = class RetryHandler { }); } + checkpointResponseEnd (headers, resume) { + if (this.end == null && this.opts.method !== 'HEAD') { + const contentLength = headers['content-length']; + this.end = contentLength != null ? Number(contentLength) - 1 : null; + + assert$8( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ); + } + + this.resume = this.end != null ? resume : null; + } + onRequestSent () { if (this.handler.onRequestSent) { this.handler.onRequestSent(); @@ -13222,6 +13245,8 @@ let RetryHandler$3 = class RetryHandler { if (statusCode >= 300) { if (this.retryOpts.statusCodes.includes(statusCode) === false) { + this.headersSent = true; + this.checkpointResponseEnd(headers, resume); return this.handler.onHeaders( statusCode, rawHeaders, @@ -13290,8 +13315,15 @@ let RetryHandler$3 = class RetryHandler { const { start, size, end = size - 1 } = contentRange; - assert$8(this.start === start, 'content-range mismatch'); - assert$8(this.end == null || this.end === end, 'content-range mismatch'); + if (this.start !== start || (this.end != null && this.end !== end)) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false + } this.resume = resume; return true @@ -13303,6 +13335,7 @@ let RetryHandler$3 = class RetryHandler { const range = parseRangeHeader(headers['content-range']); if (range == null) { + this.headersSent = true; return this.handler.onHeaders( statusCode, rawHeaders, @@ -13341,6 +13374,7 @@ let RetryHandler$3 = class RetryHandler { ); this.resume = resume; + this.headersSent = true; this.etag = headers.etag != null ? headers.etag : null; // Weak etags are not useful for comparison nor cache @@ -13380,7 +13414,7 @@ let RetryHandler$3 = class RetryHandler { } onError (err) { - if (this.aborted || isDisturbed(this.opts.body)) { + if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) { return this.handler.onError(err) } @@ -25312,7 +25346,7 @@ function requireConnection () { // is specified, the server needs to include the same field and one of // the selected subprotocol values in its response for the connection to // be established. - if (!requestProtocols.includes(secProtocol)) { + if (requestProtocols === null || !requestProtocols.includes(secProtocol)) { failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.'); return } @@ -25559,7 +25593,12 @@ function requirePermessageDeflate () { if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { callback(new MessageSizeExceededError()); + // The inflater may still hold buffered input that can emit a late + // zlib error. Remove the data listener, then deterministically stop + // the stream so a subsequent 'error' cannot fire without a listener + // (which would terminate the process as an unhandled error event). this.#inflate.removeAllListeners(); + this.#inflate.destroy(); this.#inflate = null; return } @@ -26908,6 +26947,49 @@ function requireEventsourceStream () { */ const SPACE = 0x20; + const DATA = Buffer.from('data'); + const EVENT = Buffer.from('event'); + const ID = Buffer.from('id'); + const RETRY = Buffer.from('retry'); + + function isASCIINumberBytes (buffer, start) { + if (start >= buffer.length) { + return false + } + + for (let i = start; i < buffer.length; i++) { + if (buffer[i] < 0x30 || buffer[i] > 0x39) { + return false + } + } + + return true + } + + function isValidLastEventIdBytes (buffer, start) { + for (let i = start; i < buffer.length; i++) { + if (buffer[i] === 0x00) { + return false + } + } + + return true + } + + function isFieldName (line, length, field) { + if (length !== field.length) { + return false + } + + for (let i = 0; i < length; i++) { + if (line[i] !== field[i]) { + return false + } + } + + return true + } + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -26948,11 +27030,14 @@ function requireEventsourceStream () { eventEndCheck = false /** - * @type {Buffer} + * @type {Buffer[]} */ - buffer = null + chunks = [] + chunkIndex = 0 pos = 0 + lineChunkIndex = 0 + linePos = 0 event = { data: undefined, @@ -26991,92 +27076,20 @@ function requireEventsourceStream () { return } - // Cache the chunk in the buffer, as the data might not be complete while - // processing it - // TODO: Investigate if there is a more performant way to handle - // incoming chunks - // see: https://github.com/nodejs/undici/issues/2630 - if (this.buffer) { - this.buffer = Buffer.concat([this.buffer, chunk]); - } else { - this.buffer = chunk; - } + this.chunks.push(chunk); // Strip leading byte-order-mark if we opened the stream and started // the processing of the incoming data if (this.checkBOM) { - switch (this.buffer.length) { - case 1: - // Check if the first byte is the same as the first byte of the BOM - if (this.buffer[0] === BOM[0]) { - // If it is, we need to wait for more data - callback(); - return - } - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - - // The buffer only contains one byte so we need to wait for more data - callback(); - return - case 2: - // Check if the first two bytes are the same as the first two bytes - // of the BOM - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] - ) { - // If it is, we need to wait for more data, because the third byte - // is needed to determine if it is the BOM or not - callback(); - return - } - - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - break - case 3: - // Check if the first three bytes are the same as the first three - // bytes of the BOM - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] && - this.buffer[2] === BOM[2] - ) { - // If it is, we can drop the buffered data, as it is only the BOM - this.buffer = Buffer.alloc(0); - // Set the checkBOM flag to false as we don't need to check for the - // BOM anymore - this.checkBOM = false; - - // Await more data - callback(); - return - } - // If it is not the BOM, we can start processing the data - this.checkBOM = false; - break - default: - // The buffer is longer than 3 bytes, so we can drop the BOM if it is - // present - if ( - this.buffer[0] === BOM[0] && - this.buffer[1] === BOM[1] && - this.buffer[2] === BOM[2] - ) { - // Remove the BOM from the buffer - this.buffer = this.buffer.subarray(3); - } - - // Set the checkBOM flag to false as we don't need to check for the - this.checkBOM = false; - break + if (this.handleBOM()) { + callback(); + return } } - while (this.pos < this.buffer.length) { + while (this.hasCurrentByte()) { + const byte = this.currentByte(); + // If the previous line ended with an end-of-line, we need to check // if the next character is also an end-of-line. if (this.eventEndCheck) { @@ -27089,10 +27102,9 @@ function requireEventsourceStream () { if (this.crlfCheck) { // If the current character is a line feed, we can remove it // from the buffer and reset the crlfCheck flag - if (this.buffer[this.pos] === LF) { - this.buffer = this.buffer.subarray(this.pos + 1); - this.pos = 0; + if (byte === LF) { this.crlfCheck = false; + this.consumeCurrentByte(); // It is possible that the line feed is not the end of the // event. We need to check if the next character is an @@ -27108,19 +27120,17 @@ function requireEventsourceStream () { this.crlfCheck = false; } - if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (byte === LF || byte === CR) { // If the current character is a carriage return, we need to // set the crlfCheck flag to true, as we need to check if the // next character is a line feed so we can remove it from the // buffer - if (this.buffer[this.pos] === CR) { + if (byte === CR) { this.crlfCheck = true; } - this.buffer = this.buffer.subarray(this.pos + 1); - this.pos = 0; - if ( - this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) { + this.consumeCurrentByte(); + if (this.hasPendingEvent()) { this.processEvent(this.event); } this.clearEvent(); @@ -27134,22 +27144,18 @@ function requireEventsourceStream () { // If the current character is an end-of-line, we can process the // line - if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (byte === LF || byte === CR) { // If the current character is a carriage return, we need to // set the crlfCheck flag to true, as we need to check if the // next character is a line feed - if (this.buffer[this.pos] === CR) { + if (byte === CR) { this.crlfCheck = true; } // In any case, we can process the line as we reached an // end-of-line character - this.parseLine(this.buffer.subarray(0, this.pos), this.event); - - // Remove the processed line from the buffer - this.buffer = this.buffer.subarray(this.pos + 1); - // Reset the position as we removed the processed line from the buffer - this.pos = 0; + this.parseLine(this.readLine(), this.event); + this.consumeCurrentByte(); // A line was processed and this could be the end of the event. We need // to check if the next line is empty to determine if the event is // finished. @@ -27157,7 +27163,7 @@ function requireEventsourceStream () { continue } - this.pos++; + this.advanceCursor(); } callback(); @@ -27182,64 +27188,53 @@ function requireEventsourceStream () { return } - let field = ''; - let value = ''; + let fieldLength = line.length; + let valueStart = line.length; // If the line contains a U+003A COLON character (:) if (colonPosition !== -1) { - // Collect the characters on the line before the first U+003A COLON - // character (:), and let field be that string. - // TODO: Investigate if there is a more performant way to extract the - // field - // see: https://github.com/nodejs/undici/issues/2630 - field = line.subarray(0, colonPosition).toString('utf8'); + fieldLength = colonPosition; // Collect the characters on the line after the first U+003A COLON // character (:), and let value be that string. // If value starts with a U+0020 SPACE character, remove it from value. - let valueStart = colonPosition + 1; + valueStart = colonPosition + 1; if (line[valueStart] === SPACE) { ++valueStart; } - // TODO: Investigate if there is a more performant way to extract the - // value - // see: https://github.com/nodejs/undici/issues/2630 - value = line.subarray(valueStart).toString('utf8'); + } - // Otherwise, the string is not empty but does not contain a U+003A COLON - // character (:) - } else { - // Process the field using the steps described below, using the whole - // line as the field name, and the empty string as the field value. - field = line.toString('utf8'); - value = ''; + if (isFieldName(line, fieldLength, DATA)) { + const value = line.toString('utf8', valueStart); + + if (event.data === undefined) { + event.data = value; + } else { + event.data += `\n${value}`; + } + return } - // Modify the event with the field name and value. The value is also - // decoded as UTF-8 - switch (field) { - case 'data': - if (event[field] === undefined) { - event[field] = value; - } else { - event[field] += `\n${value}`; - } - break - case 'retry': - if (isASCIINumber(value)) { - event[field] = value; - } - break - case 'id': - if (isValidLastEventId(value)) { - event[field] = value; - } - break - case 'event': - if (value.length > 0) { - event[field] = value; - } - break + if (isFieldName(line, fieldLength, RETRY)) { + if (isASCIINumberBytes(line, valueStart)) { + event.retry = line.toString('utf8', valueStart); + } + return + } + + if (isFieldName(line, fieldLength, ID)) { + if (isValidLastEventIdBytes(line, valueStart)) { + event.id = line.toString('utf8', valueStart); + } + return + } + + if (isFieldName(line, fieldLength, EVENT)) { + const value = line.toString('utf8', valueStart); + + if (value.length > 0) { + event.event = value; + } } } @@ -27269,12 +27264,151 @@ function requireEventsourceStream () { } clearEvent () { - this.event = { - data: undefined, - event: undefined, - id: undefined, - retry: undefined - }; + this.event.data = undefined; + this.event.event = undefined; + this.event.id = undefined; + this.event.retry = undefined; + } + + hasPendingEvent () { + return this.event.data !== undefined || + this.event.event !== undefined || + this.event.id !== undefined || + this.event.retry !== undefined + } + + hasCurrentByte () { + return this.chunkIndex < this.chunks.length && + this.pos < this.chunks[this.chunkIndex].length + } + + currentByte () { + return this.chunks[this.chunkIndex][this.pos] + } + + consumeCurrentByte () { + this.advanceCursor(); + this.syncLineStartToCursor(); + } + + advanceCursor () { + this.pos++; + + while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) { + this.chunkIndex++; + this.pos = 0; + } + } + + syncLineStartToCursor () { + this.lineChunkIndex = this.chunkIndex; + this.linePos = this.pos; + this.dropConsumedChunks(); + } + + dropConsumedChunks () { + while (this.lineChunkIndex > 0) { + this.chunks.shift(); + this.lineChunkIndex--; + this.chunkIndex--; + } + + if (this.chunkIndex === this.chunks.length) { + this.chunks.length = 0; + this.chunkIndex = 0; + this.pos = 0; + this.lineChunkIndex = 0; + this.linePos = 0; + } + } + + readLine () { + if (this.lineChunkIndex === this.chunkIndex) { + return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos) + } + + const chunks = []; + let length = 0; + + for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) { + const chunk = this.chunks[i]; + const start = i === this.lineChunkIndex ? this.linePos : 0; + const end = i === this.chunkIndex ? this.pos : chunk.length; + const slice = chunk.subarray(start, end); + length += slice.length; + chunks.push(slice); + } + + return Buffer.concat(chunks, length) + } + + peekBufferedByte (offset) { + let chunkIndex = this.lineChunkIndex; + let pos = this.linePos; + + while (chunkIndex < this.chunks.length) { + const chunk = this.chunks[chunkIndex]; + const remaining = chunk.length - pos; + + if (offset < remaining) { + return chunk[pos + offset] + } + + offset -= remaining; + chunkIndex++; + pos = 0; + } + } + + discardLeadingBytes (count) { + while (count > 0 && this.lineChunkIndex < this.chunks.length) { + const chunk = this.chunks[this.lineChunkIndex]; + const remaining = chunk.length - this.linePos; + + if (count < remaining) { + this.linePos += count; + count = 0; + } else { + count -= remaining; + this.lineChunkIndex++; + this.linePos = 0; + } + } + + this.chunkIndex = this.lineChunkIndex; + this.pos = this.linePos; + this.dropConsumedChunks(); + } + + handleBOM () { + const first = this.peekBufferedByte(0); + const second = this.peekBufferedByte(1); + const third = this.peekBufferedByte(2); + + if (second === undefined) { + if (first === BOM[0]) { + return true + } + + this.checkBOM = false; + return true + } + + if (third === undefined) { + if (first === BOM[0] && second === BOM[1]) { + return true + } + + this.checkBOM = false; + return false + } + + if (first === BOM[0] && second === BOM[1] && third === BOM[2]) { + this.discardLeadingBytes(3); + } + + this.checkBOM = false; + return !this.hasCurrentByte() } } From 67dbe83276cda5aa63879d1209a8c0e7b205f46a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:11:15 +0000 Subject: [PATCH 4/4] fix: apply audit fixes --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 49a5c55..180737f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3618,9 +3618,9 @@ slice-ansi@^5.0.0: is-fullwidth-code-point "^4.0.0" source-map-js@^1.2.0, source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity "sha1-HOVlD93YerwJnto33P8CTCZnrkY= sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + version "1.2.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab" + integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw== split2@^4.0.0: version "4.2.0"