node_modules: update (#146)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurla
2026-08-07 08:08:41 +02:00
committed by GitHub
co-authored by dawidd6
parent 6b3dfab4e0
commit 3d5a3a75cc
44 changed files with 660 additions and 284 deletions
+9 -9
View File
@@ -4,9 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz",
"integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/exec": "^3.0.0", "@actions/exec": "^3.0.0",
@@ -48,18 +48,18 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.24.0", "version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==", "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"
} }
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.8.2", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "3.0.0", "version": "3.0.1",
"description": "Actions core lib", "description": "Actions core lib",
"keywords": [ "keywords": [
"github", "github",
+3
View File
@@ -26,6 +26,9 @@ Returns: `Client`
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds. * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
-15
View File
@@ -20,7 +20,6 @@ When passing an object as the second argument, the following options are availab
* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use. * **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use.
* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection. * **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection.
* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request. * **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request.
* **maxDecompressedMessageSize** `number` (optional) - Maximum allowed size in bytes for decompressed messages when using the `permessage-deflate` extension. **Default:** `4194304` (4 MB).
### Example: ### Example:
@@ -45,20 +44,6 @@ import { WebSocket } from 'undici'
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat']) const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
``` ```
### Example with custom decompression limit:
To protect against decompression bombs (small compressed payloads that expand to very large sizes), you can set a custom limit:
```mjs
import { WebSocket } from 'undici'
const ws = new WebSocket('wss://echo.websocket.events', {
maxDecompressedMessageSize: 1 * 1024 * 1024
})
```
> ⚠️ **Security Note**: The `maxDecompressedMessageSize` option protects against memory exhaustion attacks where a malicious server sends a small compressed payload that decompresses to an extremely large size. If you increase this limit significantly above the default, ensure your application can handle the increased memory usage.
## Read More ## Read More
- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) - [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+12 -1
View File
@@ -350,7 +350,13 @@ function processHeader (request, key, val) {
} else if (typeof val[i] === 'object') { } else if (typeof val[i] === 'object') {
throw new InvalidArgumentError(`invalid ${key} header`) throw new InvalidArgumentError(`invalid ${key} header`)
} else { } else {
arr.push(`${val[i]}`) // Coerce primitives (and reject unsafe coercions such as functions
// with a crafted toString/Symbol.toPrimitive).
const str = `${val[i]}`
if (!isValidHeaderValue(str)) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
arr.push(str)
} }
} }
val = arr val = arr
@@ -361,7 +367,12 @@ function processHeader (request, key, val) {
} else if (val === null) { } else if (val === null) {
val = '' val = ''
} else { } else {
// Coerce primitives (and reject unsafe coercions such as functions
// with a crafted toString/Symbol.toPrimitive).
val = `${val}` val = `${val}`
if (!isValidHeaderValue(val)) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
} }
if (headerName === 'host') { if (headerName === 'host') {
+2 -2
View File
@@ -24,8 +24,6 @@ function defaultFactory (origin, opts) {
class Agent extends DispatcherBase { class Agent extends DispatcherBase {
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
super()
if (typeof factory !== 'function') { if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.') throw new InvalidArgumentError('factory must be a function.')
} }
@@ -38,6 +36,8 @@ class Agent extends DispatcherBase {
throw new InvalidArgumentError('maxRedirections must be a positive number') throw new InvalidArgumentError('maxRedirections must be a positive number')
} }
super(options)
if (connect && typeof connect !== 'function') { if (connect && typeof connect !== 'function') {
connect = { ...connect } connect = { ...connect }
} }
+144 -18
View File
@@ -10,6 +10,7 @@ const {
RequestContentLengthMismatchError, RequestContentLengthMismatchError,
ResponseContentLengthMismatchError, ResponseContentLengthMismatchError,
RequestAbortedError, RequestAbortedError,
InvalidArgumentError,
HeadersTimeoutError, HeadersTimeoutError,
HeadersOverflowError, HeadersOverflowError,
SocketError, SocketError,
@@ -57,6 +58,9 @@ const EMPTY_BUF = Buffer.alloc(0)
const FastBuffer = Buffer[Symbol.species] const FastBuffer = Buffer[Symbol.species]
const addListener = util.addListener const addListener = util.addListener
const removeAllListeners = util.removeAllListeners const removeAllListeners = util.removeAllListeners
const kIdleSocketValidation = Symbol('kIdleSocketValidation')
const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
const kSocketUsed = Symbol('kSocketUsed')
let extractBody let extractBody
@@ -279,15 +283,60 @@ class Parser {
const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
if (ret !== constants.ERROR.OK) {
const body = data.subarray(offset)
if (ret === constants.ERROR.PAUSED_UPGRADE) { if (ret === constants.ERROR.PAUSED_UPGRADE) {
this.onUpgrade(data.slice(offset)) this.onUpgrade(body)
} else if (ret === constants.ERROR.PAUSED) { } else if (ret === constants.ERROR.PAUSED) {
this.paused = true this.paused = true
socket.unshift(data.slice(offset)) socket.unshift(body)
} else if (ret !== constants.ERROR.OK) { } else {
throw this.createError(ret, body)
}
}
} catch (err) {
util.destroy(socket, err)
}
}
finish () {
assert(currentParser === null)
assert(this.ptr != null)
assert(!this.paused)
const { llhttp } = this
let ret
try {
currentParser = this
ret = llhttp.llhttp_finish(this.ptr)
} finally {
currentParser = null
}
if (ret === constants.ERROR.OK) {
return null
}
if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
this.paused = true
return null
}
return this.createError(ret, EMPTY_BUF)
}
createError (ret, data) {
const { llhttp, contentLength, bytesRead } = this
if (contentLength && bytesRead !== parseInt(contentLength, 10)) {
return new ResponseContentLengthMismatchError()
}
const ptr = llhttp.llhttp_get_error_reason(this.ptr) const ptr = llhttp.llhttp_get_error_reason(this.ptr)
let message = '' let message = ''
/* istanbul ignore else: difficult to make a test case for */
if (ptr) { if (ptr) {
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
message = message =
@@ -295,11 +344,8 @@ class Parser {
Buffer.from(llhttp.memory.buffer, ptr, len).toString() + Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
')' ')'
} }
throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
} return new HTTPParserError(message, constants.ERROR[ret], data)
} catch (err) {
util.destroy(socket, err)
}
} }
destroy () { destroy () {
@@ -329,6 +375,11 @@ class Parser {
return -1 return -1
} }
if (client[kRunning] === 0) {
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
return -1
}
const request = client[kQueue][client[kRunningIdx]] const request = client[kQueue][client[kRunningIdx]]
if (!request) { if (!request) {
return -1 return -1
@@ -432,6 +483,11 @@ class Parser {
return -1 return -1
} }
if (client[kRunning] === 0) {
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
return -1
}
const request = client[kQueue][client[kRunningIdx]] const request = client[kQueue][client[kRunningIdx]]
/* istanbul ignore next: difficult to make a test case for */ /* istanbul ignore next: difficult to make a test case for */
@@ -605,6 +661,7 @@ class Parser {
request.onComplete(headers) request.onComplete(headers)
client[kQueue][client[kRunningIdx]++] = null client[kQueue][client[kRunningIdx]++] = null
socket[kSocketUsed] = true
if (socket[kWriting]) { if (socket[kWriting]) {
assert(client[kRunning] === 0) assert(client[kRunning] === 0)
@@ -663,6 +720,9 @@ async function connectH1 (client, socket) {
socket[kWriting] = false socket[kWriting] = false
socket[kReset] = false socket[kReset] = false
socket[kBlocking] = false socket[kBlocking] = false
socket[kIdleSocketValidation] = 0
socket[kIdleSocketValidationTimeout] = null
socket[kSocketUsed] = false
socket[kParser] = new Parser(client, socket, llhttpInstance) socket[kParser] = new Parser(client, socket, llhttpInstance)
addListener(socket, 'error', function (err) { addListener(socket, 'error', function (err) {
@@ -673,8 +733,11 @@ async function connectH1 (client, socket) {
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
// to the user. // to the user.
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
// We treat all incoming data so for as a valid response. const parserErr = parser.finish()
parser.onMessageComplete() if (parserErr) {
this[kError] = parserErr
this[kClient][kOnError](parserErr)
}
return return
} }
@@ -693,8 +756,10 @@ async function connectH1 (client, socket) {
const parser = this[kParser] const parser = this[kParser]
if (parser.statusCode && !parser.shouldKeepAlive) { if (parser.statusCode && !parser.shouldKeepAlive) {
// We treat all incoming data so far as a valid response. const parserErr = parser.finish()
parser.onMessageComplete() if (parserErr) {
util.destroy(this, parserErr)
}
return return
} }
@@ -704,10 +769,11 @@ async function connectH1 (client, socket) {
const client = this[kClient] const client = this[kClient]
const parser = this[kParser] const parser = this[kParser]
clearIdleSocketValidation(this)
if (parser) { if (parser) {
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
// We treat all incoming data so far as a valid response. this[kError] = parser.finish() || this[kError]
parser.onMessageComplete()
} }
this[kParser].destroy() this[kParser].destroy()
@@ -770,7 +836,7 @@ async function connectH1 (client, socket) {
return socket.destroyed return socket.destroyed
}, },
busy (request) { busy (request) {
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
return true return true
} }
@@ -808,6 +874,31 @@ async function connectH1 (client, socket) {
} }
} }
function clearIdleSocketValidation (socket) {
if (socket[kIdleSocketValidationTimeout]) {
clearTimeout(socket[kIdleSocketValidationTimeout])
socket[kIdleSocketValidationTimeout] = null
}
socket[kIdleSocketValidation] = 0
}
function scheduleIdleSocketValidation (client, socket) {
socket[kIdleSocketValidation] = 1
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
socket[kIdleSocketValidationTimeout] = null
socket[kIdleSocketValidation] = 2
if (client[kSocket] === socket && !socket.destroyed) {
client[kResume]()
}
}, 0)
socket[kIdleSocketValidationTimeout].unref?.()
}
/**
* @param {import('./client.js')} client
*/
function resumeH1 (client) { function resumeH1 (client) {
const socket = client[kSocket] const socket = client[kSocket]
@@ -822,6 +913,32 @@ function resumeH1 (client) {
socket[kNoRef] = false socket[kNoRef] = false
} }
if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
if (socket[kIdleSocketValidation] === 0) {
scheduleIdleSocketValidation(client, socket)
socket[kParser].readMore()
if (socket.destroyed) {
return
}
return
}
if (socket[kIdleSocketValidation] === 1) {
socket[kParser].readMore()
if (socket.destroyed) {
return
}
return
}
}
if (client[kRunning] === 0) {
socket[kParser].readMore()
if (socket.destroyed) {
return
}
}
if (client[kSize] === 0) { if (client[kSize] === 0) {
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
@@ -877,8 +994,16 @@ function writeH1 (client, request) {
} }
body = bodyStream.stream body = bodyStream.stream
contentLength = bodyStream.length contentLength = bodyStream.length
} else if (util.isBlobLike(body) && request.contentType == null && body.type) { } else if (util.isBlobLike(body) && request.contentType == null) {
headers.push('content-type', body.type) const contentType = body.type
if (contentType) {
const contentTypeValue = `${contentType}`
if (!util.isValidHeaderValue(contentTypeValue)) {
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
return false
}
headers.push('content-type', contentTypeValue)
}
} }
if (body && typeof body.read === 'function') { if (body && typeof body.read === 'function') {
@@ -915,6 +1040,7 @@ function writeH1 (client, request) {
} }
const socket = client[kSocket] const socket = client[kSocket]
clearIdleSocketValidation(socket)
const abort = (err) => { const abort = (err) => {
if (request.aborted || request.completed) { if (request.aborted || request.completed) {
+3 -2
View File
@@ -106,9 +106,10 @@ class Client extends DispatcherBase {
autoSelectFamilyAttemptTimeout, autoSelectFamilyAttemptTimeout,
// h2 // h2
maxConcurrentStreams, maxConcurrentStreams,
allowH2 allowH2,
webSocket
} = {}) { } = {}) {
super() super({ webSocket })
if (keepAlive !== undefined) { if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
+10 -1
View File
@@ -11,15 +11,24 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = requ
const kOnDestroyed = Symbol('onDestroyed') const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed') const kOnClosed = Symbol('onClosed')
const kInterceptedDispatch = Symbol('Intercepted Dispatch') const kInterceptedDispatch = Symbol('Intercepted Dispatch')
const kWebSocketOptions = Symbol('webSocketOptions')
class DispatcherBase extends Dispatcher { class DispatcherBase extends Dispatcher {
constructor () { constructor (opts) {
super() super()
this[kDestroyed] = false this[kDestroyed] = false
this[kOnDestroyed] = null this[kOnDestroyed] = null
this[kClosed] = false this[kClosed] = false
this[kOnClosed] = [] this[kOnClosed] = []
this[kWebSocketOptions] = opts?.webSocket ?? {}
}
get webSocketOptions () {
return {
maxFragments: this[kWebSocketOptions].maxFragments ?? 131072,
maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024
}
} }
get destroyed () { get destroyed () {
+2 -2
View File
@@ -19,8 +19,8 @@ const kRemoveClient = Symbol('remove client')
const kStats = Symbol('stats') const kStats = Symbol('stats')
class PoolBase extends DispatcherBase { class PoolBase extends DispatcherBase {
constructor () { constructor (opts) {
super() super(opts)
this[kQueue] = new FixedQueue() this[kQueue] = new FixedQueue()
this[kClients] = [] this[kClients] = []
+2 -2
View File
@@ -37,8 +37,6 @@ class Pool extends PoolBase {
allowH2, allowH2,
...options ...options
} = {}) { } = {}) {
super()
if (connections != null && (!Number.isFinite(connections) || connections < 0)) { if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
throw new InvalidArgumentError('invalid connections') throw new InvalidArgumentError('invalid connections')
} }
@@ -63,6 +61,8 @@ class Pool extends PoolBase {
}) })
} }
super(options)
this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool) this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool)
? options.interceptors.Pool ? options.interceptors.Pool
: [] : []
+34
View File
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
return new Date(retryAfter).getTime() - current return new Date(retryAfter).getTime() - current
} }
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
const contentLength = headers['content-length']
if (contentLength == null) {
return null
}
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
return null
}
const length = Number(contentLength)
const expectedLength = range.end - range.start + 1
if (!Number.isFinite(length) || length !== expectedLength) {
return new RequestRetryError('Content-Length mismatch', statusCode, {
headers,
data: { count: retryCount }
})
}
return null
}
class RetryHandler { class RetryHandler {
constructor (opts, handlers) { constructor (opts, handlers) {
const { retryOptions, ...dispatchOpts } = opts const { retryOptions, ...dispatchOpts } = opts
@@ -229,6 +251,12 @@ class RetryHandler {
return false return false
} }
const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
if (contentLengthError != null) {
this.abort(contentLengthError)
return false
}
const { start, size, end = size - 1 } = contentRange const { start, size, end = size - 1 } = contentRange
assert(this.start === start, 'content-range mismatch') assert(this.start === start, 'content-range mismatch')
@@ -252,6 +280,12 @@ class RetryHandler {
) )
} }
const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
if (contentLengthError != null) {
this.abort(contentLengthError)
return false
}
const { start, size, end = size - 1 } = range const { start, size, end = size - 1 } = range
assert( assert(
start != null && Number.isFinite(start), start != null && Number.isFinite(start),
+15 -22
View File
@@ -275,32 +275,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
// If the attribute-name case-insensitively matches the string // If the attribute-name case-insensitively matches the string
// "SameSite", the user agent MUST process the cookie-av as follows: // "SameSite", the user agent MUST process the cookie-av as follows:
// 1. Let enforcement be "Default".
let enforcement = 'Default'
const attributeValueLowercase = attributeValue.toLowerCase() const attributeValueLowercase = attributeValue.toLowerCase()
// 1. If cookie-av's attribute-value is a case-insensitive match for
// "None", append an attribute to the cookie-attribute-list with an
// attribute-name of "SameSite" and an attribute-value of "None".
if (attributeValueLowercase === 'none') {
cookieAttributeList.sameSite = 'None'
} else if (attributeValueLowercase === 'strict') {
// 2. If cookie-av's attribute-value is a case-insensitive match for // 2. If cookie-av's attribute-value is a case-insensitive match for
// "None", set enforcement to "None". // "Strict", append an attribute to the cookie-attribute-list with
if (attributeValueLowercase.includes('none')) { // an attribute-name of "SameSite" and an attribute-value of
enforcement = 'None' // "Strict".
} cookieAttributeList.sameSite = 'Strict'
} else if (attributeValueLowercase === 'lax') {
// 3. If cookie-av's attribute-value is a case-insensitive match for // 3. If cookie-av's attribute-value is a case-insensitive match for
// "Strict", set enforcement to "Strict". // "Lax", append an attribute to the cookie-attribute-list with an
if (attributeValueLowercase.includes('strict')) { // attribute-name of "SameSite" and an attribute-value of "Lax".
enforcement = 'Strict' cookieAttributeList.sameSite = 'Lax'
} }
// 4. If cookie-av's attribute-value is a case-insensitive match for
// "Lax", set enforcement to "Lax".
if (attributeValueLowercase.includes('lax')) {
enforcement = 'Lax'
}
// 5. Append an attribute to the cookie-attribute-list with an
// attribute-name of "SameSite" and an attribute-value of
// enforcement.
cookieAttributeList.sameSite = enforcement
} else { } else {
cookieAttributeList.unparsed ??= [] cookieAttributeList.unparsed ??= []
+79 -9
View File
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
if ( if (
code < 0x20 || // exclude CTLs (0-31) code < 0x20 || // exclude CTLs (0-31)
code === 0x7F || // DEL code > 0x7E || // exclude DEL and non-ascii
code === 0x3B // ; code === 0x3B // ;
) { ) {
throw new Error('Invalid cookie path') throw new Error('Invalid cookie path')
@@ -114,16 +114,80 @@ function validateCookiePath (path) {
} }
/** /**
* I have no idea why these values aren't allowed to be honest, * <let-dig> ::= <letter> | <digit>
* but Deno tests these. - Khafra *
* <letter> ::= any one of the 52 alphabetic characters A through Z in
* upper case and a through z in lower case
*
* <digit> ::= any one of the ten digits 0 through 9r
*
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
* @param {number} code
*/
function isLetterOrDigit (code) {
return (
(code >= 0x30 && code <= 0x39) || // 0-9
(code >= 0x41 && code <= 0x5A) || // A-Z
(code >= 0x61 && code <= 0x7A) // a-z
)
}
/**
* Validates a cookie domain against the "preferred name syntax".
*
* <domain> ::= <subdomain> | " "
* <subdomain> ::= <label> | <subdomain> "." <label>
* <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
* <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
* <let-dig-hyp> ::= <let-dig> | "-"
*
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
* @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
* @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
* @param {string} domain * @param {string} domain
*/ */
function validateCookieDomain (domain) { function validateCookieDomain (domain) {
if ( // <domain> ::= <subdomain> | " "
domain.startsWith('-') || if (domain === ' ') {
domain.endsWith('.') || return
domain.endsWith('-') }
) {
if (domain.length > 255) {
throw new Error('Invalid cookie domain')
}
let labelLength = 0
for (let i = 0; i < domain.length; ++i) {
const code = domain.charCodeAt(i)
if (code === 0x2E) {
if (labelLength === 0) {
throw new Error('Invalid cookie domain')
}
if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
throw new Error('Invalid cookie domain')
}
labelLength = 0
continue
}
if (labelLength === 0 && !isLetterOrDigit(code)) {
throw new Error('Invalid cookie domain')
}
if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
throw new Error('Invalid cookie domain')
}
if (++labelLength > 63) {
throw new Error('Invalid cookie domain')
}
}
if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
throw new Error('Invalid cookie domain') throw new Error('Invalid cookie domain')
} }
} }
@@ -266,7 +330,13 @@ function stringify (cookie) {
const [key, ...value] = part.split('=') const [key, ...value] = part.split('=')
out.push(`${key.trim()}=${value.join('=')}`) const trimmedKey = key.trim()
const joinedValue = value.join('=')
validateCookieName(trimmedKey)
validateCookieValue(joinedValue)
out.push(`${trimmedKey}=${joinedValue}`)
} }
return out.join('; ') return out.join('; ')
+13 -36
View File
@@ -8,45 +8,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
const kBuffer = Symbol('kBuffer') const kBuffer = Symbol('kBuffer')
const kLength = Symbol('kLength') const kLength = Symbol('kLength')
// Default maximum decompressed message size: 4 MB
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
class PerMessageDeflate { class PerMessageDeflate {
/** @type {import('node:zlib').InflateRaw} */ /** @type {import('node:zlib').InflateRaw} */
#inflate #inflate
#options = {} #options = {}
/** @type {number} */ #maxPayloadSize = 0
#maxDecompressedSize
/** @type {boolean} */
#aborted = false
/** @type {Function|null} */
#currentCallback = null
/** /**
* @param {Map<string, string>} extensions * @param {Map<string, string>} extensions
* @param {{ maxDecompressedMessageSize?: number }} [options]
*/ */
constructor (extensions, options = {}) { constructor (extensions, options) {
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover') this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits') this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
this.#maxDecompressedSize = options.maxDecompressedMessageSize ?? kDefaultMaxDecompressedSize
this.#maxPayloadSize = options.maxPayloadSize
} }
/**
* Decompress a compressed payload.
* @param {Buffer} chunk Compressed data
* @param {boolean} fin Final fragment flag
* @param {Function} callback Callback function
*/
decompress (chunk, fin, callback) { decompress (chunk, fin, callback) {
// An endpoint uses the following algorithm to decompress a message. // An endpoint uses the following algorithm to decompress a message.
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
// payload of the message. // payload of the message.
// 2. Decompress the resulting data using DEFLATE. // 2. Decompress the resulting data using DEFLATE.
if (this.#aborted) {
callback(new MessageSizeExceededError())
return
}
if (!this.#inflate) { if (!this.#inflate) {
let windowBits = Z_DEFAULT_WINDOWBITS let windowBits = Z_DEFAULT_WINDOWBITS
@@ -69,23 +59,12 @@ class PerMessageDeflate {
this.#inflate[kLength] = 0 this.#inflate[kLength] = 0
this.#inflate.on('data', (data) => { this.#inflate.on('data', (data) => {
if (this.#aborted) {
return
}
this.#inflate[kLength] += data.length this.#inflate[kLength] += data.length
if (this.#inflate[kLength] > this.#maxDecompressedSize) { if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
this.#aborted = true callback(new MessageSizeExceededError())
this.#inflate.removeAllListeners() this.#inflate.removeAllListeners()
this.#inflate.destroy()
this.#inflate = null this.#inflate = null
if (this.#currentCallback) {
const cb = this.#currentCallback
this.#currentCallback = null
cb(new MessageSizeExceededError())
}
return return
} }
@@ -98,14 +77,13 @@ class PerMessageDeflate {
}) })
} }
this.#currentCallback = callback
this.#inflate.write(chunk) this.#inflate.write(chunk)
if (fin) { if (fin) {
this.#inflate.write(tail) this.#inflate.write(tail)
} }
this.#inflate.flush(() => { this.#inflate.flush(() => {
if (this.#aborted || !this.#inflate) { if (!this.#inflate) {
return return
} }
@@ -113,7 +91,6 @@ class PerMessageDeflate {
this.#inflate[kBuffer].length = 0 this.#inflate[kBuffer].length = 0
this.#inflate[kLength] = 0 this.#inflate[kLength] = 0
this.#currentCallback = null
callback(null, full) callback(null, full)
}) })
+95 -14
View File
@@ -18,6 +18,12 @@ const {
const { WebsocketFrameSend } = require('./frame') const { WebsocketFrameSend } = require('./frame')
const { closeWebSocketConnection } = require('./connection') const { closeWebSocketConnection } = require('./connection')
const { PerMessageDeflate } = require('./permessage-deflate') const { PerMessageDeflate } = require('./permessage-deflate')
const { MessageSizeExceededError } = require('../../core/errors')
function failWebsocketConnectionWithCode (ws, code, reason) {
closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason))
failWebsocketConnection(ws, reason)
}
// This code was influenced by ws released under the MIT license. // This code was influenced by ws released under the MIT license.
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com> // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
@@ -26,6 +32,7 @@ const { PerMessageDeflate } = require('./permessage-deflate')
class ByteParser extends Writable { class ByteParser extends Writable {
#buffers = [] #buffers = []
#fragmentsBytes = 0
#byteOffset = 0 #byteOffset = 0
#loop = false #loop = false
@@ -37,20 +44,24 @@ class ByteParser extends Writable {
/** @type {Map<string, PerMessageDeflate>} */ /** @type {Map<string, PerMessageDeflate>} */
#extensions #extensions
/** @type {{ maxDecompressedMessageSize?: number }} */ /** @type {number} */
#options #maxFragments
/** @type {number} */
#maxPayloadSize
/** /**
* @param {import('./websocket').WebSocket} ws * @param {import('./websocket').WebSocket} ws
* @param {Map<string, string>|null} extensions * @param {Map<string, string>|null} extensions
* @param {{ maxDecompressedMessageSize?: number }} [options] * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options]
*/ */
constructor (ws, extensions, options = {}) { constructor (ws, extensions, options = {}) {
super() super()
this.ws = ws this.ws = ws
this.#extensions = extensions == null ? new Map() : extensions this.#extensions = extensions == null ? new Map() : extensions
this.#options = options this.#maxFragments = options.maxFragments ?? 0
this.#maxPayloadSize = options.maxPayloadSize ?? 0
if (this.#extensions.has('permessage-deflate')) { if (this.#extensions.has('permessage-deflate')) {
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options)) this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
@@ -69,6 +80,19 @@ class ByteParser extends Writable {
this.run(callback) this.run(callback)
} }
#validatePayloadLength () {
if (
this.#maxPayloadSize > 0 &&
!isControlFrame(this.#info.opcode) &&
this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
) {
failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size')
return false
}
return true
}
/** /**
* Runs whenever a new chunk is received. * Runs whenever a new chunk is received.
* Callback is called whenever there are no more chunks buffering, * Callback is called whenever there are no more chunks buffering,
@@ -157,6 +181,10 @@ class ByteParser extends Writable {
if (payloadLength <= 125) { if (payloadLength <= 125) {
this.#info.payloadLength = payloadLength this.#info.payloadLength = payloadLength
this.#state = parserStates.READ_DATA this.#state = parserStates.READ_DATA
if (!this.#validatePayloadLength()) {
return
}
} else if (payloadLength === 126) { } else if (payloadLength === 126) {
this.#state = parserStates.PAYLOADLENGTH_16 this.#state = parserStates.PAYLOADLENGTH_16
} else if (payloadLength === 127) { } else if (payloadLength === 127) {
@@ -181,6 +209,10 @@ class ByteParser extends Writable {
this.#info.payloadLength = buffer.readUInt16BE(0) this.#info.payloadLength = buffer.readUInt16BE(0)
this.#state = parserStates.READ_DATA this.#state = parserStates.READ_DATA
if (!this.#validatePayloadLength()) {
return
}
} else if (this.#state === parserStates.PAYLOADLENGTH_64) { } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
if (this.#byteOffset < 8) { if (this.#byteOffset < 8) {
return callback() return callback()
@@ -203,6 +235,10 @@ class ByteParser extends Writable {
this.#info.payloadLength = lower this.#info.payloadLength = lower
this.#state = parserStates.READ_DATA this.#state = parserStates.READ_DATA
if (!this.#validatePayloadLength()) {
return
}
} else if (this.#state === parserStates.READ_DATA) { } else if (this.#state === parserStates.READ_DATA) {
if (this.#byteOffset < this.#info.payloadLength) { if (this.#byteOffset < this.#info.payloadLength) {
return callback() return callback()
@@ -215,27 +251,43 @@ class ByteParser extends Writable {
this.#state = parserStates.INFO this.#state = parserStates.INFO
} else { } else {
if (!this.#info.compressed) { if (!this.#info.compressed) {
this.#fragments.push(body) if (!this.writeFragments(body)) {
return
}
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
return
}
// If the frame is not fragmented, a message has been received. // If the frame is not fragmented, a message has been received.
// If the frame is fragmented, it will terminate with a fin bit set // If the frame is fragmented, it will terminate with a fin bit set
// and an opcode of 0 (continuation), therefore we handle that when // and an opcode of 0 (continuation), therefore we handle that when
// parsing continuation frames, not here. // parsing continuation frames, not here.
if (!this.#info.fragmented && this.#info.fin) { if (!this.#info.fragmented && this.#info.fin) {
const fullMessage = Buffer.concat(this.#fragments) websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
this.#fragments.length = 0
} }
this.#state = parserStates.INFO this.#state = parserStates.INFO
} else { } else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { this.#extensions.get('permessage-deflate').decompress(
body,
this.#info.fin,
(error, data) => {
if (error) { if (error) {
failWebsocketConnection(this.ws, error.message) const code = error instanceof MessageSizeExceededError ? 1009 : 1007
failWebsocketConnectionWithCode(this.ws, code, error.message)
return return
} }
this.#fragments.push(data) if (!this.writeFragments(data)) {
return
}
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
return
}
if (!this.#info.fin) { if (!this.#info.fin) {
this.#state = parserStates.INFO this.#state = parserStates.INFO
@@ -244,13 +296,13 @@ class ByteParser extends Writable {
return return
} }
websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments)) websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
this.#loop = true this.#loop = true
this.#state = parserStates.INFO this.#state = parserStates.INFO
this.#fragments.length = 0
this.run(callback) this.run(callback)
}) }
)
this.#loop = false this.#loop = false
break break
@@ -302,6 +354,35 @@ class ByteParser extends Writable {
return buffer return buffer
} }
writeFragments (fragment) {
if (
this.#maxFragments > 0 &&
this.#fragments.length === this.#maxFragments
) {
failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments')
return false
}
this.#fragmentsBytes += fragment.length
this.#fragments.push(fragment)
return true
}
consumeFragments () {
const fragments = this.#fragments
if (fragments.length === 1) {
this.#fragmentsBytes = 0
return fragments.shift()
}
const output = Buffer.concat(fragments, this.#fragmentsBytes)
this.#fragments = []
this.#fragmentsBytes = 0
return output
}
parseCloseBody (data) { parseCloseBody (data) {
assert(data.length !== 1) assert(data.length !== 1)
+8 -22
View File
@@ -44,9 +44,6 @@ class WebSocket extends EventTarget {
/** @type {SendQueue} */ /** @type {SendQueue} */
#sendQueue #sendQueue
/** @type {{ maxDecompressedMessageSize?: number }} */
#options
/** /**
* @param {string} url * @param {string} url
* @param {string|string[]} protocols * @param {string|string[]} protocols
@@ -120,11 +117,6 @@ class WebSocket extends EventTarget {
// 10. Set this's url to urlRecord. // 10. Set this's url to urlRecord.
this[kWebSocketURL] = new URL(urlRecord.href) this[kWebSocketURL] = new URL(urlRecord.href)
// Store options for later use (e.g., maxDecompressedMessageSize)
this.#options = {
maxDecompressedMessageSize: options.maxDecompressedMessageSize
}
// 11. Let client be this's relevant settings object. // 11. Let client be this's relevant settings object.
const client = environmentSettingsObject.settingsObject const client = environmentSettingsObject.settingsObject
@@ -443,7 +435,14 @@ class WebSocket extends EventTarget {
// once this happens, the connection is open // once this happens, the connection is open
this[kResponse] = response this[kResponse] = response
const parser = new ByteParser(this, parsedExtensions, this.#options) const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions
const maxFragments = webSocketOptions?.maxFragments
const maxPayloadSize = webSocketOptions?.maxPayloadSize
const parser = new ByteParser(this, parsedExtensions, {
maxFragments,
maxPayloadSize
})
parser.on('drain', onParserDrain) parser.on('drain', onParserDrain)
parser.on('error', onParserError.bind(this)) parser.on('error', onParserError.bind(this))
@@ -546,19 +545,6 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([
{ {
key: 'headers', key: 'headers',
converter: webidl.nullableConverter(webidl.converters.HeadersInit) converter: webidl.nullableConverter(webidl.converters.HeadersInit)
},
{
key: 'maxDecompressedMessageSize',
converter: webidl.nullableConverter((V) => {
V = webidl.converters['unsigned long long'](V)
if (V <= 0) {
throw webidl.errors.exception({
header: 'WebSocket constructor',
message: 'maxDecompressedMessageSize must be greater than 0'
})
}
return V
})
} }
]) ])
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "undici", "name": "undici",
"version": "6.24.0", "version": "6.28.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js", "description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org", "homepage": "https://undici.nodejs.org",
"bugs": { "bugs": {
+17
View File
@@ -78,6 +78,8 @@ export declare namespace Client {
localAddress?: string; localAddress?: string;
/** Max response body size in bytes, -1 is disabled */ /** Max response body size in bytes, -1 is disabled */
maxResponseSize?: number; maxResponseSize?: number;
/** WebSocket-specific options */
webSocket?: Client.WebSocketOptions;
/** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */ /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
autoSelectFamily?: boolean; autoSelectFamily?: boolean;
/** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
@@ -103,6 +105,21 @@ export declare namespace Client {
bytesWritten?: number bytesWritten?: number
bytesRead?: number bytesRead?: number
} }
export interface WebSocketOptions {
/**
* Maximum number of fragments in a message.
* Set to 0 to disable the limit.
* @default 131072
*/
maxFragments?: number;
/**
* Maximum allowed payload size in bytes for WebSocket messages.
* Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.
* Set to 0 to disable the limit.
* @default 134217728 (128 MB)
*/
maxPayloadSize?: number;
}
} }
export default Client; export default Client;
+1 -8
View File
@@ -146,12 +146,5 @@ export declare const ErrorEvent: {
interface WebSocketInit { interface WebSocketInit {
protocols?: string | string[], protocols?: string | string[],
dispatcher?: Dispatcher, dispatcher?: Dispatcher,
headers?: HeadersInit, headers?: HeadersInit
/**
* Maximum size in bytes for decompressed WebSocket messages.
* When a message exceeds this limit during decompression, the connection
* will be closed with status code 1009 (Message Too Big).
* @default 4194304 (4 MB)
*/
maxDecompressedMessageSize?: number
} }
+8 -1
View File
@@ -28,19 +28,26 @@ function composeNode(ctx, token, props, onError) {
case 'block-map': case 'block-map':
case 'block-seq': case 'block-seq':
case 'flow-collection': case 'flow-collection':
try {
node = composeCollection(CN, ctx, token, props, onError); node = composeCollection(CN, ctx, token, props, onError);
if (anchor) if (anchor)
node.anchor = anchor.source.substring(1); node.anchor = anchor.source.substring(1);
}
catch (error) {
// Almost certainly here due to a stack overflow
const message = error instanceof Error ? error.message : String(error);
onError(token, 'RESOURCE_EXHAUSTION', message);
}
break; break;
default: { default: {
const message = token.type === 'error' const message = token.type === 'error'
? token.message ? token.message
: `Unsupported token (type: ${token.type})`; : `Unsupported token (type: ${token.type})`;
onError(token, 'UNEXPECTED_TOKEN', message); onError(token, 'UNEXPECTED_TOKEN', message);
node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError);
isSrcToken = false; isSrcToken = false;
} }
} }
node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
if (anchor && node.anchor === '') if (anchor && node.anchor === '')
onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string'); onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
if (atKey && if (atKey &&
+4 -2
View File
@@ -94,8 +94,10 @@ class Composer {
} }
} }
if (afterDoc) { if (afterDoc) {
Array.prototype.push.apply(doc.errors, this.errors); for (let i = 0; i < this.errors.length; ++i)
Array.prototype.push.apply(doc.warnings, this.warnings); doc.errors.push(this.errors[i]);
for (let i = 0; i < this.warnings.length; ++i)
doc.warnings.push(this.warnings[i]);
} }
else { else {
doc.errors = this.errors; doc.errors = this.errors;
+5 -3
View File
@@ -142,7 +142,7 @@ function doubleQuotedValue(source, onError) {
next = source[++i + 1]; next = source[++i + 1];
} }
else if (next === 'x' || next === 'u' || next === 'U') { else if (next === 'x' || next === 'u' || next === 'U') {
const length = { x: 2, u: 4, U: 8 }[next]; const length = next === 'x' ? 2 : next === 'u' ? 4 : 8;
res += parseCharCode(source, i + 1, length, onError); res += parseCharCode(source, i + 1, length, onError);
i += length; i += length;
} }
@@ -212,12 +212,14 @@ function parseCharCode(source, offset, length, onError) {
const cc = source.substr(offset, length); const cc = source.substr(offset, length);
const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc); const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc);
const code = ok ? parseInt(cc, 16) : NaN; const code = ok ? parseInt(cc, 16) : NaN;
if (isNaN(code)) { try {
return String.fromCodePoint(code);
}
catch {
const raw = source.substr(offset - 2, length + 2); const raw = source.substr(offset - 2, length + 2);
onError(offset - 2, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`); onError(offset - 2, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`);
return raw; return raw;
} }
return String.fromCodePoint(code);
} }
export { resolveFlowScalar }; export { resolveFlowScalar };
+2
View File
@@ -19,6 +19,8 @@ class Alias extends NodeBase {
* instance of the `source` anchor before this node. * instance of the `source` anchor before this node.
*/ */
resolve(doc, ctx) { resolve(doc, ctx) {
if (ctx?.maxAliasCount === 0)
throw new ReferenceError('Alias resolution is disabled');
let nodes; let nodes;
if (ctx?.aliasResolveCache) { if (ctx?.aliasResolveCache) {
nodes = ctx.aliasResolveCache; nodes = ctx.aliasResolveCache;
+15 -11
View File
@@ -310,7 +310,7 @@ class Lexer {
const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)); const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true));
this.indentNext = this.indentValue + 1; this.indentNext = this.indentValue + 1;
this.indentValue += n; this.indentValue += n;
return yield* this.parseBlockStart(); return 'block-start';
} }
return 'doc'; return 'doc';
} }
@@ -631,15 +631,17 @@ class Lexer {
return 0; return 0;
} }
*pushIndicators() { *pushIndicators() {
let n = 0;
loop: while (true) {
switch (this.charAt(0)) { switch (this.charAt(0)) {
case '!': case '!':
return ((yield* this.pushTag()) + n += yield* this.pushTag();
(yield* this.pushSpaces(true)) + n += yield* this.pushSpaces(true);
(yield* this.pushIndicators())); continue loop;
case '&': case '&':
return ((yield* this.pushUntil(isNotAnchorChar)) + n += yield* this.pushUntil(isNotAnchorChar);
(yield* this.pushSpaces(true)) + n += yield* this.pushSpaces(true);
(yield* this.pushIndicators())); continue loop;
case '-': // this is an error case '-': // this is an error
case '?': // this is an error outside flow collections case '?': // this is an error outside flow collections
case ':': { case ':': {
@@ -650,13 +652,15 @@ class Lexer {
this.indentNext = this.indentValue + 1; this.indentNext = this.indentValue + 1;
else if (this.flowKey) else if (this.flowKey)
this.flowKey = false; this.flowKey = false;
return ((yield* this.pushCount(1)) + n += yield* this.pushCount(1);
(yield* this.pushSpaces(true)) + n += yield* this.pushSpaces(true);
(yield* this.pushIndicators())); continue loop;
} }
} }
} }
return 0; break loop;
}
return n;
} }
*pushTag() { *pushTag() {
if (this.charAt(1) === '<') { if (this.charAt(1) === '<') {
+12 -4
View File
@@ -67,6 +67,14 @@ function getFirstKeyStartProps(prev) {
} }
return prev.splice(i, prev.length); return prev.splice(i, prev.length);
} }
function arrayPushArray(target, source) {
// May exhaust call stack with large `source` array
if (source.length < 1e5)
Array.prototype.push.apply(target, source);
else
for (let i = 0; i < source.length; ++i)
target.push(source[i]);
}
function fixFlowSeqItems(fc) { function fixFlowSeqItems(fc) {
if (fc.start.type === 'flow-seq-start') { if (fc.start.type === 'flow-seq-start') {
for (const it of fc.items) { for (const it of fc.items) {
@@ -79,12 +87,12 @@ function fixFlowSeqItems(fc) {
delete it.key; delete it.key;
if (isFlowToken(it.value)) { if (isFlowToken(it.value)) {
if (it.value.end) if (it.value.end)
Array.prototype.push.apply(it.value.end, it.sep); arrayPushArray(it.value.end, it.sep);
else else
it.value.end = it.sep; it.value.end = it.sep;
} }
else else
Array.prototype.push.apply(it.start, it.sep); arrayPushArray(it.start, it.sep);
delete it.sep; delete it.sep;
} }
} }
@@ -502,7 +510,7 @@ class Parser {
const prev = map.items[map.items.length - 2]; const prev = map.items[map.items.length - 2];
const end = prev?.value?.end; const end = prev?.value?.end;
if (Array.isArray(end)) { if (Array.isArray(end)) {
Array.prototype.push.apply(end, it.start); arrayPushArray(end, it.start);
end.push(this.sourceToken); end.push(this.sourceToken);
map.items.pop(); map.items.pop();
return; return;
@@ -717,7 +725,7 @@ class Parser {
const prev = seq.items[seq.items.length - 2]; const prev = seq.items[seq.items.length - 2];
const end = prev?.value?.end; const end = prev?.value?.end;
if (Array.isArray(end)) { if (Array.isArray(end)) {
Array.prototype.push.apply(end, it.start); arrayPushArray(end, it.start);
end.push(this.sourceToken); end.push(this.sourceToken);
seq.items.pop(); seq.items.pop();
return; return;
+11 -8
View File
@@ -1,4 +1,4 @@
import { isScalar, isAlias, isSeq, isMap } from '../../nodes/identity.js'; import { isScalar, isSeq, isAlias, isMap } from '../../nodes/identity.js';
import { Scalar } from '../../nodes/Scalar.js'; import { Scalar } from '../../nodes/Scalar.js';
// If the value associated with a merge key is a single mapping node, each of // If the value associated with a merge key is a single mapping node, each of
@@ -26,18 +26,18 @@ const isMergeKey = (ctx, key) => (merge.identify(key) ||
merge.identify(key.value))) && merge.identify(key.value))) &&
ctx?.doc.schema.tags.some(tag => tag.tag === merge.tag && tag.default); ctx?.doc.schema.tags.some(tag => tag.tag === merge.tag && tag.default);
function addMergeToJSMap(ctx, map, value) { function addMergeToJSMap(ctx, map, value) {
value = ctx && isAlias(value) ? value.resolve(ctx.doc) : value; const source = resolveAliasValue(ctx, value);
if (isSeq(value)) if (isSeq(source))
for (const it of value.items) for (const it of source.items)
mergeValue(ctx, map, it); mergeValue(ctx, map, it);
else if (Array.isArray(value)) else if (Array.isArray(source))
for (const it of value) for (const it of source)
mergeValue(ctx, map, it); mergeValue(ctx, map, it);
else else
mergeValue(ctx, map, value); mergeValue(ctx, map, source);
} }
function mergeValue(ctx, map, value) { function mergeValue(ctx, map, value) {
const source = ctx && isAlias(value) ? value.resolve(ctx.doc) : value; const source = resolveAliasValue(ctx, value);
if (!isMap(source)) if (!isMap(source))
throw new Error('Merge sources must be maps or map aliases'); throw new Error('Merge sources must be maps or map aliases');
const srcMap = source.toJSON(null, ctx, Map); const srcMap = source.toJSON(null, ctx, Map);
@@ -60,5 +60,8 @@ function mergeValue(ctx, map, value) {
} }
return map; return map;
} }
function resolveAliasValue(ctx, value) {
return ctx && isAlias(value) ? value.resolve(ctx.doc, ctx) : value;
}
export { addMergeToJSMap, isMergeKey, merge }; export { addMergeToJSMap, isMergeKey, merge };
+1
View File
@@ -20,6 +20,7 @@ function createStringifyContext(doc, options) {
nullStr: 'null', nullStr: 'null',
simpleKeys: false, simpleKeys: false,
singleQuote: null, singleQuote: null,
trailingComma: false,
trueStr: 'true', trueStr: 'true',
verifyAliasOrder: true verifyAliasOrder: true
}, doc.schema.toStringOptions, options); }, doc.schema.toStringOptions, options);
+13 -3
View File
@@ -102,12 +102,22 @@ function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
if (comment) if (comment)
reqNewline = true; reqNewline = true;
let str = stringify(item, itemCtx, () => (comment = null)); let str = stringify(item, itemCtx, () => (comment = null));
if (i < items.length - 1) reqNewline || (reqNewline = lines.length > linesAtValue || str.includes('\n'));
if (i < items.length - 1) {
str += ','; str += ',';
}
else if (ctx.options.trailingComma) {
if (ctx.options.lineWidth > 0) {
reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) +
(str.length + 2) >
ctx.options.lineWidth);
}
if (reqNewline) {
str += ',';
}
}
if (comment) if (comment)
str += lineComment(str, itemIndent, commentString(comment)); str += lineComment(str, itemIndent, commentString(comment));
if (!reqNewline && (lines.length > linesAtValue || str.includes('\n')))
reqNewline = true;
lines.push(str); lines.push(str);
linesAtValue = lines.length; linesAtValue = lines.length;
} }
+2 -1
View File
@@ -8,7 +8,8 @@ function stringifyNumber({ format, minFractionDigits, tag, value }) {
if (!format && if (!format &&
minFractionDigits && minFractionDigits &&
(!tag || tag === 'tag:yaml.org,2002:float') && (!tag || tag === 'tag:yaml.org,2002:float') &&
/^\d/.test(n)) { /^-?\d/.test(n) &&
!n.includes('e')) {
let i = n.indexOf('.'); let i = n.indexOf('.');
if (i < 0) { if (i < 0) {
i = n.length; i = n.length;
+8 -1
View File
@@ -30,19 +30,26 @@ function composeNode(ctx, token, props, onError) {
case 'block-map': case 'block-map':
case 'block-seq': case 'block-seq':
case 'flow-collection': case 'flow-collection':
try {
node = composeCollection.composeCollection(CN, ctx, token, props, onError); node = composeCollection.composeCollection(CN, ctx, token, props, onError);
if (anchor) if (anchor)
node.anchor = anchor.source.substring(1); node.anchor = anchor.source.substring(1);
}
catch (error) {
// Almost certainly here due to a stack overflow
const message = error instanceof Error ? error.message : String(error);
onError(token, 'RESOURCE_EXHAUSTION', message);
}
break; break;
default: { default: {
const message = token.type === 'error' const message = token.type === 'error'
? token.message ? token.message
: `Unsupported token (type: ${token.type})`; : `Unsupported token (type: ${token.type})`;
onError(token, 'UNEXPECTED_TOKEN', message); onError(token, 'UNEXPECTED_TOKEN', message);
node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError);
isSrcToken = false; isSrcToken = false;
} }
} }
node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
if (anchor && node.anchor === '') if (anchor && node.anchor === '')
onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string'); onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
if (atKey && if (atKey &&
+4 -2
View File
@@ -97,8 +97,10 @@ class Composer {
} }
} }
if (afterDoc) { if (afterDoc) {
Array.prototype.push.apply(doc.errors, this.errors); for (let i = 0; i < this.errors.length; ++i)
Array.prototype.push.apply(doc.warnings, this.warnings); doc.errors.push(this.errors[i]);
for (let i = 0; i < this.warnings.length; ++i)
doc.warnings.push(this.warnings[i]);
} }
else { else {
doc.errors = this.errors; doc.errors = this.errors;
+5 -3
View File
@@ -144,7 +144,7 @@ function doubleQuotedValue(source, onError) {
next = source[++i + 1]; next = source[++i + 1];
} }
else if (next === 'x' || next === 'u' || next === 'U') { else if (next === 'x' || next === 'u' || next === 'U') {
const length = { x: 2, u: 4, U: 8 }[next]; const length = next === 'x' ? 2 : next === 'u' ? 4 : 8;
res += parseCharCode(source, i + 1, length, onError); res += parseCharCode(source, i + 1, length, onError);
i += length; i += length;
} }
@@ -214,12 +214,14 @@ function parseCharCode(source, offset, length, onError) {
const cc = source.substr(offset, length); const cc = source.substr(offset, length);
const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc); const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc);
const code = ok ? parseInt(cc, 16) : NaN; const code = ok ? parseInt(cc, 16) : NaN;
if (isNaN(code)) { try {
return String.fromCodePoint(code);
}
catch {
const raw = source.substr(offset - 2, length + 2); const raw = source.substr(offset - 2, length + 2);
onError(offset - 2, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`); onError(offset - 2, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`);
return raw; return raw;
} }
return String.fromCodePoint(code);
} }
exports.resolveFlowScalar = resolveFlowScalar; exports.resolveFlowScalar = resolveFlowScalar;
+1 -1
View File
@@ -1,5 +1,5 @@
import type { LineCounter } from './parse/line-counter'; import type { LineCounter } from './parse/line-counter';
export type ErrorCode = 'ALIAS_PROPS' | 'BAD_ALIAS' | 'BAD_DIRECTIVE' | 'BAD_DQ_ESCAPE' | 'BAD_INDENT' | 'BAD_PROP_ORDER' | 'BAD_SCALAR_START' | 'BLOCK_AS_IMPLICIT_KEY' | 'BLOCK_IN_FLOW' | 'DUPLICATE_KEY' | 'IMPOSSIBLE' | 'KEY_OVER_1024_CHARS' | 'MISSING_CHAR' | 'MULTILINE_IMPLICIT_KEY' | 'MULTIPLE_ANCHORS' | 'MULTIPLE_DOCS' | 'MULTIPLE_TAGS' | 'NON_STRING_KEY' | 'TAB_AS_INDENT' | 'TAG_RESOLVE_FAILED' | 'UNEXPECTED_TOKEN' | 'BAD_COLLECTION_TYPE'; export type ErrorCode = 'ALIAS_PROPS' | 'BAD_ALIAS' | 'BAD_DIRECTIVE' | 'BAD_DQ_ESCAPE' | 'BAD_INDENT' | 'BAD_PROP_ORDER' | 'BAD_SCALAR_START' | 'BLOCK_AS_IMPLICIT_KEY' | 'BLOCK_IN_FLOW' | 'DUPLICATE_KEY' | 'IMPOSSIBLE' | 'KEY_OVER_1024_CHARS' | 'MISSING_CHAR' | 'MULTILINE_IMPLICIT_KEY' | 'MULTIPLE_ANCHORS' | 'MULTIPLE_DOCS' | 'MULTIPLE_TAGS' | 'NON_STRING_KEY' | 'RESOURCE_EXHAUSTION' | 'TAB_AS_INDENT' | 'TAG_RESOLVE_FAILED' | 'UNEXPECTED_TOKEN' | 'BAD_COLLECTION_TYPE';
export type LinePos = { export type LinePos = {
line: number; line: number;
col: number; col: number;
+2
View File
@@ -21,6 +21,8 @@ class Alias extends Node.NodeBase {
* instance of the `source` anchor before this node. * instance of the `source` anchor before this node.
*/ */
resolve(doc, ctx) { resolve(doc, ctx) {
if (ctx?.maxAliasCount === 0)
throw new ReferenceError('Alias resolution is disabled');
let nodes; let nodes;
if (ctx?.aliasResolveCache) { if (ctx?.aliasResolveCache) {
nodes = ctx.aliasResolveCache; nodes = ctx.aliasResolveCache;
+5 -1
View File
@@ -31,7 +31,11 @@ export declare class Scalar<T = unknown> extends NodeBase {
* The YAML 1.1 schema also supports 'BIN' and 'TIME' * The YAML 1.1 schema also supports 'BIN' and 'TIME'
*/ */
format?: string; format?: string;
/** If `value` is a number, use this value when stringifying this node. */ /**
* If `value` is a number that is serialized as a decimal string
* (i.e. not using exponential notation),
* use this value when stringifying this node.
*/
minFractionDigits?: number; minFractionDigits?: number;
/** Set during parsing to the source string value */ /** Set during parsing to the source string value */
source?: string; source?: string;
+6
View File
@@ -326,6 +326,12 @@ export type ToStringOptions = {
* Default: `null` * Default: `null`
*/ */
singleQuote?: boolean | null; singleQuote?: boolean | null;
/**
* Add a trailing comma after the last entry in a flow map or flow sequence that's split across multiple lines.
*
* Default: `'false'`
*/
trailingComma?: boolean;
/** /**
* String representation for `true`. * String representation for `true`.
* With the core schema, use `'true'`, `'True'`, or `'TRUE'`. * With the core schema, use `'true'`, `'True'`, or `'TRUE'`.
+15 -11
View File
@@ -312,7 +312,7 @@ class Lexer {
const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)); const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true));
this.indentNext = this.indentValue + 1; this.indentNext = this.indentValue + 1;
this.indentValue += n; this.indentValue += n;
return yield* this.parseBlockStart(); return 'block-start';
} }
return 'doc'; return 'doc';
} }
@@ -633,15 +633,17 @@ class Lexer {
return 0; return 0;
} }
*pushIndicators() { *pushIndicators() {
let n = 0;
loop: while (true) {
switch (this.charAt(0)) { switch (this.charAt(0)) {
case '!': case '!':
return ((yield* this.pushTag()) + n += yield* this.pushTag();
(yield* this.pushSpaces(true)) + n += yield* this.pushSpaces(true);
(yield* this.pushIndicators())); continue loop;
case '&': case '&':
return ((yield* this.pushUntil(isNotAnchorChar)) + n += yield* this.pushUntil(isNotAnchorChar);
(yield* this.pushSpaces(true)) + n += yield* this.pushSpaces(true);
(yield* this.pushIndicators())); continue loop;
case '-': // this is an error case '-': // this is an error
case '?': // this is an error outside flow collections case '?': // this is an error outside flow collections
case ':': { case ':': {
@@ -652,13 +654,15 @@ class Lexer {
this.indentNext = this.indentValue + 1; this.indentNext = this.indentValue + 1;
else if (this.flowKey) else if (this.flowKey)
this.flowKey = false; this.flowKey = false;
return ((yield* this.pushCount(1)) + n += yield* this.pushCount(1);
(yield* this.pushSpaces(true)) + n += yield* this.pushSpaces(true);
(yield* this.pushIndicators())); continue loop;
} }
} }
} }
return 0; break loop;
}
return n;
} }
*pushTag() { *pushTag() {
if (this.charAt(1) === '<') { if (this.charAt(1) === '<') {
+12 -4
View File
@@ -70,6 +70,14 @@ function getFirstKeyStartProps(prev) {
} }
return prev.splice(i, prev.length); return prev.splice(i, prev.length);
} }
function arrayPushArray(target, source) {
// May exhaust call stack with large `source` array
if (source.length < 1e5)
Array.prototype.push.apply(target, source);
else
for (let i = 0; i < source.length; ++i)
target.push(source[i]);
}
function fixFlowSeqItems(fc) { function fixFlowSeqItems(fc) {
if (fc.start.type === 'flow-seq-start') { if (fc.start.type === 'flow-seq-start') {
for (const it of fc.items) { for (const it of fc.items) {
@@ -82,12 +90,12 @@ function fixFlowSeqItems(fc) {
delete it.key; delete it.key;
if (isFlowToken(it.value)) { if (isFlowToken(it.value)) {
if (it.value.end) if (it.value.end)
Array.prototype.push.apply(it.value.end, it.sep); arrayPushArray(it.value.end, it.sep);
else else
it.value.end = it.sep; it.value.end = it.sep;
} }
else else
Array.prototype.push.apply(it.start, it.sep); arrayPushArray(it.start, it.sep);
delete it.sep; delete it.sep;
} }
} }
@@ -507,7 +515,7 @@ class Parser {
const prev = map.items[map.items.length - 2]; const prev = map.items[map.items.length - 2];
const end = prev?.value?.end; const end = prev?.value?.end;
if (Array.isArray(end)) { if (Array.isArray(end)) {
Array.prototype.push.apply(end, it.start); arrayPushArray(end, it.start);
end.push(this.sourceToken); end.push(this.sourceToken);
map.items.pop(); map.items.pop();
return; return;
@@ -722,7 +730,7 @@ class Parser {
const prev = seq.items[seq.items.length - 2]; const prev = seq.items[seq.items.length - 2];
const end = prev?.value?.end; const end = prev?.value?.end;
if (Array.isArray(end)) { if (Array.isArray(end)) {
Array.prototype.push.apply(end, it.start); arrayPushArray(end, it.start);
end.push(this.sourceToken); end.push(this.sourceToken);
seq.items.pop(); seq.items.pop();
return; return;
+10 -7
View File
@@ -28,18 +28,18 @@ const isMergeKey = (ctx, key) => (merge.identify(key) ||
merge.identify(key.value))) && merge.identify(key.value))) &&
ctx?.doc.schema.tags.some(tag => tag.tag === merge.tag && tag.default); ctx?.doc.schema.tags.some(tag => tag.tag === merge.tag && tag.default);
function addMergeToJSMap(ctx, map, value) { function addMergeToJSMap(ctx, map, value) {
value = ctx && identity.isAlias(value) ? value.resolve(ctx.doc) : value; const source = resolveAliasValue(ctx, value);
if (identity.isSeq(value)) if (identity.isSeq(source))
for (const it of value.items) for (const it of source.items)
mergeValue(ctx, map, it); mergeValue(ctx, map, it);
else if (Array.isArray(value)) else if (Array.isArray(source))
for (const it of value) for (const it of source)
mergeValue(ctx, map, it); mergeValue(ctx, map, it);
else else
mergeValue(ctx, map, value); mergeValue(ctx, map, source);
} }
function mergeValue(ctx, map, value) { function mergeValue(ctx, map, value) {
const source = ctx && identity.isAlias(value) ? value.resolve(ctx.doc) : value; const source = resolveAliasValue(ctx, value);
if (!identity.isMap(source)) if (!identity.isMap(source))
throw new Error('Merge sources must be maps or map aliases'); throw new Error('Merge sources must be maps or map aliases');
const srcMap = source.toJSON(null, ctx, Map); const srcMap = source.toJSON(null, ctx, Map);
@@ -62,6 +62,9 @@ function mergeValue(ctx, map, value) {
} }
return map; return map;
} }
function resolveAliasValue(ctx, value) {
return ctx && identity.isAlias(value) ? value.resolve(ctx.doc, ctx) : value;
}
exports.addMergeToJSMap = addMergeToJSMap; exports.addMergeToJSMap = addMergeToJSMap;
exports.isMergeKey = isMergeKey; exports.isMergeKey = isMergeKey;
+1
View File
@@ -22,6 +22,7 @@ function createStringifyContext(doc, options) {
nullStr: 'null', nullStr: 'null',
simpleKeys: false, simpleKeys: false,
singleQuote: null, singleQuote: null,
trailingComma: false,
trueStr: 'true', trueStr: 'true',
verifyAliasOrder: true verifyAliasOrder: true
}, doc.schema.toStringOptions, options); }, doc.schema.toStringOptions, options);
+13 -3
View File
@@ -104,12 +104,22 @@ function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
if (comment) if (comment)
reqNewline = true; reqNewline = true;
let str = stringify.stringify(item, itemCtx, () => (comment = null)); let str = stringify.stringify(item, itemCtx, () => (comment = null));
if (i < items.length - 1) reqNewline || (reqNewline = lines.length > linesAtValue || str.includes('\n'));
if (i < items.length - 1) {
str += ','; str += ',';
}
else if (ctx.options.trailingComma) {
if (ctx.options.lineWidth > 0) {
reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) +
(str.length + 2) >
ctx.options.lineWidth);
}
if (reqNewline) {
str += ',';
}
}
if (comment) if (comment)
str += stringifyComment.lineComment(str, itemIndent, commentString(comment)); str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
if (!reqNewline && (lines.length > linesAtValue || str.includes('\n')))
reqNewline = true;
lines.push(str); lines.push(str);
linesAtValue = lines.length; linesAtValue = lines.length;
} }
+2 -1
View File
@@ -10,7 +10,8 @@ function stringifyNumber({ format, minFractionDigits, tag, value }) {
if (!format && if (!format &&
minFractionDigits && minFractionDigits &&
(!tag || tag === 'tag:yaml.org,2002:float') && (!tag || tag === 'tag:yaml.org,2002:float') &&
/^\d/.test(n)) { /^-?\d/.test(n) &&
!n.includes('e')) {
let i = n.indexOf('.'); let i = n.indexOf('.');
if (i < 0) { if (i < 0) {
i = n.length; i = n.length;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "yaml", "name": "yaml",
"version": "2.8.2", "version": "2.9.0",
"license": "ISC", "license": "ISC",
"author": "Eemeli Aro <eemeli@gmail.com>", "author": "Eemeli Aro <eemeli@gmail.com>",
"funding": "https://github.com/sponsors/eemeli", "funding": "https://github.com/sponsors/eemeli",