diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 703c221..2777e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3246bb9..367d837 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,6 +21,8 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup Flutter (stable latest) uses: ./ @@ -40,6 +42,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup Flutter (exact version) uses: ./ @@ -55,6 +59,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup Flutter (dry-run) id: flutter @@ -63,7 +69,10 @@ jobs: dry-run: true - name: Verify outputs + env: + FLUTTER_VERSION: ${{ steps.flutter.outputs.flutter-version }} + CHANNEL: ${{ steps.flutter.outputs.channel }} run: | - echo "Flutter version: ${{ steps.flutter.outputs.flutter-version }}" - echo "Channel: ${{ steps.flutter.outputs.channel }}" - test -n "${{ steps.flutter.outputs.flutter-version }}" + echo "Flutter version: $FLUTTER_VERSION" + echo "Channel: $CHANNEL" + test -n "$FLUTTER_VERSION" diff --git a/README.md b/README.md index 2940e57..2f88840 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,16 @@ The action sets the following outputs: | `cache-pub-hit` | Whether pub cache was restored | | `architecture` | Resolved CPU architecture | +## Security + +The intro bullets above are backed by a consistent design: every artifact this action installs is verified, and no input ever reaches a shell. + +* **Archive verification** — Release archives are downloaded over HTTPS and verified against the SHA-256 checksum in the official release manifest before extraction. A mismatch fails the run. +* **Git mode verification** — Versions and refs are resolved to a commit hash first (via the release manifest or `git ls-remote`), and after cloning the checkout is verified to match that commit. SDK caches are keyed by the commit hash and the repository URL. +* **No shell interpolation** — Inputs and version-file contents are passed to subprocesses as argument arrays. Nothing is ever interpolated into a shell command. +* **Supply chain** — All runtime dependencies are bundled into `dist/`, and CI verifies the bundle is reproducible from source on every change. The repository's own workflows are SHA-pinned, run with minimal permissions, and use `npm ci --ignore-scripts` wherever a write token is present. +* **Cache integrity** — Caches are restored by exact key match only. There is no partial-match fallback that could restore unexpected content. + ## Blog Post [Why I Built a New GitHub Action for Flutter](https://koji-1009.medium.com/why-i-built-a-new-github-action-for-flutter-592c24e96a55) — design decisions and motivation behind this action. diff --git a/__tests__/git-source.test.ts b/__tests__/git-source.test.ts index 21ab4c2..66b3517 100644 --- a/__tests__/git-source.test.ts +++ b/__tests__/git-source.test.ts @@ -538,9 +538,22 @@ describe("resolveGit (dispatch)", () => { }); describe("installFromGit", () => { + const FULL_HASH = "abc123def4567890abc123def4567890abc123de"; + + // Echo FULL_HASH for `git rev-parse HEAD` so the post-clone verification + // passes by default; tests override this to simulate a mismatch. + function mockExecWithHead(head: string) { + vi.mocked(exec).mockImplementation(async (_cmd, args, options) => { + if (args?.includes("rev-parse")) { + options?.listeners?.stdout?.(Buffer.from(`${head}\n`)); + } + return 0; + }); + } + beforeEach(() => { vi.clearAllMocks(); - vi.mocked(exec).mockResolvedValue(0); + mockExecWithHead(FULL_HASH); }); const gitEnvMatcher = expect.objectContaining({ @@ -614,6 +627,46 @@ describe("installFromGit", () => { ); }); + it("verifies cloned HEAD against the resolved commit for branch ref", async () => { + await installFromGit( + "https://github.com/flutter/flutter.git", + "stable", + "/opt/flutter", + FULL_HASH, + ); + expect(exec).toHaveBeenCalledWith( + "git", + ["-C", "/opt/flutter", "rev-parse", "HEAD"], + expect.anything(), + ); + }); + + it("throws when cloned HEAD does not match the resolved commit", async () => { + mockExecWithHead("1111111111111111111111111111111111111111"); + + await expect( + installFromGit( + "https://github.com/flutter/flutter.git", + "stable", + "/opt/flutter", + FULL_HASH, + ), + ).rejects.toThrow("does not match resolved commit"); + }); + + it("skips HEAD verification for short hash commitHash", async () => { + await installFromGit( + "https://github.com/flutter/flutter.git", + "stable", + "/opt/flutter", + "abc1234", + ); + const revParseCall = vi + .mocked(exec) + .mock.calls.find((c) => (c[1] as string[])?.includes("rev-parse")); + expect(revParseCall).toBeUndefined(); + }); + it("throws when command times out", async () => { vi.useFakeTimers(); vi.mocked(exec).mockReturnValue(new Promise(() => {})); diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 3dcad29..7f03a6c 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -241,6 +241,19 @@ describe("main run()", () => { ); }); + it("calls setFailed for an invalid git-source value", async () => { + const { inputs } = setupDefaultMocks(); + inputs["git-source"] = "releases"; + + await run(); + + expect(setFailed).toHaveBeenCalledWith( + expect.stringContaining("Invalid git-source"), + ); + expect(fetchManifest).not.toHaveBeenCalled(); + expect(installFromArchive).not.toHaveBeenCalled(); + }); + it("uses git mode with ref spec", async () => { const { inputs } = setupDefaultMocks(); inputs["git-source"] = "git"; diff --git a/__tests__/version.test.ts b/__tests__/version.test.ts index 0154d92..0582fb2 100644 --- a/__tests__/version.test.ts +++ b/__tests__/version.test.ts @@ -19,6 +19,10 @@ import { } from "../src/version"; vi.mock("@actions/http-client"); +vi.mock("@actions/core"); +vi.mock("node:timers/promises", () => ({ + setTimeout: vi.fn().mockResolvedValue(undefined), +})); const linuxFixture: FlutterManifest = JSON.parse( readFileSync(join(__dirname, "fixtures", "releases_linux.json"), "utf8"), @@ -133,6 +137,18 @@ describe("parseVersionSpec", () => { }); }); + it("throws for ambiguous major.minor form 3.4", () => { + expect(() => parseVersionSpec("3.4")).toThrow( + "Ambiguous version '3.4': use '3.4.x'", + ); + }); + + it("throws for ambiguous bare major form 3", () => { + expect(() => parseVersionSpec("3")).toThrow( + "Ambiguous version '3': use '3.x'", + ); + }); + it("returns ref for branch name", () => { expect(parseVersionSpec("my-feature-branch")).toEqual({ type: "ref", @@ -601,14 +617,63 @@ describe("fetchManifest", () => { }); it("throws when result is null", async () => { + const getJson = vi.fn().mockResolvedValue({ result: null }); HttpClient.mockImplementation( class { - getJson = vi.fn().mockResolvedValue({ result: null }); + getJson = getJson; }, ); await expect(fetchManifest("linux")).rejects.toThrow( "Failed to fetch manifest", ); + // A 404 (null result) must not be retried. + expect(getJson).toHaveBeenCalledTimes(1); + }); + + it("retries on HTTP 500 and succeeds on next attempt", async () => { + const getJson = vi + .fn() + .mockRejectedValueOnce( + Object.assign(new Error("Failed request: (500)"), { statusCode: 500 }), + ) + .mockResolvedValue({ result: JSON.parse(JSON.stringify(linuxFixture)) }); + HttpClient.mockImplementation( + class { + getJson = getJson; + }, + ); + + const result = await fetchManifest("linux"); + expect(result.releases.length).toBeGreaterThan(0); + expect(getJson).toHaveBeenCalledTimes(2); + }); + + it("retries on network error and throws after exhausting attempts", async () => { + const getJson = vi.fn().mockRejectedValue(new Error("ECONNRESET")); + HttpClient.mockImplementation( + class { + getJson = getJson; + }, + ); + + await expect(fetchManifest("linux")).rejects.toThrow("ECONNRESET"); + expect(getJson).toHaveBeenCalledTimes(3); + }); + + it("does not retry on HTTP 403", async () => { + const getJson = vi + .fn() + .mockRejectedValue( + Object.assign(new Error("Failed request: (403)"), { statusCode: 403 }), + ); + HttpClient.mockImplementation( + class { + getJson = getJson; + }, + ); + + await expect(fetchManifest("linux")).rejects.toThrow("(403)"); + expect(getJson).toHaveBeenCalledTimes(1); }); }); diff --git a/dist/post/index.js b/dist/post/index.js index e3404df..9dbf272 100644 --- a/dist/post/index.js +++ b/dist/post/index.js @@ -658,6 +658,21 @@ var require_errors = __commonJS({ } [kSecureProxyConnectionError] = true; }; + var kMessageSizeExceededError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED"); + var MessageSizeExceededError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "MessageSizeExceededError"; + this.message = message || "Max decompressed message size exceeded"; + this.code = "UND_ERR_WS_MESSAGE_SIZE_EXCEEDED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kMessageSizeExceededError] === true; + } + get [kMessageSizeExceededError]() { + return true; + } + }; module2.exports = { AbortError: AbortError3, HTTPParserError, @@ -681,7 +696,8 @@ var require_errors = __commonJS({ ResponseExceededMaxSizeError, RequestRetryError, ResponseError, - SecureProxyConnectionError + SecureProxyConnectionError, + MessageSizeExceededError }; } }); @@ -1691,6 +1707,9 @@ var require_request = __commonJS({ if (upgrade && typeof upgrade !== "string") { throw new InvalidArgumentError("upgrade must be a string"); } + if (upgrade && !isValidHeaderValue(upgrade)) { + throw new InvalidArgumentError("invalid upgrade header"); + } if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { throw new InvalidArgumentError("invalid headersTimeout"); } @@ -1923,12 +1942,18 @@ var require_request = __commonJS({ } else { val = `${val}`; } - if (request.host === null && headerName === "host") { + if (headerName === "host") { + if (request.host !== null) { + throw new InvalidArgumentError("duplicate host header"); + } if (typeof val !== "string") { throw new InvalidArgumentError("invalid host header"); } request.host = val; - } else if (request.contentLength === null && headerName === "content-length") { + } else if (headerName === "content-length") { + if (request.contentLength !== null) { + throw new InvalidArgumentError("duplicate content-length header"); + } request.contentLength = parseInt(val, 10); if (!Number.isFinite(request.contentLength)) { throw new InvalidArgumentError("invalid content-length header"); @@ -2025,13 +2050,21 @@ var require_dispatcher_base = __commonJS({ var kOnDestroyed = /* @__PURE__ */ Symbol("onDestroyed"); var kOnClosed = /* @__PURE__ */ Symbol("onClosed"); var kInterceptedDispatch = /* @__PURE__ */ Symbol("Intercepted Dispatch"); + var kWebSocketOptions = /* @__PURE__ */ Symbol("webSocketOptions"); var DispatcherBase = class extends Dispatcher { - constructor() { + constructor(opts) { super(); this[kDestroyed] = false; this[kOnDestroyed] = null; this[kClosed] = false; this[kOnClosed] = []; + this[kWebSocketOptions] = opts?.webSocket ?? {}; + } + get webSocketOptions() { + return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 + }; } get destroyed() { return this[kDestroyed]; @@ -5684,6 +5717,9 @@ var require_client_h1 = __commonJS({ var FastBuffer = Buffer[Symbol.species]; var addListener = util4.addListener; var removeAllListeners = util4.removeAllListeners; + var kIdleSocketValidation = /* @__PURE__ */ Symbol("kIdleSocketValidation"); + var kIdleSocketValidationTimeout = /* @__PURE__ */ Symbol("kIdleSocketValidationTimeout"); + var kSocketUsed = /* @__PURE__ */ Symbol("kSocketUsed"); var extractBody; async function lazyllhttp() { const llhttpWasmData = process.env.JEST_WORKER_ID ? require_llhttp_wasm() : void 0; @@ -5846,24 +5882,55 @@ var require_client_h1 = __commonJS({ currentBufferRef = null; } const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr; - if (ret === constants3.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)); - } else if (ret === constants3.ERROR.PAUSED) { - this.paused = true; - socket.unshift(data.slice(offset)); - } else if (ret !== constants3.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr); - let message = ""; - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); - message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")"; + if (ret !== constants3.ERROR.OK) { + const body2 = data.subarray(offset); + if (ret === constants3.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body2); + } else if (ret === constants3.ERROR.PAUSED) { + this.paused = true; + socket.unshift(body2); + } else { + throw this.createError(ret, body2); } - throw new HTTPParserError(message, constants3.ERROR[ret], data.slice(offset)); } } catch (err) { util4.destroy(socket, err); } } + finish() { + assert4(currentParser === null); + assert4(this.ptr != null); + assert4(!this.paused); + const { llhttp } = this; + let ret; + try { + currentParser = this; + ret = llhttp.llhttp_finish(this.ptr); + } finally { + currentParser = null; + } + if (ret === constants3.ERROR.OK) { + return null; + } + if (ret === constants3.ERROR.PAUSED || ret === constants3.ERROR.PAUSED_UPGRADE) { + this.paused = true; + return null; + } + return this.createError(ret, EMPTY_BUF); + } + createError(ret, data) { + const { llhttp, contentLength: contentLength2, bytesRead } = this; + if (contentLength2 && bytesRead !== parseInt(contentLength2, 10)) { + return new ResponseContentLengthMismatchError(); + } + const ptr = llhttp.llhttp_get_error_reason(this.ptr); + let message = ""; + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); + message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")"; + } + return new HTTPParserError(message, constants3.ERROR[ret], data); + } destroy() { assert4(this.ptr != null); assert4(currentParser == null); @@ -5883,6 +5950,10 @@ var require_client_h1 = __commonJS({ if (socket.destroyed) { return -1; } + if (client[kRunning] === 0) { + util4.destroy(socket, new SocketError("bad response", util4.getSocketInfo(socket))); + return -1; + } const request = client[kQueue][client[kRunningIdx]]; if (!request) { return -1; @@ -5962,6 +6033,10 @@ var require_client_h1 = __commonJS({ if (socket.destroyed) { return -1; } + if (client[kRunning] === 0) { + util4.destroy(socket, new SocketError("bad response", util4.getSocketInfo(socket))); + return -1; + } const request = client[kQueue][client[kRunningIdx]]; if (!request) { return -1; @@ -6087,6 +6162,7 @@ var require_client_h1 = __commonJS({ } request.onComplete(headers); client[kQueue][client[kRunningIdx]++] = null; + socket[kSocketUsed] = true; if (socket[kWriting]) { assert4(client[kRunning] === 0); util4.destroy(socket, new InformationalError("reset")); @@ -6130,12 +6206,19 @@ var require_client_h1 = __commonJS({ socket[kWriting] = false; socket[kReset] = false; socket[kBlocking] = false; + socket[kIdleSocketValidation] = 0; + socket[kIdleSocketValidationTimeout] = null; + socket[kSocketUsed] = false; socket[kParser] = new Parser(client, socket, llhttpInstance); addListener(socket, "error", function(err) { assert4(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); const parser = this[kParser]; if (err.code === "ECONNRESET" && parser.statusCode && !parser.shouldKeepAlive) { - parser.onMessageComplete(); + const parserErr = parser.finish(); + if (parserErr) { + this[kError] = parserErr; + this[kClient][kOnError](parserErr); + } return; } this[kError] = err; @@ -6150,7 +6233,10 @@ var require_client_h1 = __commonJS({ addListener(socket, "end", function() { const parser = this[kParser]; if (parser.statusCode && !parser.shouldKeepAlive) { - parser.onMessageComplete(); + const parserErr = parser.finish(); + if (parserErr) { + util4.destroy(this, parserErr); + } return; } util4.destroy(this, new SocketError("other side closed", util4.getSocketInfo(this))); @@ -6158,9 +6244,10 @@ var require_client_h1 = __commonJS({ addListener(socket, "close", function() { const client2 = this[kClient]; const parser = this[kParser]; + clearIdleSocketValidation(this); if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - parser.onMessageComplete(); + this[kError] = parser.finish() || this[kError]; } this[kParser].destroy(); this[kParser] = null; @@ -6209,7 +6296,7 @@ var require_client_h1 = __commonJS({ return socket.destroyed; }, busy(request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true; } if (request) { @@ -6227,6 +6314,24 @@ var require_client_h1 = __commonJS({ } }; } + 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?.(); + } function resumeH1(client) { const socket = client[kSocket]; if (socket && !socket.destroyed) { @@ -6239,6 +6344,29 @@ var require_client_h1 = __commonJS({ socket.ref(); 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 (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE); @@ -6291,6 +6419,7 @@ var require_client_h1 = __commonJS({ process.emitWarning(new RequestContentLengthMismatchError()); } const socket = client[kSocket]; + clearIdleSocketValidation(socket); const abort = (err) => { if (request.aborted || request.completed) { return; @@ -7470,9 +7599,10 @@ var require_client = __commonJS({ autoSelectFamilyAttemptTimeout, // h2 maxConcurrentStreams, - allowH2 + allowH2, + webSocket } = {}) { - super(); + super({ webSocket }); if (keepAlive !== void 0) { throw new InvalidArgumentError("unsupported keepAlive, use pipelining=0 instead"); } @@ -7978,8 +8108,8 @@ var require_pool_base = __commonJS({ var kRemoveClient = /* @__PURE__ */ Symbol("remove client"); var kStats = /* @__PURE__ */ Symbol("stats"); var PoolBase = class extends DispatcherBase { - constructor() { - super(); + constructor(opts) { + super(opts); this[kQueue] = new FixedQueue(); this[kClients] = []; this[kQueued] = 0; @@ -8150,7 +8280,6 @@ var require_pool = __commonJS({ allowH2, ...options } = {}) { - super(); if (connections != null && (!Number.isFinite(connections) || connections < 0)) { throw new InvalidArgumentError("invalid connections"); } @@ -8171,6 +8300,7 @@ var require_pool = __commonJS({ ...connect }); } + super(options); this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool) ? options.interceptors.Pool : []; this[kConnections] = connections || null; this[kUrl] = util4.parseOrigin(origin); @@ -8370,7 +8500,6 @@ var require_agent = __commonJS({ } var Agent3 = class extends DispatcherBase { constructor({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - super(); if (typeof factory !== "function") { throw new InvalidArgumentError("factory must be a function."); } @@ -8380,6 +8509,7 @@ var require_agent = __commonJS({ if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { throw new InvalidArgumentError("maxRedirections must be a positive number"); } + super(options); if (connect && typeof connect !== "function") { connect = { ...connect }; } @@ -16074,18 +16204,14 @@ var require_parse = __commonJS({ } else if (attributeNameLowercase === "httponly") { cookieAttributeList.httpOnly = true; } else if (attributeNameLowercase === "samesite") { - let enforcement = "Default"; const attributeValueLowercase = attributeValue.toLowerCase(); - if (attributeValueLowercase.includes("none")) { - enforcement = "None"; + if (attributeValueLowercase === "none") { + cookieAttributeList.sameSite = "None"; + } else if (attributeValueLowercase === "strict") { + cookieAttributeList.sameSite = "Strict"; + } else if (attributeValueLowercase === "lax") { + cookieAttributeList.sameSite = "Lax"; } - if (attributeValueLowercase.includes("strict")) { - enforcement = "Strict"; - } - if (attributeValueLowercase.includes("lax")) { - enforcement = "Lax"; - } - cookieAttributeList.sameSite = enforcement; } else { cookieAttributeList.unparsed ??= []; cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`); @@ -16700,13 +16826,17 @@ var require_util7 = __commonJS({ return extensionList; } function isValidClientWindowBits(value) { + if (value.length === 0) { + return false; + } for (let i = 0; i < value.length; i++) { const byte = value.charCodeAt(i); if (byte < 48 || byte > 57) { return false; } } - return true; + const num = Number.parseInt(value, 10); + return num >= 8 && num <= 15; } var hasIntl = typeof process.versions.icu === "string"; var fatalDecoder = hasIntl ? new TextDecoder("utf-8", { fatal: true }) : void 0; @@ -17005,6 +17135,7 @@ var require_permessage_deflate = __commonJS({ "use strict"; var { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require("node:zlib"); var { isValidClientWindowBits } = require_util7(); + var { MessageSizeExceededError } = require_errors(); var tail = Buffer.from([0, 0, 255, 255]); var kBuffer = /* @__PURE__ */ Symbol("kBuffer"); var kLength = /* @__PURE__ */ Symbol("kLength"); @@ -17012,10 +17143,21 @@ var require_permessage_deflate = __commonJS({ /** @type {import('node:zlib').InflateRaw} */ #inflate; #options = {}; - constructor(extensions) { + #maxPayloadSize = 0; + /** + * @param {Map} extensions + */ + constructor(extensions, options) { this.#options.serverNoContextTakeover = extensions.has("server_no_context_takeover"); this.#options.serverMaxWindowBits = extensions.get("server_max_window_bits"); + 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) { if (!this.#inflate) { let windowBits = Z_DEFAULT_WINDOWBITS; @@ -17026,12 +17168,23 @@ var require_permessage_deflate = __commonJS({ } windowBits = Number.parseInt(this.#options.serverMaxWindowBits); } - this.#inflate = createInflateRaw({ windowBits }); + try { + this.#inflate = createInflateRaw({ windowBits }); + } catch (err) { + callback(err); + return; + } this.#inflate[kBuffer] = []; this.#inflate[kLength] = 0; this.#inflate.on("data", (data) => { - this.#inflate[kBuffer].push(data); this.#inflate[kLength] += data.length; + if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { + callback(new MessageSizeExceededError()); + this.#inflate.removeAllListeners(); + this.#inflate = null; + return; + } + this.#inflate[kBuffer].push(data); }); this.#inflate.on("error", (err) => { this.#inflate = null; @@ -17043,6 +17196,9 @@ var require_permessage_deflate = __commonJS({ this.#inflate.write(tail); } this.#inflate.flush(() => { + if (!this.#inflate) { + return; + } const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength]); this.#inflate[kBuffer].length = 0; this.#inflate[kLength] = 0; @@ -17076,8 +17232,14 @@ var require_receiver = __commonJS({ var { WebsocketFrameSend } = require_frame(); var { closeWebSocketConnection } = require_connection(); var { PerMessageDeflate } = require_permessage_deflate(); + var { MessageSizeExceededError } = require_errors(); + function failWebsocketConnectionWithCode(ws, code, reason) { + closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason)); + failWebsocketConnection(ws, reason); + } var ByteParser = class extends Writable { #buffers = []; + #fragmentsBytes = 0; #byteOffset = 0; #loop = false; #state = parserStates.INFO; @@ -17085,12 +17247,23 @@ var require_receiver = __commonJS({ #fragments = []; /** @type {Map} */ #extensions; - constructor(ws, extensions) { + /** @type {number} */ + #maxFragments; + /** @type {number} */ + #maxPayloadSize; + /** + * @param {import('./websocket').WebSocket} ws + * @param {Map|null} extensions + * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] + */ + constructor(ws, extensions, options = {}) { super(); this.ws = ws; this.#extensions = extensions == null ? /* @__PURE__ */ new Map() : extensions; + this.#maxFragments = options.maxFragments ?? 0; + this.#maxPayloadSize = options.maxPayloadSize ?? 0; if (this.#extensions.has("permessage-deflate")) { - this.#extensions.set("permessage-deflate", new PerMessageDeflate(extensions)); + this.#extensions.set("permessage-deflate", new PerMessageDeflate(extensions, options)); } } /** @@ -17103,6 +17276,13 @@ var require_receiver = __commonJS({ this.#loop = true; 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. * Callback is called whenever there are no more chunks buffering, @@ -17162,6 +17342,9 @@ var require_receiver = __commonJS({ if (payloadLength <= 125) { this.#info.payloadLength = payloadLength; this.#state = parserStates.READ_DATA; + if (!this.#validatePayloadLength()) { + return; + } } else if (payloadLength === 126) { this.#state = parserStates.PAYLOADLENGTH_16; } else if (payloadLength === 127) { @@ -17182,19 +17365,25 @@ var require_receiver = __commonJS({ const buffer2 = this.consume(2); this.#info.payloadLength = buffer2.readUInt16BE(0); this.#state = parserStates.READ_DATA; + if (!this.#validatePayloadLength()) { + return; + } } else if (this.#state === parserStates.PAYLOADLENGTH_64) { if (this.#byteOffset < 8) { return callback(); } const buffer2 = this.consume(8); const upper = buffer2.readUInt32BE(0); - if (upper > 2 ** 31 - 1) { + const lower = buffer2.readUInt32BE(4); + if (upper !== 0 || lower > 2 ** 31 - 1) { failWebsocketConnection(this.ws, "Received payload length > 2^31 bytes."); return; } - const lower = buffer2.readUInt32BE(4); - this.#info.payloadLength = (upper << 8) + lower; + this.#info.payloadLength = lower; this.#state = parserStates.READ_DATA; + if (!this.#validatePayloadLength()) { + return; + } } else if (this.#state === parserStates.READ_DATA) { if (this.#byteOffset < this.#info.payloadLength) { return callback(); @@ -17205,32 +17394,46 @@ var require_receiver = __commonJS({ this.#state = parserStates.INFO; } else { if (!this.#info.compressed) { - this.#fragments.push(body2); + if (!this.writeFragments(body2)) { + return; + } + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message); + return; + } if (!this.#info.fragmented && this.#info.fin) { - const fullMessage = Buffer.concat(this.#fragments); - websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage); - this.#fragments.length = 0; + websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments()); } this.#state = parserStates.INFO; } else { - this.#extensions.get("permessage-deflate").decompress(body2, this.#info.fin, (error2, data) => { - if (error2) { - closeWebSocketConnection(this.ws, 1007, error2.message, error2.message.length); - return; - } - this.#fragments.push(data); - if (!this.#info.fin) { - this.#state = parserStates.INFO; + this.#extensions.get("permessage-deflate").decompress( + body2, + this.#info.fin, + (error2, data) => { + if (error2) { + const code = error2 instanceof MessageSizeExceededError ? 1009 : 1007; + failWebsocketConnectionWithCode(this.ws, code, error2.message); + return; + } + 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) { + this.#state = parserStates.INFO; + this.#loop = true; + this.run(callback); + return; + } + websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments()); this.#loop = true; + this.#state = parserStates.INFO; this.run(callback); - return; } - websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments)); - this.#loop = true; - this.#state = parserStates.INFO; - this.#fragments.length = 0; - this.run(callback); - }); + ); this.#loop = false; break; } @@ -17273,6 +17476,26 @@ var require_receiver = __commonJS({ this.#byteOffset -= n; return buffer2; } + 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) { assert4(data.length !== 1); let code; @@ -17710,7 +17933,13 @@ var require_websocket = __commonJS({ */ #onConnectionEstablished(response, parsedExtensions) { this[kResponse] = response; - const parser = new ByteParser(this, parsedExtensions); + 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("error", onParserError.bind(this)); response.socket.ws = this; @@ -18637,13 +18866,15 @@ var require_brace_expansion = __commonJS({ parts.push.apply(parts, p); return parts; } - function expandTop(str) { + function expandTop(str, options) { if (!str) return []; + options = options || {}; + var max = options.max == null ? Infinity : options.max; if (str.substr(0, 2) === "{}") { str = "\\{\\}" + str.substr(2); } - return expand(escapeBraces(str), true).map(unescapeBraces); + return expand(escapeBraces(str), max, true).map(unescapeBraces); } function embrace(str) { return "{" + str + "}"; @@ -18657,7 +18888,7 @@ var require_brace_expansion = __commonJS({ function gte(i, y) { return i >= y; } - function expand(str, isTop) { + function expand(str, max, isTop) { var expansions = []; var m = balanced("{", "}", str); if (!m || /\$$/.test(m.pre)) return [str]; @@ -18668,7 +18899,7 @@ var require_brace_expansion = __commonJS({ if (!isSequence && !isOptions) { if (m.post.match(/,(?!,).*\}/)) { str = m.pre + "{" + m.body + escClose + m.post; - return expand(str); + return expand(str, max, true); } return [str]; } @@ -18678,9 +18909,9 @@ var require_brace_expansion = __commonJS({ } else { n = parseCommaParts(m.body); if (n.length === 1) { - n = expand(n[0], false).map(embrace); + n = expand(n[0], max, false).map(embrace); if (n.length === 1) { - var post = m.post.length ? expand(m.post, false) : [""]; + var post = m.post.length ? expand(m.post, max, false) : [""]; return post.map(function(p) { return m.pre + n[0] + p; }); @@ -18688,13 +18919,13 @@ var require_brace_expansion = __commonJS({ } } var pre = m.pre; - var post = m.post.length ? expand(m.post, false) : [""]; + var post = m.post.length ? expand(m.post, max, false) : [""]; var N; if (isSequence) { var x = numeric(n[0]); var y = numeric(n[1]); var width = Math.max(n[0].length, n[1].length); - var incr = n.length == 3 ? Math.abs(numeric(n[2])) : 1; + var incr = n.length == 3 ? Math.max(Math.abs(numeric(n[2])), 1) : 1; var test = lte; var reverse = y < x; if (reverse) { @@ -18703,7 +18934,7 @@ var require_brace_expansion = __commonJS({ } var pad = n.some(isPadded); N = []; - for (var i = x; test(i, y); i += incr) { + for (var i = x; test(i, y) && N.length < max; i += incr) { var c; if (isAlphaSequence) { c = String.fromCharCode(i); @@ -18726,11 +18957,11 @@ var require_brace_expansion = __commonJS({ } } else { N = concatMap(n, function(el) { - return expand(el, false); + return expand(el, max, false); }); } for (var j = 0; j < N.length; j++) { - for (var k = 0; k < post.length; k++) { + for (var k = 0; k < post.length && expansions.length < max; k++) { var expansion = pre + N[j] + post[k]; if (!isTop || isSequence || expansion) expansions.push(expansion); @@ -34731,6 +34962,19 @@ var isName = function(string) { function isExist(v) { return typeof v !== "undefined"; } +var DANGEROUS_PROPERTY_NAMES = [ + // '__proto__', + // 'constructor', + // 'prototype', + "hasOwnProperty", + "toString", + "valueOf", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__" +]; +var criticalProperties = ["__proto__", "constructor", "prototype"]; // node_modules/fast-xml-parser/src/validator.js var defaultOptions = { @@ -35038,7 +35282,1539 @@ function getPositionFromMatch(match2) { return match2.startIndex + match2[1].length; } +// node_modules/@nodable/entities/src/entities.js +var BASIC_LATIN = { + amp: "&", + AMP: "&", + lt: "<", + LT: "<", + gt: ">", + GT: ">", + quot: '"', + QUOT: '"', + apos: "'", + lsquo: "\u2018", + rsquo: "\u2019", + ldquo: "\u201C", + rdquo: "\u201D", + lsquor: "\u201A", + rsquor: "\u2019", + ldquor: "\u201E", + bdquo: "\u201E", + comma: ",", + period: ".", + colon: ":", + semi: ";", + excl: "!", + quest: "?", + num: "#", + dollar: "$", + percent: "%", + ast: "*", + commat: "@", + lowbar: "_", + verbar: "|", + vert: "|", + sol: "/", + bsol: "\\", + lbrace: "{", + rbrace: "}", + lbrack: "[", + rbrack: "]", + lpar: "(", + rpar: ")", + nbsp: "\xA0", + iexcl: "\xA1", + cent: "\xA2", + pound: "\xA3", + curren: "\xA4", + yen: "\xA5", + brvbar: "\xA6", + sect: "\xA7", + uml: "\xA8", + copy: "\xA9", + COPY: "\xA9", + ordf: "\xAA", + laquo: "\xAB", + not: "\xAC", + shy: "\xAD", + reg: "\xAE", + REG: "\xAE", + macr: "\xAF", + deg: "\xB0", + plusmn: "\xB1", + sup2: "\xB2", + sup3: "\xB3", + acute: "\xB4", + micro: "\xB5", + para: "\xB6", + middot: "\xB7", + cedil: "\xB8", + sup1: "\xB9", + ordm: "\xBA", + raquo: "\xBB", + frac14: "\xBC", + frac12: "\xBD", + half: "\xBD", + frac34: "\xBE", + iquest: "\xBF", + times: "\xD7", + div: "\xF7", + divide: "\xF7" +}; +var LATIN_ACCENTS = { + Agrave: "\xC0", + agrave: "\xE0", + Aacute: "\xC1", + aacute: "\xE1", + Acirc: "\xC2", + acirc: "\xE2", + Atilde: "\xC3", + atilde: "\xE3", + Auml: "\xC4", + auml: "\xE4", + Aring: "\xC5", + aring: "\xE5", + AElig: "\xC6", + aelig: "\xE6", + Ccedil: "\xC7", + ccedil: "\xE7", + Egrave: "\xC8", + egrave: "\xE8", + Eacute: "\xC9", + eacute: "\xE9", + Ecirc: "\xCA", + ecirc: "\xEA", + Euml: "\xCB", + euml: "\xEB", + Igrave: "\xCC", + igrave: "\xEC", + Iacute: "\xCD", + iacute: "\xED", + Icirc: "\xCE", + icirc: "\xEE", + Iuml: "\xCF", + iuml: "\xEF", + ETH: "\xD0", + eth: "\xF0", + Ntilde: "\xD1", + ntilde: "\xF1", + Ograve: "\xD2", + ograve: "\xF2", + Oacute: "\xD3", + oacute: "\xF3", + Ocirc: "\xD4", + ocirc: "\xF4", + Otilde: "\xD5", + otilde: "\xF5", + Ouml: "\xD6", + ouml: "\xF6", + Oslash: "\xD8", + oslash: "\xF8", + Ugrave: "\xD9", + ugrave: "\xF9", + Uacute: "\xDA", + uacute: "\xFA", + Ucirc: "\xDB", + ucirc: "\xFB", + Uuml: "\xDC", + uuml: "\xFC", + Yacute: "\xDD", + yacute: "\xFD", + THORN: "\xDE", + thorn: "\xFE", + szlig: "\xDF", + yuml: "\xFF", + Yuml: "\u0178" +}; +var LATIN_EXTENDED = { + Amacr: "\u0100", + amacr: "\u0101", + Abreve: "\u0102", + abreve: "\u0103", + Aogon: "\u0104", + aogon: "\u0105", + Cacute: "\u0106", + cacute: "\u0107", + Ccirc: "\u0108", + ccirc: "\u0109", + Cdot: "\u010A", + cdot: "\u010B", + Ccaron: "\u010C", + ccaron: "\u010D", + Dcaron: "\u010E", + dcaron: "\u010F", + Dstrok: "\u0110", + dstrok: "\u0111", + Emacr: "\u0112", + emacr: "\u0113", + Ecaron: "\u011A", + ecaron: "\u011B", + Edot: "\u0116", + edot: "\u0117", + Eogon: "\u0118", + eogon: "\u0119", + Gcirc: "\u011C", + gcirc: "\u011D", + Gbreve: "\u011E", + gbreve: "\u011F", + Gdot: "\u0120", + gdot: "\u0121", + Gcedil: "\u0122", + Hcirc: "\u0124", + hcirc: "\u0125", + Hstrok: "\u0126", + hstrok: "\u0127", + Itilde: "\u0128", + itilde: "\u0129", + Imacr: "\u012A", + imacr: "\u012B", + Iogon: "\u012E", + iogon: "\u012F", + Idot: "\u0130", + IJlig: "\u0132", + ijlig: "\u0133", + Jcirc: "\u0134", + jcirc: "\u0135", + Kcedil: "\u0136", + kcedil: "\u0137", + kgreen: "\u0138", + Lacute: "\u0139", + lacute: "\u013A", + Lcedil: "\u013B", + lcedil: "\u013C", + Lcaron: "\u013D", + lcaron: "\u013E", + Lmidot: "\u013F", + lmidot: "\u0140", + Lstrok: "\u0141", + lstrok: "\u0142", + Nacute: "\u0143", + nacute: "\u0144", + Ncaron: "\u0147", + ncaron: "\u0148", + Ncedil: "\u0145", + ncedil: "\u0146", + ENG: "\u014A", + eng: "\u014B", + Omacr: "\u014C", + omacr: "\u014D", + Odblac: "\u0150", + odblac: "\u0151", + OElig: "\u0152", + oelig: "\u0153", + Racute: "\u0154", + racute: "\u0155", + Rcaron: "\u0158", + rcaron: "\u0159", + Rcedil: "\u0156", + rcedil: "\u0157", + Sacute: "\u015A", + sacute: "\u015B", + Scirc: "\u015C", + scirc: "\u015D", + Scedil: "\u015E", + scedil: "\u015F", + Scaron: "\u0160", + scaron: "\u0161", + Tcedil: "\u0162", + tcedil: "\u0163", + Tcaron: "\u0164", + tcaron: "\u0165", + Tstrok: "\u0166", + tstrok: "\u0167", + Utilde: "\u0168", + utilde: "\u0169", + Umacr: "\u016A", + umacr: "\u016B", + Ubreve: "\u016C", + ubreve: "\u016D", + Uring: "\u016E", + uring: "\u016F", + Udblac: "\u0170", + udblac: "\u0171", + Uogon: "\u0172", + uogon: "\u0173", + Wcirc: "\u0174", + wcirc: "\u0175", + Ycirc: "\u0176", + ycirc: "\u0177", + Zacute: "\u0179", + zacute: "\u017A", + Zdot: "\u017B", + zdot: "\u017C", + Zcaron: "\u017D", + zcaron: "\u017E" +}; +var GREEK = { + Alpha: "\u0391", + alpha: "\u03B1", + Beta: "\u0392", + beta: "\u03B2", + Gamma: "\u0393", + gamma: "\u03B3", + Delta: "\u0394", + delta: "\u03B4", + Epsilon: "\u0395", + epsilon: "\u03B5", + epsiv: "\u03F5", + varepsilon: "\u03F5", + Zeta: "\u0396", + zeta: "\u03B6", + Eta: "\u0397", + eta: "\u03B7", + Theta: "\u0398", + theta: "\u03B8", + thetasym: "\u03D1", + vartheta: "\u03D1", + Iota: "\u0399", + iota: "\u03B9", + Kappa: "\u039A", + kappa: "\u03BA", + kappav: "\u03F0", + varkappa: "\u03F0", + Lambda: "\u039B", + lambda: "\u03BB", + Mu: "\u039C", + mu: "\u03BC", + Nu: "\u039D", + nu: "\u03BD", + Xi: "\u039E", + xi: "\u03BE", + Omicron: "\u039F", + omicron: "\u03BF", + Pi: "\u03A0", + pi: "\u03C0", + piv: "\u03D6", + varpi: "\u03D6", + Rho: "\u03A1", + rho: "\u03C1", + rhov: "\u03F1", + varrho: "\u03F1", + Sigma: "\u03A3", + sigma: "\u03C3", + sigmaf: "\u03C2", + sigmav: "\u03C2", + varsigma: "\u03C2", + Tau: "\u03A4", + tau: "\u03C4", + Upsilon: "\u03A5", + upsilon: "\u03C5", + upsi: "\u03C5", + Upsi: "\u03D2", + upsih: "\u03D2", + Phi: "\u03A6", + phi: "\u03C6", + phiv: "\u03D5", + varphi: "\u03D5", + Chi: "\u03A7", + chi: "\u03C7", + Psi: "\u03A8", + psi: "\u03C8", + Omega: "\u03A9", + omega: "\u03C9", + ohm: "\u03A9", + Gammad: "\u03DC", + gammad: "\u03DD", + digamma: "\u03DD" +}; +var CYRILLIC = { + Afr: "\u{1D504}", + afr: "\u{1D51E}", + Acy: "\u0410", + acy: "\u0430", + Bcy: "\u0411", + bcy: "\u0431", + Vcy: "\u0412", + vcy: "\u0432", + Gcy: "\u0413", + gcy: "\u0433", + Dcy: "\u0414", + dcy: "\u0434", + IEcy: "\u0415", + iecy: "\u0435", + IOcy: "\u0401", + iocy: "\u0451", + ZHcy: "\u0416", + zhcy: "\u0436", + Zcy: "\u0417", + zcy: "\u0437", + Icy: "\u0418", + icy: "\u0438", + Jcy: "\u0419", + jcy: "\u0439", + Kcy: "\u041A", + kcy: "\u043A", + Lcy: "\u041B", + lcy: "\u043B", + Mcy: "\u041C", + mcy: "\u043C", + Ncy: "\u041D", + ncy: "\u043D", + Ocy: "\u041E", + ocy: "\u043E", + Pcy: "\u041F", + pcy: "\u043F", + Rcy: "\u0420", + rcy: "\u0440", + Scy: "\u0421", + scy: "\u0441", + Tcy: "\u0422", + tcy: "\u0442", + Ucy: "\u0423", + ucy: "\u0443", + Fcy: "\u0424", + fcy: "\u0444", + KHcy: "\u0425", + khcy: "\u0445", + TScy: "\u0426", + tscy: "\u0446", + CHcy: "\u0427", + chcy: "\u0447", + SHcy: "\u0428", + shcy: "\u0448", + SHCHcy: "\u0429", + shchcy: "\u0449", + HARDcy: "\u042A", + hardcy: "\u044A", + Ycy: "\u042B", + ycy: "\u044B", + SOFTcy: "\u042C", + softcy: "\u044C", + Ecy: "\u042D", + ecy: "\u044D", + YUcy: "\u042E", + yucy: "\u044E", + YAcy: "\u042F", + yacy: "\u044F", + DJcy: "\u0402", + djcy: "\u0452", + GJcy: "\u0403", + gjcy: "\u0453", + Jukcy: "\u0404", + jukcy: "\u0454", + DScy: "\u0405", + dscy: "\u0455", + Iukcy: "\u0406", + iukcy: "\u0456", + YIcy: "\u0407", + yicy: "\u0457", + Jsercy: "\u0408", + jsercy: "\u0458", + LJcy: "\u0409", + ljcy: "\u0459", + NJcy: "\u040A", + njcy: "\u045A", + TSHcy: "\u040B", + tshcy: "\u045B", + KJcy: "\u040C", + kjcy: "\u045C", + Ubrcy: "\u040E", + ubrcy: "\u045E", + DZcy: "\u040F", + dzcy: "\u045F" +}; +var MATH = { + plus: "+", + pm: "\xB1", + times: "\xD7", + div: "\xF7", + divide: "\xF7", + sdot: "\u22C5", + star: "\u2606", + starf: "\u2605", + bigstar: "\u2605", + lowast: "\u2217", + ast: "*", + midast: "*", + compfn: "\u2218", + smallcircle: "\u2218", + bullet: "\u2022", + bull: "\u2022", + nbsp: "\xA0", + hellip: "\u2026", + mldr: "\u2026", + prime: "\u2032", + Prime: "\u2033", + tprime: "\u2034", + bprime: "\u2035", + backprime: "\u2035", + minus: "\u2212", + minusd: "\u2238", + dotminus: "\u2238", + plusdo: "\u2214", + dotplus: "\u2214", + plusmn: "\xB1", + minusplus: "\u2213", + mnplus: "\u2213", + mp: "\u2213", + setminus: "\u2216", + smallsetminus: "\u2216", + Backslash: "\u2216", + setmn: "\u2216", + ssetmn: "\u2216", + lowbar: "_", + verbar: "|", + vert: "|", + VerticalLine: "|", + colon: ":", + Colon: "\u2237", + Proportion: "\u2237", + ratio: "\u2236", + equals: "=", + ne: "\u2260", + nequiv: "\u2262", + equiv: "\u2261", + Congruent: "\u2261", + sim: "\u223C", + thicksim: "\u223C", + thksim: "\u223C", + sime: "\u2243", + simeq: "\u2243", + TildeEqual: "\u2243", + asymp: "\u2248", + approx: "\u2248", + thickapprox: "\u2248", + thkap: "\u2248", + TildeTilde: "\u2248", + ncong: "\u2247", + cong: "\u2245", + TildeFullEqual: "\u2245", + asympeq: "\u224D", + CupCap: "\u224D", + bump: "\u224E", + Bumpeq: "\u224E", + HumpDownHump: "\u224E", + bumpe: "\u224F", + bumpeq: "\u224F", + HumpEqual: "\u224F", + le: "\u2264", + LessEqual: "\u2264", + ge: "\u2265", + GreaterEqual: "\u2265", + lesseqgtr: "\u22DA", + lesseqqgtr: "\u2A8B", + greater: ">", + less: "<" +}; +var MATH_ADVANCED = { + alefsym: "\u2135", + aleph: "\u2135", + beth: "\u2136", + gimel: "\u2137", + daleth: "\u2138", + forall: "\u2200", + ForAll: "\u2200", + part: "\u2202", + PartialD: "\u2202", + exist: "\u2203", + Exists: "\u2203", + nexist: "\u2204", + nexists: "\u2204", + empty: "\u2205", + emptyset: "\u2205", + emptyv: "\u2205", + varnothing: "\u2205", + nabla: "\u2207", + Del: "\u2207", + isin: "\u2208", + isinv: "\u2208", + in: "\u2208", + Element: "\u2208", + notin: "\u2209", + notinva: "\u2209", + ni: "\u220B", + niv: "\u220B", + SuchThat: "\u220B", + ReverseElement: "\u220B", + notni: "\u220C", + notniva: "\u220C", + prod: "\u220F", + Product: "\u220F", + coprod: "\u2210", + Coproduct: "\u2210", + sum: "\u2211", + Sum: "\u2211", + minus: "\u2212", + mp: "\u2213", + plusdo: "\u2214", + dotplus: "\u2214", + setminus: "\u2216", + lowast: "\u2217", + radic: "\u221A", + Sqrt: "\u221A", + prop: "\u221D", + propto: "\u221D", + Proportional: "\u221D", + varpropto: "\u221D", + infin: "\u221E", + infintie: "\u29DD", + ang: "\u2220", + angle: "\u2220", + angmsd: "\u2221", + measuredangle: "\u2221", + angsph: "\u2222", + mid: "\u2223", + VerticalBar: "\u2223", + nmid: "\u2224", + nsmid: "\u2224", + npar: "\u2226", + parallel: "\u2225", + spar: "\u2225", + nparallel: "\u2226", + nspar: "\u2226", + and: "\u2227", + wedge: "\u2227", + or: "\u2228", + vee: "\u2228", + cap: "\u2229", + cup: "\u222A", + int: "\u222B", + Integral: "\u222B", + conint: "\u222E", + ContourIntegral: "\u222E", + Conint: "\u222F", + DoubleContourIntegral: "\u222F", + Cconint: "\u2230", + there4: "\u2234", + therefore: "\u2234", + Therefore: "\u2234", + becaus: "\u2235", + because: "\u2235", + Because: "\u2235", + ratio: "\u2236", + Proportion: "\u2237", + minusd: "\u2238", + dotminus: "\u2238", + mDDot: "\u223A", + homtht: "\u223B", + sim: "\u223C", + bsimg: "\u223D", + backsim: "\u223D", + ac: "\u223E", + mstpos: "\u223E", + acd: "\u223F", + VerticalTilde: "\u2240", + wr: "\u2240", + wreath: "\u2240", + nsime: "\u2244", + nsimeq: "\u2244", + ncong: "\u2247", + simne: "\u2246", + ncongdot: "\u2A6D\u0338", + ngsim: "\u2275", + nsim: "\u2241", + napprox: "\u2249", + nap: "\u2249", + ngeq: "\u2271", + nge: "\u2271", + nleq: "\u2270", + nle: "\u2270", + ngtr: "\u226F", + ngt: "\u226F", + nless: "\u226E", + nlt: "\u226E", + nprec: "\u2280", + npr: "\u2280", + nsucc: "\u2281", + nsc: "\u2281" +}; +var ARROWS = { + larr: "\u2190", + leftarrow: "\u2190", + LeftArrow: "\u2190", + uarr: "\u2191", + uparrow: "\u2191", + UpArrow: "\u2191", + rarr: "\u2192", + rightarrow: "\u2192", + RightArrow: "\u2192", + darr: "\u2193", + downarrow: "\u2193", + DownArrow: "\u2193", + harr: "\u2194", + leftrightarrow: "\u2194", + LeftRightArrow: "\u2194", + varr: "\u2195", + updownarrow: "\u2195", + UpDownArrow: "\u2195", + nwarr: "\u2196", + nwarrow: "\u2196", + UpperLeftArrow: "\u2196", + nearr: "\u2197", + nearrow: "\u2197", + UpperRightArrow: "\u2197", + searr: "\u2198", + searrow: "\u2198", + LowerRightArrow: "\u2198", + swarr: "\u2199", + swarrow: "\u2199", + LowerLeftArrow: "\u2199", + lArr: "\u21D0", + Leftarrow: "\u21D0", + uArr: "\u21D1", + Uparrow: "\u21D1", + rArr: "\u21D2", + Rightarrow: "\u21D2", + dArr: "\u21D3", + Downarrow: "\u21D3", + hArr: "\u21D4", + Leftrightarrow: "\u21D4", + iff: "\u21D4", + vArr: "\u21D5", + Updownarrow: "\u21D5", + lAarr: "\u21DA", + Lleftarrow: "\u21DA", + rAarr: "\u21DB", + Rrightarrow: "\u21DB", + lrarr: "\u21C6", + leftrightarrows: "\u21C6", + rlarr: "\u21C4", + rightleftarrows: "\u21C4", + lrhar: "\u21CB", + leftrightharpoons: "\u21CB", + ReverseEquilibrium: "\u21CB", + rlhar: "\u21CC", + rightleftharpoons: "\u21CC", + Equilibrium: "\u21CC", + udarr: "\u21C5", + UpArrowDownArrow: "\u21C5", + duarr: "\u21F5", + DownArrowUpArrow: "\u21F5", + llarr: "\u21C7", + leftleftarrows: "\u21C7", + rrarr: "\u21C9", + rightrightarrows: "\u21C9", + ddarr: "\u21CA", + downdownarrows: "\u21CA", + har: "\u21BD", + lhard: "\u21BD", + leftharpoondown: "\u21BD", + lharu: "\u21BC", + leftharpoonup: "\u21BC", + rhard: "\u21C1", + rightharpoondown: "\u21C1", + rharu: "\u21C0", + rightharpoonup: "\u21C0", + lsh: "\u21B0", + Lsh: "\u21B0", + rsh: "\u21B1", + Rsh: "\u21B1", + ldsh: "\u21B2", + rdsh: "\u21B3", + hookleftarrow: "\u21A9", + hookrightarrow: "\u21AA", + mapstoleft: "\u21A4", + mapstoup: "\u21A5", + map: "\u21A6", + mapsto: "\u21A6", + mapstodown: "\u21A7", + crarr: "\u21B5", + nleftarrow: "\u219A", + nleftrightarrow: "\u21AE", + nrightarrow: "\u219B", + nrarr: "\u219B", + larrtl: "\u21A2", + rarrtl: "\u21A3", + leftarrowtail: "\u21A2", + rightarrowtail: "\u21A3", + twoheadleftarrow: "\u219E", + twoheadrightarrow: "\u21A0", + Larr: "\u219E", + Rarr: "\u21A0", + larrhk: "\u21A9", + rarrhk: "\u21AA", + larrlp: "\u21AB", + looparrowleft: "\u21AB", + rarrlp: "\u21AC", + looparrowright: "\u21AC", + harrw: "\u21AD", + leftrightsquigarrow: "\u21AD", + nrarrw: "\u219D\u0338", + rarrw: "\u219D", + rightsquigarrow: "\u219D", + larrbfs: "\u291F", + rarrbfs: "\u2920", + nvHarr: "\u2904", + nvlArr: "\u2902", + nvrArr: "\u2903", + larrfs: "\u291D", + rarrfs: "\u291E", + Map: "\u2905", + larrsim: "\u2973", + rarrsim: "\u2974", + harrcir: "\u2948", + Uarrocir: "\u2949", + lurdshar: "\u294A", + ldrdhar: "\u2967", + ldrushar: "\u294B", + rdldhar: "\u2969", + lrhard: "\u296D", + uharr: "\u21BE", + uharl: "\u21BF", + dharr: "\u21C2", + dharl: "\u21C3", + Uarr: "\u219F", + Darr: "\u21A1", + zigrarr: "\u21DD", + nwArr: "\u21D6", + neArr: "\u21D7", + seArr: "\u21D8", + swArr: "\u21D9", + nharr: "\u21AE", + nhArr: "\u21CE", + nlarr: "\u219A", + nlArr: "\u21CD", + nrArr: "\u21CF", + larrb: "\u21E4", + LeftArrowBar: "\u21E4", + rarrb: "\u21E5", + RightArrowBar: "\u21E5" +}; +var SHAPES = { + square: "\u25A1", + Square: "\u25A1", + squ: "\u25A1", + squf: "\u25AA", + squarf: "\u25AA", + blacksquar: "\u25AA", + blacksquare: "\u25AA", + FilledVerySmallSquare: "\u25AA", + blk34: "\u2593", + blk12: "\u2592", + blk14: "\u2591", + block: "\u2588", + srect: "\u25AD", + rect: "\u25AD", + sdot: "\u22C5", + sdotb: "\u22A1", + dotsquare: "\u22A1", + triangle: "\u25B5", + tri: "\u25B5", + trine: "\u25B5", + utri: "\u25B5", + triangledown: "\u25BF", + dtri: "\u25BF", + tridown: "\u25BF", + triangleleft: "\u25C3", + ltri: "\u25C3", + triangleright: "\u25B9", + rtri: "\u25B9", + blacktriangle: "\u25B4", + utrif: "\u25B4", + blacktriangledown: "\u25BE", + dtrif: "\u25BE", + blacktriangleleft: "\u25C2", + ltrif: "\u25C2", + blacktriangleright: "\u25B8", + rtrif: "\u25B8", + loz: "\u25CA", + lozenge: "\u25CA", + blacklozenge: "\u29EB", + lozf: "\u29EB", + bigcirc: "\u25EF", + xcirc: "\u25EF", + circ: "\u02C6", + Circle: "\u25CB", + cir: "\u25CB", + o: "\u25CB", + bullet: "\u2022", + bull: "\u2022", + hellip: "\u2026", + mldr: "\u2026", + nldr: "\u2025", + boxh: "\u2500", + HorizontalLine: "\u2500", + boxv: "\u2502", + boxdr: "\u250C", + boxdl: "\u2510", + boxur: "\u2514", + boxul: "\u2518", + boxvr: "\u251C", + boxvl: "\u2524", + boxhd: "\u252C", + boxhu: "\u2534", + boxvh: "\u253C", + boxH: "\u2550", + boxV: "\u2551", + boxdR: "\u2552", + boxDr: "\u2553", + boxDR: "\u2554", + boxDl: "\u2555", + boxdL: "\u2556", + boxDL: "\u2557", + boxuR: "\u2558", + boxUr: "\u2559", + boxUR: "\u255A", + boxUl: "\u255C", + boxuL: "\u255B", + boxUL: "\u255D", + boxvR: "\u255E", + boxVr: "\u255F", + boxVR: "\u2560", + boxVl: "\u2562", + boxvL: "\u2561", + boxVL: "\u2563", + boxHd: "\u2564", + boxhD: "\u2565", + boxHD: "\u2566", + boxHu: "\u2567", + boxhU: "\u2568", + boxHU: "\u2569", + boxvH: "\u256A", + boxVh: "\u256B", + boxVH: "\u256C" +}; +var PUNCTUATION = { + excl: "!", + iexcl: "\xA1", + brvbar: "\xA6", + sect: "\xA7", + uml: "\xA8", + copy: "\xA9", + ordf: "\xAA", + laquo: "\xAB", + not: "\xAC", + shy: "\xAD", + reg: "\xAE", + macr: "\xAF", + deg: "\xB0", + plusmn: "\xB1", + sup2: "\xB2", + sup3: "\xB3", + acute: "\xB4", + micro: "\xB5", + para: "\xB6", + middot: "\xB7", + cedil: "\xB8", + sup1: "\xB9", + ordm: "\xBA", + raquo: "\xBB", + frac14: "\xBC", + frac12: "\xBD", + frac34: "\xBE", + iquest: "\xBF", + nbsp: "\xA0", + comma: ",", + period: ".", + colon: ":", + semi: ";", + vert: "|", + Verbar: "\u2016", + verbar: "|", + dblac: "\u02DD", + circ: "\u02C6", + caron: "\u02C7", + breve: "\u02D8", + dot: "\u02D9", + ring: "\u02DA", + ogon: "\u02DB", + tilde: "\u02DC", + DiacriticalGrave: "`", + DiacriticalAcute: "\xB4", + DiacriticalTilde: "\u02DC", + DiacriticalDot: "\u02D9", + DiacriticalDoubleAcute: "\u02DD", + grave: "`" +}; +var CURRENCY = { + cent: "\xA2", + pound: "\xA3", + curren: "\xA4", + yen: "\xA5", + euro: "\u20AC", + dollar: "$", + fnof: "\u0192", + inr: "\u20B9", + af: "\u060B", + birr: "\u1265\u122D", + peso: "\u20B1", + rub: "\u20BD", + won: "\u20A9", + yuan: "\xA5", + cedil: "\xB8" +}; +var FRACTIONS = { + frac12: "\xBD", + half: "\xBD", + frac13: "\u2153", + frac14: "\xBC", + frac15: "\u2155", + frac16: "\u2159", + frac18: "\u215B", + frac23: "\u2154", + frac25: "\u2156", + frac34: "\xBE", + frac35: "\u2157", + frac38: "\u215C", + frac45: "\u2158", + frac56: "\u215A", + frac58: "\u215D", + frac78: "\u215E", + frasl: "\u2044" +}; +var MISC_SYMBOLS = { + trade: "\u2122", + TRADE: "\u2122", + telrec: "\u2315", + target: "\u2316", + ulcorn: "\u231C", + ulcorner: "\u231C", + urcorn: "\u231D", + urcorner: "\u231D", + dlcorn: "\u231E", + llcorner: "\u231E", + drcorn: "\u231F", + lrcorner: "\u231F", + intercal: "\u22BA", + intcal: "\u22BA", + oplus: "\u2295", + CirclePlus: "\u2295", + ominus: "\u2296", + CircleMinus: "\u2296", + otimes: "\u2297", + CircleTimes: "\u2297", + osol: "\u2298", + odot: "\u2299", + CircleDot: "\u2299", + oast: "\u229B", + circledast: "\u229B", + odash: "\u229D", + circleddash: "\u229D", + ocirc: "\u229A", + circledcirc: "\u229A", + boxplus: "\u229E", + plusb: "\u229E", + boxminus: "\u229F", + minusb: "\u229F", + boxtimes: "\u22A0", + timesb: "\u22A0", + boxdot: "\u22A1", + sdotb: "\u22A1", + veebar: "\u22BB", + vee: "\u2228", + barvee: "\u22BD", + and: "\u2227", + wedge: "\u2227", + Cap: "\u22D2", + Cup: "\u22D3", + Fork: "\u22D4", + pitchfork: "\u22D4", + epar: "\u22D5", + ltlarr: "\u2976", + nvap: "\u224D\u20D2", + nvsim: "\u223C\u20D2", + nvge: "\u2265\u20D2", + nvle: "\u2264\u20D2", + nvlt: "<\u20D2", + nvgt: ">\u20D2", + nvltrie: "\u22B4\u20D2", + nvrtrie: "\u22B5\u20D2", + Vdash: "\u22A9", + dashv: "\u22A3", + vDash: "\u22A8", + Vvdash: "\u22AA", + nvdash: "\u22AC", + nvDash: "\u22AD", + nVdash: "\u22AE", + nVDash: "\u22AF" +}; +var ALL_ENTITIES = { + ...BASIC_LATIN, + ...LATIN_ACCENTS, + ...LATIN_EXTENDED, + ...GREEK, + ...CYRILLIC, + ...MATH, + ...MATH_ADVANCED, + ...ARROWS, + ...SHAPES, + ...PUNCTUATION, + ...CURRENCY, + ...FRACTIONS, + ...MISC_SYMBOLS +}; +var XML = { + amp: "&", + apos: "'", + gt: ">", + lt: "<", + quot: '"' +}; +var COMMON_HTML = { + nbsp: "\xA0", + copy: "\xA9", + reg: "\xAE", + trade: "\u2122", + mdash: "\u2014", + ndash: "\u2013", + hellip: "\u2026", + laquo: "\xAB", + raquo: "\xBB", + lsquo: "\u2018", + rsquo: "\u2019", + ldquo: "\u201C", + rdquo: "\u201D", + bull: "\u2022", + para: "\xB6", + sect: "\xA7", + deg: "\xB0", + frac12: "\xBD", + frac14: "\xBC", + frac34: "\xBE" +}; + +// node_modules/@nodable/entities/src/EntityDecoder.js +var ENTITY_ACTION = Object.freeze({ + /** Resolve and expand the entity normally. */ + ALLOW: "allow", + /** Silently skip this entity — it will not be registered. */ + BLOCK: "block", + /** Throw an error, aborting entity registration entirely. */ + THROW: "throw" +}); +var SPECIAL_CHARS = new Set("!?\\\\/[]$%{}^&*()<>|+"); +function validateEntityName(name) { + if (name[0] === "#") { + throw new Error(`[EntityReplacer] Invalid character '#' in entity name: "${name}"`); + } + for (const ch of name) { + if (SPECIAL_CHARS.has(ch)) { + throw new Error(`[EntityReplacer] Invalid character '${ch}' in entity name: "${name}"`); + } + } + return name; +} +function mergeEntityMaps(...maps) { + const out = /* @__PURE__ */ Object.create(null); + for (const map of maps) { + if (!map) continue; + for (const key of Object.keys(map)) { + const raw = map[key]; + if (typeof raw === "string") { + out[key] = raw; + } else if (raw && typeof raw === "object" && raw.val !== void 0) { + const val = raw.val; + if (typeof val === "string") { + out[key] = val; + } + } + } + } + return out; +} +var LIMIT_TIER_EXTERNAL = "external"; +var LIMIT_TIER_BASE = "base"; +var LIMIT_TIER_ALL = "all"; +function parseLimitTiers(raw) { + if (!raw || raw === LIMIT_TIER_EXTERNAL) return /* @__PURE__ */ new Set([LIMIT_TIER_EXTERNAL]); + if (raw === LIMIT_TIER_ALL) return /* @__PURE__ */ new Set([LIMIT_TIER_ALL]); + if (raw === LIMIT_TIER_BASE) return /* @__PURE__ */ new Set([LIMIT_TIER_BASE]); + if (Array.isArray(raw)) return new Set(raw); + return /* @__PURE__ */ new Set([LIMIT_TIER_EXTERNAL]); +} +var NCR_LEVEL = Object.freeze({ allow: 0, leave: 1, remove: 2, throw: 3 }); +var XML10_ALLOWED_C0 = /* @__PURE__ */ new Set([9, 10, 13]); +function parseNCRConfig(ncr) { + if (!ncr) { + return { xmlVersion: 1, onLevel: NCR_LEVEL.allow, nullLevel: NCR_LEVEL.remove }; + } + const xmlVersion = ncr.xmlVersion === 1.1 ? 1.1 : 1; + const onLevel = NCR_LEVEL[ncr.onNCR] ?? NCR_LEVEL.allow; + const nullLevel = NCR_LEVEL[ncr.nullNCR] ?? NCR_LEVEL.remove; + const clampedNull = Math.max(nullLevel, NCR_LEVEL.remove); + return { xmlVersion, onLevel, nullLevel: clampedNull }; +} +var EntityDecoder = class { + /** + * @param {object} [options] + * @param {object|null} [options.namedEntities] — extra named entities merged into base map + * @param {object} [options.limit] — security limits + * @param {number} [options.limit.maxTotalExpansions=0] — 0 = unlimited + * @param {number} [options.limit.maxExpandedLength=0] — 0 = unlimited + * @param {'external'|'base'|'all'|string[]} [options.limit.applyLimitsTo='external'] + * Which entity tiers count against the security limits: + * - 'external' (default) — only input/runtime + persistent external entities + * - 'base' — only DEFAULT_XML_ENTITIES + namedEntities + * - 'all' — every entity regardless of tier + * - string[] — explicit combination, e.g. ['external', 'base'] + * @param {((resolved: string, original: string) => string)|null} [options.postCheck=null] + * @param {string[]} [options.remove=[]] — entity names (e.g. ['nbsp', '#13']) to delete (replace with empty string) + * @param {string[]} [options.leave=[]] — entity names to keep as literal (unchanged in output) + * @param {object} [options.ncr] — Numeric Character Reference controls + * @param {1.0|1.1} [options.ncr.xmlVersion=1.0] + * XML version governing which codepoint ranges are restricted: + * - 1.0 — C0 controls U+0001–U+001F (except U+0009/000A/000D) are prohibited + * - 1.1 — C0 controls are allowed when written as NCRs; C1 (U+007F–U+009F) decoded as-is + * @param {'allow'|'leave'|'remove'|'throw'} [options.ncr.onNCR='allow'] + * Base action for numeric references. Severity order: allow < leave < remove < throw. + * For codepoint ranges that carry a minimum level (surrogates → remove, XML 1.0 C0 → remove), + * the effective action is max(onNCR, rangeMinimum). + * @param {'remove'|'throw'} [options.ncr.nullNCR='remove'] + * Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe. + * @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} [options.onExternalEntity=null] + * Hook called when an external entity is registered via `setExternalEntities()` or + * `addExternalEntity()`. Return `ENTITY_ACTION.ALLOW` to accept the entity, + * `ENTITY_ACTION.BLOCK` to silently skip it, or `ENTITY_ACTION.THROW` to abort with an error. + * @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} [options.onInputEntity=null] + * Hook called when an input entity is registered via `addInputEntities()`. Return + * `ENTITY_ACTION.ALLOW` to accept, `ENTITY_ACTION.BLOCK` to silently skip, or + * `ENTITY_ACTION.THROW` to abort with an error. + */ + constructor(options = {}) { + this._limit = options.limit || {}; + this._maxTotalExpansions = this._limit.maxTotalExpansions || 0; + this._maxExpandedLength = this._limit.maxExpandedLength || 0; + this._postCheck = typeof options.postCheck === "function" ? options.postCheck : (r) => r; + this._limitTiers = parseLimitTiers(this._limit.applyLimitsTo ?? LIMIT_TIER_EXTERNAL); + this._numericAllowed = options.numericAllowed ?? true; + this._baseMap = mergeEntityMaps(XML, options.namedEntities || null); + this._externalMap = /* @__PURE__ */ Object.create(null); + this._inputMap = /* @__PURE__ */ Object.create(null); + this._totalExpansions = 0; + this._expandedLength = 0; + this._removeSet = new Set(options.remove && Array.isArray(options.remove) ? options.remove : []); + this._leaveSet = new Set(options.leave && Array.isArray(options.leave) ? options.leave : []); + const ncrCfg = parseNCRConfig(options.ncr); + this._ncrXmlVersion = ncrCfg.xmlVersion; + this._ncrOnLevel = ncrCfg.onLevel; + this._ncrNullLevel = ncrCfg.nullLevel; + this._onExternalEntity = typeof options.onExternalEntity === "function" ? options.onExternalEntity : null; + this._onInputEntity = typeof options.onInputEntity === "function" ? options.onInputEntity : null; + } + // ------------------------------------------------------------------------- + // Private: registration hook dispatch + // ------------------------------------------------------------------------- + /** + * Invoke a registration hook for a single entity name/value pair. + * Returns true when the entity should be accepted, false when it should be + * silently skipped (BLOCK), and throws when the hook returns THROW. + * + * @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} hook + * @param {string} name + * @param {string} value + * @param {string} context — used in error messages ('external' | 'input') + * @returns {boolean} true = accept, false = skip + */ + _applyRegistrationHook(hook, name, value, context3) { + if (!hook) return true; + const action5 = hook(name, value); + if (action5 === ENTITY_ACTION.BLOCK) return false; + if (action5 === ENTITY_ACTION.THROW) { + throw new Error( + `[EntityDecoder] Registration of ${context3} entity "&${name};" was rejected by hook` + ); + } + return true; + } + // ------------------------------------------------------------------------- + // Persistent external entity registration + // ------------------------------------------------------------------------- + /** + * Replace the full set of persistent external entities. + * All keys are validated — throws on invalid characters. + * If `onExternalEntity` is set, it is called once per entry; entries that + * return `ENTITY_ACTION.BLOCK` are silently omitted, `ENTITY_ACTION.THROW` + * aborts the whole call. + * @param {Record} map + */ + setExternalEntities(map) { + if (map) { + for (const key of Object.keys(map)) { + validateEntityName(key); + } + } + if (!this._onExternalEntity) { + this._externalMap = mergeEntityMaps(map); + return; + } + const flat = mergeEntityMaps(map); + const filtered = /* @__PURE__ */ Object.create(null); + for (const [name, value] of Object.entries(flat)) { + if (this._applyRegistrationHook(this._onExternalEntity, name, value, "external")) { + filtered[name] = value; + } + } + this._externalMap = filtered; + } + /** + * Add a single persistent external entity. + * If `onExternalEntity` is set it is called before the entity is stored; + * `ENTITY_ACTION.BLOCK` silently skips storage, `ENTITY_ACTION.THROW` raises. + * @param {string} key + * @param {string} value + */ + addExternalEntity(key, value) { + validateEntityName(key); + if (typeof value === "string" && value.indexOf("&") === -1) { + if (this._applyRegistrationHook(this._onExternalEntity, key, value, "external")) { + this._externalMap[key] = value; + } + } + } + // ------------------------------------------------------------------------- + // Input / runtime entity registration (per document) + // ------------------------------------------------------------------------- + /** + * Inject DOCTYPE entities for the current document. + * Also resets per-document expansion counters. + * If `onInputEntity` is set it is called once per entry; entries returning + * `ENTITY_ACTION.BLOCK` are silently omitted, `ENTITY_ACTION.THROW` aborts. + * @param {Record} map + */ + addInputEntities(map) { + this._totalExpansions = 0; + this._expandedLength = 0; + if (!this._onInputEntity) { + this._inputMap = mergeEntityMaps(map); + return; + } + const flat = mergeEntityMaps(map); + const filtered = /* @__PURE__ */ Object.create(null); + for (const [name, value] of Object.entries(flat)) { + if (this._applyRegistrationHook(this._onInputEntity, name, value, "input")) { + filtered[name] = value; + } + } + this._inputMap = filtered; + } + // ------------------------------------------------------------------------- + // Per-document reset + // ------------------------------------------------------------------------- + /** + * Wipe input/runtime entities and reset counters. + * Call this before processing each new document. + * @returns {this} + */ + reset() { + this._inputMap = /* @__PURE__ */ Object.create(null); + this._totalExpansions = 0; + this._expandedLength = 0; + return this; + } + // ------------------------------------------------------------------------- + // XML version (can be set after construction, e.g. once parser reads ) + // ------------------------------------------------------------------------- + /** + * Update the XML version used for NCR classification. + * Call this as soon as the document's `` declaration is parsed. + * @param {1.0|1.1|number} version + */ + setXmlVersion(version3) { + this._ncrXmlVersion = version3 === 1.1 ? 1.1 : 1; + } + // ------------------------------------------------------------------------- + // Primary API + // ------------------------------------------------------------------------- + /** + * Replace all entity references in `str` in a single pass. + * + * @param {string} str + * @returns {string} + */ + decode(str) { + if (typeof str !== "string" || str.length === 0) return str; + if (str.indexOf("&") === -1) return str; + const original = str; + const chunks = []; + const len = str.length; + let last = 0; + let i = 0; + const limitExpansions = this._maxTotalExpansions > 0; + const limitLength = this._maxExpandedLength > 0; + const checkLimits = limitExpansions || limitLength; + while (i < len) { + if (str.charCodeAt(i) !== 38) { + i++; + continue; + } + let j = i + 1; + while (j < len && str.charCodeAt(j) !== 59 && j - i <= 32) j++; + if (j >= len || str.charCodeAt(j) !== 59) { + i++; + continue; + } + const token = str.slice(i + 1, j); + if (token.length === 0) { + i++; + continue; + } + let replacement; + let tier2; + if (this._removeSet.has(token)) { + replacement = ""; + if (tier2 === void 0) { + tier2 = LIMIT_TIER_EXTERNAL; + } + } else if (this._leaveSet.has(token)) { + i++; + continue; + } else if (token.charCodeAt(0) === 35) { + const ncrResult = this._resolveNCR(token); + if (ncrResult === void 0) { + i++; + continue; + } + replacement = ncrResult; + tier2 = LIMIT_TIER_BASE; + } else { + const resolved = this._resolveName(token); + replacement = resolved?.value; + tier2 = resolved?.tier; + } + if (replacement === void 0) { + i++; + continue; + } + if (i > last) chunks.push(str.slice(last, i)); + chunks.push(replacement); + last = j + 1; + i = last; + if (checkLimits && this._tierCounts(tier2)) { + if (limitExpansions) { + this._totalExpansions++; + if (this._totalExpansions > this._maxTotalExpansions) { + throw new Error( + `[EntityReplacer] Entity expansion count limit exceeded: ${this._totalExpansions} > ${this._maxTotalExpansions}` + ); + } + } + if (limitLength) { + const delta = replacement.length - (token.length + 2); + if (delta > 0) { + this._expandedLength += delta; + if (this._expandedLength > this._maxExpandedLength) { + throw new Error( + `[EntityReplacer] Expanded content length limit exceeded: ${this._expandedLength} > ${this._maxExpandedLength}` + ); + } + } + } + } + } + if (last < len) chunks.push(str.slice(last)); + const result = chunks.length === 0 ? str : chunks.join(""); + return this._postCheck(result, original); + } + // ------------------------------------------------------------------------- + // Private: limit tier check + // ------------------------------------------------------------------------- + /** + * Returns true if a resolved entity of the given tier should count + * against the expansion/length limits. + * @param {string} tier — LIMIT_TIER_EXTERNAL | LIMIT_TIER_BASE + * @returns {boolean} + */ + _tierCounts(tier2) { + if (this._limitTiers.has(LIMIT_TIER_ALL)) return true; + return this._limitTiers.has(tier2); + } + // ------------------------------------------------------------------------- + // Private: entity resolution + // ------------------------------------------------------------------------- + /** + * Resolve a named entity token (without & and ;). + * Priority: inputMap > externalMap > baseMap + * Returns the resolved value tagged with its limit tier. + * + * @param {string} name + * @returns {{ value: string, tier: string }|undefined} + */ + _resolveName(name) { + if (name in this._inputMap) return { value: this._inputMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._externalMap) return { value: this._externalMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._baseMap) return { value: this._baseMap[name], tier: LIMIT_TIER_BASE }; + return void 0; + } + /** + * Classify a codepoint and return the minimum action level that must be applied. + * Returns -1 when no minimum is imposed (normal allow path). + * + * Ranges checked (in priority order): + * 1. U+0000 — null, governed by nullNCR (always ≥ remove) + * 2. U+D800–U+DFFF — surrogates, always prohibited (min: remove) + * 3. U+0001–U+001F \ {0x09,0x0A,0x0D} — XML 1.0 restricted C0 (min: remove) + * (skipped in XML 1.1 — C0 controls are allowed when written as NCRs) + * + * @param {number} cp — codepoint + * @returns {number} — minimum NCR_LEVEL value, or -1 for no restriction + */ + _classifyNCR(cp) { + if (cp === 0) return this._ncrNullLevel; + if (cp >= 55296 && cp <= 57343) return NCR_LEVEL.remove; + if (this._ncrXmlVersion === 1) { + if (cp >= 1 && cp <= 31 && !XML10_ALLOWED_C0.has(cp)) return NCR_LEVEL.remove; + } + return -1; + } + /** + * Execute a resolved NCR action. + * + * @param {number} action — NCR_LEVEL value + * @param {string} token — raw token (e.g. '#38') for error messages + * @param {number} cp — codepoint, used only for error messages + * @returns {string|undefined} + * - decoded character string → 'allow' + * - '' → 'remove' + * - undefined → 'leave' (caller must skip past '&' only) + * - throws Error → 'throw' + */ + _applyNCRAction(action5, token, cp) { + switch (action5) { + case NCR_LEVEL.allow: + return String.fromCodePoint(cp); + case NCR_LEVEL.remove: + return ""; + case NCR_LEVEL.leave: + return void 0; + // signal: keep literal + case NCR_LEVEL.throw: + throw new Error( + `[EntityDecoder] Prohibited numeric character reference &${token}; (U+${cp.toString(16).toUpperCase().padStart(4, "0")})` + ); + default: + return String.fromCodePoint(cp); + } + } + /** + * Full NCR resolution pipeline for a numeric token. + * + * Steps: + * 1. Parse the codepoint (decimal or hex). + * 2. Validate the raw codepoint range (NaN, <0, >0x10FFFF). + * 3. If numericAllowed is false and no minimum restriction applies → leave as-is. + * 4. Classify the codepoint to find the minimum required action level. + * 5. Resolve effective action = max(onNCR, minimum). + * 6. Apply and return. + * + * @param {string} token — e.g. '#38', '#x26', '#X26' + * @returns {string|undefined} + * - string (incl. '') — replacement ('' = remove) + * - undefined — leave original &token; as-is + */ + _resolveNCR(token) { + const second = token.charCodeAt(1); + let cp; + if (second === 120 || second === 88) { + cp = parseInt(token.slice(2), 16); + } else { + cp = parseInt(token.slice(1), 10); + } + if (Number.isNaN(cp) || cp < 0 || cp > 1114111) return void 0; + const minimum = this._classifyNCR(cp); + if (!this._numericAllowed && minimum < NCR_LEVEL.remove) return void 0; + const effective = minimum === -1 ? this._ncrOnLevel : Math.max(this._ncrOnLevel, minimum); + return this._applyNCRAction(effective, token, cp); + } +}; + // node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js +var defaultOnDangerousProperty = (name) => { + if (DANGEROUS_PROPERTY_NAMES.includes(name)) { + return "__" + name; + } + return name; +}; var defaultOptions2 = { preserveOrder: false, attributeNamePrefix: "@_", @@ -35058,7 +36834,8 @@ var defaultOptions2 = { numberParseOptions: { hex: true, leadingZeros: true, - eNotation: true + eNotation: true, + unicode: false }, tagValueProcessor: function(tagName, val) { return val; @@ -35074,6 +36851,7 @@ var defaultOptions2 = { unpairedTags: [], processEntities: true, htmlEntities: false, + entityDecoder: null, ignoreDeclaration: false, ignorePiTags: false, transformTagName: false, @@ -35084,38 +36862,84 @@ var defaultOptions2 = { // skipEmptyListItem: false captureMetaData: false, maxNestedTags: 100, - strictReservedNames: true + strictReservedNames: true, + jPath: true, + // if true, pass jPath string to callbacks; if false, pass matcher instance + onDangerousProperty: defaultOnDangerousProperty }; -function normalizeProcessEntities(value) { +function validatePropertyName(propertyName, optionName) { + if (typeof propertyName !== "string") { + return; + } + const normalized = propertyName.toLowerCase(); + if (DANGEROUS_PROPERTY_NAMES.some((dangerous) => normalized === dangerous.toLowerCase())) { + throw new Error( + `[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution` + ); + } + if (criticalProperties.some((dangerous) => normalized === dangerous.toLowerCase())) { + throw new Error( + `[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution` + ); + } +} +function normalizeProcessEntities(value, htmlEntities) { if (typeof value === "boolean") { return { enabled: value, // true or false maxEntitySize: 1e4, - maxExpansionDepth: 10, - maxTotalExpansions: 1e3, + maxExpansionDepth: 1e4, + maxTotalExpansions: Infinity, maxExpandedLength: 1e5, + maxEntityCount: 1e3, allowedTags: null, - tagFilter: null + tagFilter: null, + appliesTo: "all" }; } if (typeof value === "object" && value !== null) { return { enabled: value.enabled !== false, - // default true if not specified - maxEntitySize: value.maxEntitySize ?? 1e4, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1e3, - maxExpandedLength: value.maxExpandedLength ?? 1e5, + maxEntitySize: Math.max(1, value.maxEntitySize ?? 1e4), + maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 1e4), + maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity), + maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 1e5), + maxEntityCount: Math.max(1, value.maxEntityCount ?? 1e3), allowedTags: value.allowedTags ?? null, - tagFilter: value.tagFilter ?? null + tagFilter: value.tagFilter ?? null, + appliesTo: value.appliesTo ?? "all" }; } return normalizeProcessEntities(true); } var buildOptions = function(options) { const built = Object.assign({}, defaultOptions2, options); - built.processEntities = normalizeProcessEntities(built.processEntities); + const propertyNameOptions = [ + { value: built.attributeNamePrefix, name: "attributeNamePrefix" }, + { value: built.attributesGroupName, name: "attributesGroupName" }, + { value: built.textNodeName, name: "textNodeName" }, + { value: built.cdataPropName, name: "cdataPropName" }, + { value: built.commentPropName, name: "commentPropName" } + ]; + for (const { value, name } of propertyNameOptions) { + if (value) { + validatePropertyName(value, name); + } + } + if (built.onDangerousProperty === null) { + built.onDangerousProperty = defaultOnDangerousProperty; + } + built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities); + built.unpairedTagsSet = new Set(built.unpairedTags); + if (built.stopNodes && Array.isArray(built.stopNodes)) { + built.stopNodes = built.stopNodes.map((node) => { + if (typeof node === "string" && node.startsWith("*.")) { + return ".." + node.substring(2); + } + return node; + }); + } return built; }; @@ -35153,14 +36977,41 @@ var XmlNode = class { } }; +// node_modules/xml-naming/src/index.js +var nameStartChar10 = ":A-Za-z_\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u0486\u0488-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD"; +var nameChar10 = nameStartChar10 + "\\-\\.\\d\xB7\u0300-\u036F\u203F-\u2040"; +var nameStartChar11 = ":A-Za-z_\xC0-\u02FF\u0370-\u037D\u037F-\u0486\u0488-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}"; +var nameChar11 = nameStartChar11 + "\\-\\.\\d\xB7\u0300-\u036F\u0487\u203F-\u2040"; +var buildRegexes = (startChar, char, flags = "") => { + const ncStart = startChar.replace(":", ""); + const ncChar = char.replace(":", ""); + const ncNamePat = `[${ncStart}][${ncChar}]*`; + return { + name: new RegExp(`^[${startChar}][${char}]*$`, flags), + ncName: new RegExp(`^${ncNamePat}$`, flags), + qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags), + nmToken: new RegExp(`^[${char}]+$`, flags), + nmTokens: new RegExp(`^[${char}]+(?:\\s+[${char}]+)*$`, flags) + }; +}; +var regexes10 = buildRegexes(nameStartChar10, nameChar10); +var regexes11 = buildRegexes(nameStartChar11, nameChar11, "u"); +var getRegexes = (xmlVersion = "1.0") => xmlVersion === "1.1" ? regexes11 : regexes10; +var qName = (str, { xmlVersion = "1.0" } = {}) => getRegexes(xmlVersion).qName.test(str); + // node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js var DocTypeReader = class { - constructor(options) { + constructor(options, xmlVersion) { this.suppressValidationErr = !options; this.options = options; + this.xmlVersion = xmlVersion || 1; + } + setXmlVersion(xmlVersion = 1) { + this.xmlVersion = xmlVersion; } readDocType(xmlData, i) { const entities = /* @__PURE__ */ Object.create(null); + let entityCount = 0; if (xmlData[i + 3] === "O" && xmlData[i + 4] === "C" && xmlData[i + 5] === "T" && xmlData[i + 6] === "Y" && xmlData[i + 7] === "P" && xmlData[i + 8] === "E") { i = i + 9; let angleBracketsCount = 1; @@ -35173,11 +37024,13 @@ var DocTypeReader = class { let entityName, val; [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { - const escaped = entityName.replace(/[.\-+*:]/g, "\\."); - entities[entityName] = { - regx: RegExp(`&${escaped};`, "g"), - val - }; + if (this.options.enabled !== false && this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { + throw new Error( + `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` + ); + } + entities[entityName] = val; + entityCount++; } } else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) { i += 8; @@ -35221,12 +37074,12 @@ var DocTypeReader = class { } readEntityExp(xmlData, i) { i = skipWhitespace(xmlData, i); - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } - validateEntityName(entityName); + let entityName = xmlData.substring(startIndex, i); + validateEntityName2(entityName, { xmlVersion: this.xmlVersion }); i = skipWhitespace(xmlData, i); if (!this.suppressValidationErr) { if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") { @@ -35237,7 +37090,7 @@ var DocTypeReader = class { } let entityValue = ""; [i, entityValue] = this.readIdentifierVal(xmlData, i, "entity"); - if (this.options.enabled !== false && this.options.maxEntitySize && entityValue.length > this.options.maxEntitySize) { + if (this.options.enabled !== false && this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` ); @@ -35247,12 +37100,12 @@ var DocTypeReader = class { } readNotationExp(xmlData, i) { i = skipWhitespace(xmlData, i); - let notationName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } - !this.suppressValidationErr && validateEntityName(notationName); + let notationName = xmlData.substring(startIndex, i); + !this.suppressValidationErr && validateEntityName2(notationName, { xmlVersion: this.xmlVersion }); i = skipWhitespace(xmlData, i); const identifierType = xmlData.substring(i, i + 6).toUpperCase(); if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") { @@ -35283,10 +37136,11 @@ var DocTypeReader = class { throw new Error(`Expected quoted string, found "${startChar}"`); } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); } @@ -35295,12 +37149,12 @@ var DocTypeReader = class { } readElementExp(xmlData, i) { i = skipWhitespace(xmlData, i); - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } - if (!this.suppressValidationErr && !isName(elementName)) { + let elementName = xmlData.substring(startIndex, i); + if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } i = skipWhitespace(xmlData, i); @@ -35309,10 +37163,11 @@ var DocTypeReader = class { else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) i += 2; else if (xmlData[i] === "(") { i++; + const startIndex2 = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex2, i); if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -35327,19 +37182,19 @@ var DocTypeReader = class { } readAttlistExp(xmlData, i) { i = skipWhitespace(xmlData, i); - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } - validateEntityName(elementName); + let elementName = xmlData.substring(startIndex, i); + validateEntityName2(elementName, { xmlVersion: this.xmlVersion }); i = skipWhitespace(xmlData, i); - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } - if (!validateEntityName(attributeName)) { + let attributeName = xmlData.substring(startIndex, i); + if (!validateEntityName2(attributeName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid attribute name: "${attributeName}"`); } i = skipWhitespace(xmlData, i); @@ -35354,13 +37209,13 @@ var DocTypeReader = class { i++; let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + const startIndex2 = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex2, i); notation = notation.trim(); - if (!validateEntityName(notation)) { + if (!validateEntityName2(notation, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid notation name: "${notation}"`); } allowedNotations.push(notation); @@ -35375,10 +37230,11 @@ var DocTypeReader = class { i++; attributeType += " (" + allowedNotations.join("|") + ")"; } else { + const startIndex2 = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex2, i); const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) { throw new Error(`Invalid attribute type: "${attributeType}"`); @@ -35416,34 +37272,275 @@ function hasSeq(data, seq, i) { } return true; } -function validateEntityName(name) { - if (isName(name)) +function validateEntityName2(name, xmlVersion) { + if (qName(name, { xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); } +// node_modules/anynum/digitTable.js +var SCRIPT_ZEROS = [ + // Basic Latin (ASCII) — included for completeness / pass-through + 48, + // 0-9 + // Arabic scripts + 1632, + // Arabic-Indic ٠١٢٣٤٥٦٧٨٩ + 1776, + // Extended Arabic-Indic (Urdu/Persian/Sindhi) ۰۱۲۳ + // Indic scripts + 2406, + // Devanagari ०१२३४५६७८९ + 2534, + // Bengali ০১২৩৪৫৬৭৮৯ + 2662, + // Gurmukhi ੦੧੨੩੪੫੬੭੮੯ + 2790, + // Gujarati ૦૧૨૩૪૫૬૭૮૯ + 2918, + // Odia ୦୧୨୩୪୫୬୭୮୯ + 3046, + // Tamil ௦௧௨௩௪௫௬௭௮௯ + 3174, + // Telugu ౦౧౨౩౪౫౬౭౮౯ + 3302, + // Kannada ೦೧೨೩೪೫೬೭೮೯ + 3430, + // Malayalam ൦൧൨൩൪൫൬൭൮൯ + 3558, + // Sinhala Archaic ෦෧෨෩෪෫෬෭෮෯ + // Southeast Asian scripts + 3664, + // Thai ๐๑๒๓๔๕๖๗๘๙ + 3792, + // Lao ໐໑໒໓໔໕໖໗໘໙ + 3872, + // Tibetan ༠༡༢༣༤༥༦༧༨༩ + 4160, + // Myanmar ၀၁၂၃၄၅၆၇၈၉ + 4240, + // Myanmar Shan ႐႑႒႓႔႕႖႗႘႙ + 6112, + // Khmer ០១២៣៤៥៦៧៨៩ + 6160, + // Mongolian ᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙ + 6470, + // Limbu ᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏ + 6608, + // New Tai Lue ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙ + 6784, + // Tai Tham Hora ᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉ + 6800, + // Tai Tham Tham ᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙ + 6992, + // Balinese ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙ + 7088, + // Sundanese ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹ + 7232, + // Lepcha ᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉ + 7248, + // Ol Chiki ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙ + // Fullwidth (CJK context) + 65296, + // Fullwidth 0123456789 + // Mathematical digit variants (Unicode math block) + 120782, + // Mathematical Bold + 120792, + // Mathematical Double-Struck + 120802, + // Mathematical Sans-Serif + 120812, + // Mathematical Sans-Serif Bold + 120822, + // Mathematical Monospace + // Other scripts + 66720, + // Osmanya 𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩 + 68912, + // Hanifi Rohingya 𐴰𐴱𐴲𐴳𐴴𐴵𐴶𐴷𐴸𐴹 + 69734, + // Brahmi 𑁦𑁧𑁨𑁩𑁪𑁫𑁬𑁭𑁮𑁯 + 69872, + // Sora Sompeng 𑃰𑃱𑃲𑃳𑃴𑃵𑃶𑃷𑃸𑃹 + 69942, + // Chakma 𑄶𑄷𑄸𑄹𑄺𑄻𑄼𑄽𑄾𑄿 + 70096, + // Sharada 𑇐𑇑𑇒𑇓𑇔𑇕𑇖𑇗𑇘𑇙 + 70384, + // Khudawadi 𑋰𑋱𑋲𑋳𑋴𑋵𑋶𑋷𑋸𑋹 + 70736, + // Newa 𑑐𑑑𑑒𑑓𑑔𑑕𑑖𑑗𑑘𑑙 + 70864, + // Tirhuta 𑓐𑓑𑓒𑓓𑓔𑓕𑓖𑓗𑓘𑓙 + 71248, + // Modi 𑙐𑙑𑙒𑙓𑙔𑙕𑙖𑙗𑙘𑙙 + 71360, + // Takri 𑛀𑛁𑛂𑛃𑛄𑛅𑛆𑛇𑛈𑛉 + 71472, + // Ahom 𑜰𑜱𑜲𑜳𑜴𑜵𑜶𑜷𑜸𑜹 + 71904, + // Warang Citi 𑣠𑣡𑣢𑣣𑣤𑣥𑣦𑣧𑣨𑣩 + 72016, + // Dives Akuru 𑥐𑥑𑥒𑥓𑥔𑥕𑥖𑥗𑥘𑥙 + 72688, + // Khitan Small Script 𑯰𑯱𑯲𑯳𑯴𑯵𑯶𑯷𑯸𑯹 + 72784, + // Bhaiksuki 𑱐𑱑𑱒𑱓𑱔𑱕𑱖𑱗𑱘𑱙 + 73040, + // Masaram Gondi 𑵐𑵑𑵒𑵓𑵔𑵕𑵖𑵗𑵘𑵙 + 73120, + // Gunjala Gondi 𑶠𑶡𑶢𑶣𑶤𑶥𑶦𑶧𑶨𑶩 + 73552, + // Kawi 𑽐𑽑𑽒𑽓𑽔𑽕𑽖𑽗𑽘𑽙 + 92768, + // Mro 𖩠𖩡𖩢𖩣𖩤𖩥𖩦𖩧𖩨𖩩 + 92864, + // Tangsa 𖫀𖫁𖫂𖫃𖫄𖫅𖫆𖫇𖫈𖫉 + 93008, + // Pahawh Hmong 𖭐𖭑𖭒𖭓𖭔𖭕𖭖𖭗𖭘𖭙 + 123200, + // Nyiakeng Puachue Hmong 𞅀𞅁𞅂𞅃𞅄𞅅𞅆𞅇𞅈𞅉 + 123632, + // Wancho 𞋰𞋱𞋲𞋳𞋴𞋵𞋶𞋷𞋸𞋹 + 124144, + // Nag Mundari 𞓰𞓱𞓲𞓳𞓴𞓵𞓶𞓷𞓸𞓹 + 125264, + // Adlam 𞥐𞥑𞥒𞥓𞥔𞥕𞥖𞥗𞥘𞥙 + 130032 + // Segmented digit symbols 🯰🯱🯲🯳🯴🯵🯶🯷🯸🯹 +]; +var NOT_DIGIT = 255; +var HIGH_MAP = /* @__PURE__ */ new Map(); +var LOW_MAX = 65535; +var LOW_MIN = 1632; +var TABLE_OFFSET = LOW_MIN; +var TABLE_SIZE = LOW_MAX - LOW_MIN + 1; +var TABLE = new Uint8Array(TABLE_SIZE).fill(NOT_DIGIT); +for (const zero of SCRIPT_ZEROS) { + for (let d = 0; d < 10; d++) { + const cp = zero + d; + if (cp <= LOW_MAX) { + TABLE[cp - TABLE_OFFSET] = d; + } else { + HIGH_MAP.set(cp, d); + } + } +} + +// node_modules/anynum/anynum.js +var CHAR_0 = 48; +var CHAR_9 = 57; +var CHAR_MINUS = 45; +var MINUS_SET = /* @__PURE__ */ new Set([8722, 65293, 65123]); +function anynum(str) { + if (typeof str !== "string") return str; + const len = str.length; + if (len === 0) return str; + let firstHit = -1; + for (let i = 0; i < len; i++) { + const cc = str.charCodeAt(i); + if (cc >= CHAR_0 && cc <= CHAR_9 || cc === CHAR_MINUS) continue; + if (cc < TABLE_OFFSET) { + if (MINUS_SET.has(cc)) { + firstHit = i; + break; + } + continue; + } + if (cc >= 55296 && cc <= 56319) { + if (i + 1 < len) { + const low = str.charCodeAt(i + 1); + if (low >= 56320 && low <= 57343) { + const cp = 65536 + (cc - 55296 << 10) + (low - 56320); + if (HIGH_MAP.has(cp)) { + firstHit = i; + break; + } + } + } + continue; + } + if (TABLE[cc - TABLE_OFFSET] !== NOT_DIGIT || MINUS_SET.has(cc)) { + firstHit = i; + break; + } + } + if (firstHit === -1) return str; + const chars = []; + if (firstHit > 0) chars.push(str.slice(0, firstHit)); + for (let i = firstHit; i < len; i++) { + const cc = str.charCodeAt(i); + if (cc >= CHAR_0 && cc <= CHAR_9 || cc === CHAR_MINUS) { + chars.push(str[i]); + continue; + } + if (cc < TABLE_OFFSET) { + chars.push(MINUS_SET.has(cc) ? "-" : str[i]); + continue; + } + if (cc >= 55296 && cc <= 56319) { + if (i + 1 < len) { + const low = str.charCodeAt(i + 1); + if (low >= 56320 && low <= 57343) { + const cp = 65536 + (cc - 55296 << 10) + (low - 56320); + const d2 = HIGH_MAP.get(cp); + if (d2 !== void 0) { + chars.push(String.fromCharCode(d2 + 48)); + i++; + continue; + } + } + } + chars.push(str[i]); + continue; + } + if (MINUS_SET.has(cc)) { + chars.push("-"); + continue; + } + const d = TABLE[cc - TABLE_OFFSET]; + chars.push(d !== NOT_DIGIT ? String.fromCharCode(d + 48) : str[i]); + } + return chars.join(""); +} +var anynum_default = anynum; + // node_modules/strnum/strnum.js var hexRegex = /^[-+]?0x[a-fA-F0-9]+$/; +var binRegex = /^0b[01]+$/; +var octRegex = /^0o[0-7]+$/; var numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/; var consider = { hex: true, - // oct: false, + binary: false, + octal: false, leadingZeros: true, decimalPoint: ".", eNotation: true, //skipLike: /regex/, - infinity: "original" + infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal)) + unicode: false }; function toNumber(str, options = {}) { options = Object.assign({}, consider, options); if (!str || typeof str !== "string") return str; let trimmedStr = str.trim(); - if (options.skipLike !== void 0 && options.skipLike.test(trimmedStr)) return str; - else if (str === "0") return 0; - else if (options.hex && hexRegex.test(trimmedStr)) { + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== void 0 && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; + if (options.unicode) { + trimmedStr = anynum_default(trimmedStr); + if (trimmedStr === "0") return 0; + } + if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); + } else if (options.binary && binRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 2); + } else if (options.octal && octRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 8); } else if (!isFinite(trimmedStr)) { return handleInfinity(str, Number(trimmedStr), options); } else if (trimmedStr.includes("e") || trimmedStr.includes("E")) { @@ -35500,10 +37597,14 @@ function resolveEnotation(str, trimmedStr, options) { if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; else if (leadingZeros.length === 1 && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - } else if (options.leadingZeros && !eAdjacentToLeadingZeros) { - trimmedStr = (notation[1] || "") + notation[3]; + } else if (leadingZeros.length > 0) { + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { return Number(trimmedStr); - } else return str; + } } else { return str; } @@ -35519,6 +37620,8 @@ function trimZeros(numStr) { return numStr; } function parse_int(numStr, base) { + const str = numStr.trim(); + if (base === 2 || base === 8) numStr = str.substring(2); if (parseInt) return parseInt(numStr, base); else if (Number.parseInt) return Number.parseInt(numStr, base); else if (window && window.parseInt) return window.parseInt(numStr, base); @@ -35560,38 +37663,1610 @@ function getIgnoreAttributesFn(ignoreAttributes) { return () => false; } +// node_modules/path-expression-matcher/src/Expression.js +var Expression = class { + /** + * Create a new Expression + * @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]") + * @param {Object} options - Configuration options + * @param {string} options.separator - Path separator (default: '.') + */ + constructor(pattern, options = {}, data) { + this.pattern = pattern; + this.separator = options.separator || "."; + this.segments = this._parse(pattern); + this.data = data; + this._hasDeepWildcard = this.segments.some((seg) => seg.type === "deep-wildcard"); + this._hasAttributeCondition = this.segments.some((seg) => seg.attrName !== void 0); + this._hasPositionSelector = this.segments.some((seg) => seg.position !== void 0); + } + /** + * Parse pattern string into segments + * @private + * @param {string} pattern - Pattern to parse + * @returns {Array} Array of segment objects + */ + _parse(pattern) { + const segments = []; + let i = 0; + let currentPart = ""; + while (i < pattern.length) { + if (pattern[i] === this.separator) { + if (i + 1 < pattern.length && pattern[i + 1] === this.separator) { + if (currentPart.trim()) { + segments.push(this._parseSegment(currentPart.trim())); + currentPart = ""; + } + segments.push({ type: "deep-wildcard" }); + i += 2; + } else { + if (currentPart.trim()) { + segments.push(this._parseSegment(currentPart.trim())); + } + currentPart = ""; + i++; + } + } else { + currentPart += pattern[i]; + i++; + } + } + if (currentPart.trim()) { + segments.push(this._parseSegment(currentPart.trim())); + } + return segments; + } + /** + * Parse a single segment + * @private + * @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first") + * @returns {Object} Segment object + */ + _parseSegment(part) { + const segment = { type: "tag" }; + let bracketContent = null; + let withoutBrackets = part; + const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/); + if (bracketMatch) { + withoutBrackets = bracketMatch[1] + bracketMatch[3]; + if (bracketMatch[2]) { + const content = bracketMatch[2].slice(1, -1); + if (content) { + bracketContent = content; + } + } + } + let namespace = void 0; + let tagAndPosition = withoutBrackets; + if (withoutBrackets.includes("::")) { + const nsIndex = withoutBrackets.indexOf("::"); + namespace = withoutBrackets.substring(0, nsIndex).trim(); + tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); + if (!namespace) { + throw new Error(`Invalid namespace in pattern: ${part}`); + } + } + let tag = void 0; + let positionMatch = null; + if (tagAndPosition.includes(":")) { + const colonIndex = tagAndPosition.lastIndexOf(":"); + const tagPart = tagAndPosition.substring(0, colonIndex).trim(); + const posPart = tagAndPosition.substring(colonIndex + 1).trim(); + const isPositionKeyword = ["first", "last", "odd", "even"].includes(posPart) || /^nth\(\d+\)$/.test(posPart); + if (isPositionKeyword) { + tag = tagPart; + positionMatch = posPart; + } else { + tag = tagAndPosition; + } + } else { + tag = tagAndPosition; + } + if (!tag) { + throw new Error(`Invalid segment pattern: ${part}`); + } + segment.tag = tag; + if (namespace) { + segment.namespace = namespace; + } + if (bracketContent) { + if (bracketContent.includes("=")) { + const eqIndex = bracketContent.indexOf("="); + segment.attrName = bracketContent.substring(0, eqIndex).trim(); + segment.attrValue = bracketContent.substring(eqIndex + 1).trim(); + } else { + segment.attrName = bracketContent.trim(); + } + } + if (positionMatch) { + const nthMatch = positionMatch.match(/^nth\((\d+)\)$/); + if (nthMatch) { + segment.position = "nth"; + segment.positionValue = parseInt(nthMatch[1], 10); + } else { + segment.position = positionMatch; + } + } + return segment; + } + /** + * Get the number of segments + * @returns {number} + */ + get length() { + return this.segments.length; + } + /** + * Check if expression contains deep wildcard + * @returns {boolean} + */ + hasDeepWildcard() { + return this._hasDeepWildcard; + } + /** + * Check if expression has attribute conditions + * @returns {boolean} + */ + hasAttributeCondition() { + return this._hasAttributeCondition; + } + /** + * Check if expression has position selectors + * @returns {boolean} + */ + hasPositionSelector() { + return this._hasPositionSelector; + } + /** + * Get string representation + * @returns {string} + */ + toString() { + return this.pattern; + } +}; + +// node_modules/path-expression-matcher/src/ExpressionSet.js +var ExpressionSet = class { + constructor() { + this._byDepthAndTag = /* @__PURE__ */ new Map(); + this._wildcardByDepth = /* @__PURE__ */ new Map(); + this._deepWildcards = []; + this._deepByTerminalTag = /* @__PURE__ */ new Map(); + this._patterns = /* @__PURE__ */ new Set(); + this._sealed = false; + } + /** + * Add an Expression to the set. + * Duplicate patterns (same pattern string) are silently ignored. + * + * @param {import('./Expression.js').default} expression - A pre-constructed Expression instance + * @returns {this} for chaining + * @throws {TypeError} if called after seal() + * + * @example + * set.add(new Expression('root.users.user')); + * set.add(new Expression('..script')); + */ + add(expression) { + if (this._sealed) { + throw new TypeError( + "ExpressionSet is sealed. Create a new ExpressionSet to add more expressions." + ); + } + if (this._patterns.has(expression.pattern)) return this; + this._patterns.add(expression.pattern); + if (expression.hasDeepWildcard()) { + const lastSeg2 = expression.segments[expression.segments.length - 1]; + if (lastSeg2 && lastSeg2.type !== "deep-wildcard" && lastSeg2.tag !== "*") { + const tag2 = lastSeg2.tag; + if (!this._deepByTerminalTag.has(tag2)) this._deepByTerminalTag.set(tag2, []); + this._deepByTerminalTag.get(tag2).push(expression); + } else { + this._deepWildcards.push(expression); + } + return this; + } + const depth = expression.length; + const lastSeg = expression.segments[expression.segments.length - 1]; + const tag = lastSeg?.tag; + if (!tag || tag === "*") { + if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []); + this._wildcardByDepth.get(depth).push(expression); + } else { + const key = `${depth}:${tag}`; + if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []); + this._byDepthAndTag.get(key).push(expression); + } + return this; + } + /** + * Add multiple expressions at once. + * + * @param {import('./Expression.js').default[]} expressions - Array of Expression instances + * @returns {this} for chaining + * + * @example + * set.addAll([ + * new Expression('root.users.user'), + * new Expression('root.config.setting'), + * ]); + */ + addAll(expressions) { + for (const expr of expressions) this.add(expr); + return this; + } + /** + * Check whether a pattern string is already present in the set. + * + * @param {import('./Expression.js').default} expression + * @returns {boolean} + */ + has(expression) { + return this._patterns.has(expression.pattern); + } + /** + * Number of expressions in the set. + * @type {number} + */ + get size() { + return this._patterns.size; + } + /** + * Seal the set against further modifications. + * Useful to prevent accidental mutations after config is built. + * Calling add() or addAll() on a sealed set throws a TypeError. + * + * @returns {this} + */ + seal() { + this._sealed = true; + return this; + } + /** + * Whether the set has been sealed. + * @type {boolean} + */ + get isSealed() { + return this._sealed; + } + /** + * Test whether the matcher's current path matches any expression in the set. + * + * Evaluation order (cheapest → most expensive): + * 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions + * 2. Depth-only wildcard bucket — O(1) lookup, rare + * 3. Deep-wildcard list — always checked, but usually small + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {boolean} true if any expression matches the current path + * + * @example + * if (stopNodes.matchesAny(matcher)) { + * // handle stop node + * } + */ + matchesAny(matcher) { + return this.findMatch(matcher) !== null; + } + /** + * Find and return the first Expression that matches the matcher's current path. + * + * Uses the same evaluation order as matchesAny (cheapest → most expensive): + * 1. Exact depth + tag bucket + * 2. Depth-only wildcard bucket + * 3. Deep-wildcard list + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {import('./Expression.js').default | null} the first matching Expression, or null + * + * @example + * const expr = stopNodes.findMatch(matcher); + * if (expr) { + * // access expr.config, expr.pattern, etc. + * } + */ + findMatch(matcher) { + const depth = matcher.getDepth(); + const tag = matcher.getCurrentTag(); + const exactKey = `${depth}:${tag}`; + const exactBucket = this._byDepthAndTag.get(exactKey); + if (exactBucket) { + for (let i = 0; i < exactBucket.length; i++) { + if (matcher.matches(exactBucket[i])) return exactBucket[i]; + } + } + const wildcardBucket = this._wildcardByDepth.get(depth); + if (wildcardBucket) { + for (let i = 0; i < wildcardBucket.length; i++) { + if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i]; + } + } + const deepBucket = this._deepByTerminalTag.get(tag); + if (deepBucket) { + for (let i = 0; i < deepBucket.length; i++) { + if (matcher.matches(deepBucket[i])) return deepBucket[i]; + } + } + for (let i = 0; i < this._deepWildcards.length; i++) { + if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i]; + } + return null; + } +}; + +// node_modules/path-expression-matcher/src/Matcher.js +var MatcherView = class { + /** + * @param {Matcher} matcher - The parent Matcher instance to read from. + */ + constructor(matcher) { + this._matcher = matcher; + } + /** + * Get the path separator used by the parent matcher. + * @returns {string} + */ + get separator() { + return this._matcher.separator; + } + /** + * Get current tag name. + * @returns {string|undefined} + */ + getCurrentTag() { + const path11 = this._matcher.path; + return path11.length > 0 ? path11[path11.length - 1].tag : void 0; + } + /** + * Get current namespace. + * @returns {string|undefined} + */ + getCurrentNamespace() { + const path11 = this._matcher.path; + return path11.length > 0 ? path11[path11.length - 1].namespace : void 0; + } + /** + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} + */ + getAttrValue(attrName) { + const path11 = this._matcher.path; + if (path11.length === 0) return void 0; + return path11[path11.length - 1].values?.[attrName]; + } + /** + * Check if current node has an attribute. + * @param {string} attrName + * @returns {boolean} + */ + hasAttr(attrName) { + const path11 = this._matcher.path; + if (path11.length === 0) return false; + const current = path11[path11.length - 1]; + return current.values !== void 0 && attrName in current.values; + } + /** + * Get the value of a "kept" attribute from the nearest ancestor (or + * current node) that declared it via `push(tag, attrs, ns, { keep: [...] })`. + * @param {string} attrName + * @returns {*} + */ + getAnyParentAttr(attrName) { + return this._matcher.getAnyParentAttr(attrName); + } + /** + * Check whether any ancestor (or the current node) kept the given + * attribute via `push(tag, attrs, ns, { keep: [...] })`. + * @param {string} attrName + * @returns {boolean} + */ + hasAnyParentAttr(attrName) { + return this._matcher.hasAnyParentAttr(attrName); + } + /** + * Get current node's sibling position (child index in parent). + * @returns {number} + */ + getPosition() { + const path11 = this._matcher.path; + if (path11.length === 0) return -1; + return path11[path11.length - 1].position ?? 0; + } + /** + * Get current node's repeat counter (occurrence count of this tag name). + * @returns {number} + */ + getCounter() { + const path11 = this._matcher.path; + if (path11.length === 0) return -1; + return path11[path11.length - 1].counter ?? 0; + } + /** + * Get current node's sibling index (alias for getPosition). + * @returns {number} + * @deprecated Use getPosition() or getCounter() instead + */ + getIndex() { + return this.getPosition(); + } + /** + * Get current path depth. + * @returns {number} + */ + getDepth() { + return this._matcher.path.length; + } + /** + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] + * @returns {string} + */ + toString(separator, includeNamespace = true) { + return this._matcher.toString(separator, includeNamespace); + } + /** + * Get path as array of tag names. + * @returns {string[]} + */ + toArray() { + return this._matcher.path.map((n) => n.tag); + } + /** + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} + */ + matches(expression) { + return this._matcher.matches(expression); + } + /** + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this._matcher); + } +}; +var Matcher = class { + /** + * Create a new Matcher. + * @param {Object} [options={}] + * @param {string} [options.separator='.'] - Default path separator + */ + constructor(options = {}) { + this.separator = options.separator || "."; + this.path = []; + this.siblingStacks = []; + this._pathStringCache = null; + this._view = new MatcherView(this); + this._keptAttrs = []; + } + /** + * Push a new tag onto the path. + * @param {string} tagName + * @param {Object|null} [attrValues=null] + * @param {string|null} [namespace=null] + * @param {Object|null} [options=null] + * @param {string[]} [options.keep] - Names of attributes (from attrValues) + */ + push(tagName, attrValues = null, namespace = null, options = null) { + this._pathStringCache = null; + if (this.path.length > 0) { + this.path[this.path.length - 1].values = void 0; + } + const currentLevel = this.path.length; + if (!this.siblingStacks[currentLevel]) { + this.siblingStacks[currentLevel] = /* @__PURE__ */ new Map(); + } + const siblings = this.siblingStacks[currentLevel]; + const siblingKey = namespace ? `${namespace}:${tagName}` : tagName; + const counter = siblings.get(siblingKey) || 0; + let position = 0; + for (const count of siblings.values()) { + position += count; + } + siblings.set(siblingKey, counter + 1); + const node = { + tag: tagName, + position, + counter + }; + if (namespace !== null && namespace !== void 0) { + node.namespace = namespace; + } + if (attrValues !== null && attrValues !== void 0) { + node.values = attrValues; + } + this.path.push(node); + const depth = this.path.length; + const keep = options !== null ? options.keep : null; + if (keep !== null && keep !== void 0 && keep.length > 0 && attrValues) { + for (let i = 0; i < keep.length; i++) { + const name = keep[i]; + if (attrValues[name] !== void 0) { + this._keptAttrs.push({ depth, name, value: attrValues[name] }); + } + } + } + } + /** + * Pop the last tag from the path. + * @returns {Object|undefined} The popped node + */ + pop() { + if (this.path.length === 0) return void 0; + this._pathStringCache = null; + const node = this.path.pop(); + if (this.siblingStacks.length > this.path.length + 1) { + this.siblingStacks.length = this.path.length + 1; + } + const poppedDepth = this.path.length + 1; + while (this._keptAttrs.length > 0 && this._keptAttrs[this._keptAttrs.length - 1].depth >= poppedDepth) { + this._keptAttrs.pop(); + } + return node; + } + /** + * Update current node's attribute values. + * Useful when attributes are parsed after push. + * @param {Object} attrValues + */ + updateCurrent(attrValues) { + if (this.path.length > 0) { + const current = this.path[this.path.length - 1]; + if (attrValues !== null && attrValues !== void 0) { + current.values = attrValues; + } + } + } + /** + * Get current tag name. + * @returns {string|undefined} + */ + getCurrentTag() { + return this.path.length > 0 ? this.path[this.path.length - 1].tag : void 0; + } + /** + * Get current namespace. + * @returns {string|undefined} + */ + getCurrentNamespace() { + return this.path.length > 0 ? this.path[this.path.length - 1].namespace : void 0; + } + /** + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} + */ + getAttrValue(attrName) { + if (this.path.length === 0) return void 0; + return this.path[this.path.length - 1].values?.[attrName]; + } + /** + * Check if current node has an attribute. + * @param {string} attrName + * @returns {boolean} + */ + hasAttr(attrName) { + if (this.path.length === 0) return false; + const current = this.path[this.path.length - 1]; + return current.values !== void 0 && attrName in current.values; + } + /** + * Get the value of a "kept" attribute from the nearest ancestor (or + * current node) that declared it via `push(tag, attrs, ns, { keep: [...] })`. + * Unlike getAttrValue(), this works regardless of how deep the path has + * gone since the attribute was pushed — but only for attribute names that + * were explicitly marked with `keep` at push time. Cost is proportional to + * the number of currently-kept attributes (typically 0-3), not path depth. + * @param {string} attrName + * @returns {*} the value, or undefined if no ancestor kept this attribute + */ + getAnyParentAttr(attrName) { + const kept = this._keptAttrs; + for (let i = kept.length - 1; i >= 0; i--) { + if (kept[i].name === attrName) return kept[i].value; + } + return void 0; + } + /** + * Check whether any ancestor (or the current node) kept the given + * attribute via `push(tag, attrs, ns, { keep: [...] })`. + * @param {string} attrName + * @returns {boolean} + */ + hasAnyParentAttr(attrName) { + const kept = this._keptAttrs; + for (let i = kept.length - 1; i >= 0; i--) { + if (kept[i].name === attrName) return true; + } + return false; + } + /** + * Get current node's sibling position (child index in parent). + * @returns {number} + */ + getPosition() { + if (this.path.length === 0) return -1; + return this.path[this.path.length - 1].position ?? 0; + } + /** + * Get current node's repeat counter (occurrence count of this tag name). + * @returns {number} + */ + getCounter() { + if (this.path.length === 0) return -1; + return this.path[this.path.length - 1].counter ?? 0; + } + /** + * Get current node's sibling index (alias for getPosition). + * @returns {number} + * @deprecated Use getPosition() or getCounter() instead + */ + getIndex() { + return this.getPosition(); + } + /** + * Get current path depth. + * @returns {number} + */ + getDepth() { + return this.path.length; + } + /** + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] + * @returns {string} + */ + toString(separator, includeNamespace = true) { + const sep8 = separator || this.separator; + const isDefault = sep8 === this.separator && includeNamespace === true; + if (isDefault) { + if (this._pathStringCache !== null) { + return this._pathStringCache; + } + const result = this.path.map( + (n) => n.namespace ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep8); + this._pathStringCache = result; + return result; + } + return this.path.map( + (n) => includeNamespace && n.namespace ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep8); + } + /** + * Get path as array of tag names. + * @returns {string[]} + */ + toArray() { + return this.path.map((n) => n.tag); + } + /** + * Reset the path to empty. + */ + reset() { + this._pathStringCache = null; + this.path = []; + this.siblingStacks = []; + this._keptAttrs = []; + } + /** + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} + */ + matches(expression) { + const segments = expression.segments; + if (segments.length === 0) { + return false; + } + if (expression.hasDeepWildcard()) { + return this._matchWithDeepWildcard(segments); + } + return this._matchSimple(segments); + } + /** + * @private + */ + _matchSimple(segments) { + if (this.path.length !== segments.length) { + return false; + } + for (let i = 0; i < segments.length; i++) { + if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) { + return false; + } + } + return true; + } + /** + * @private + */ + _matchWithDeepWildcard(segments) { + let pathIdx = this.path.length - 1; + let segIdx = segments.length - 1; + while (segIdx >= 0 && pathIdx >= 0) { + const segment = segments[segIdx]; + if (segment.type === "deep-wildcard") { + segIdx--; + if (segIdx < 0) { + return true; + } + const nextSeg = segments[segIdx]; + let found = false; + for (let i = pathIdx; i >= 0; i--) { + if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) { + pathIdx = i - 1; + segIdx--; + found = true; + break; + } + } + if (!found) { + return false; + } + } else { + if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) { + return false; + } + pathIdx--; + segIdx--; + } + } + return segIdx < 0; + } + /** + * @private + */ + _matchSegment(segment, node, isCurrentNode) { + if (segment.tag !== "*" && segment.tag !== node.tag) { + return false; + } + if (segment.namespace !== void 0) { + if (segment.namespace !== "*" && segment.namespace !== node.namespace) { + return false; + } + } + if (segment.attrName !== void 0) { + if (!isCurrentNode) { + return false; + } + if (!node.values || !(segment.attrName in node.values)) { + return false; + } + if (segment.attrValue !== void 0) { + if (String(node.values[segment.attrName]) !== String(segment.attrValue)) { + return false; + } + } + } + if (segment.position !== void 0) { + if (!isCurrentNode) { + return false; + } + const counter = node.counter ?? 0; + if (segment.position === "first" && counter !== 0) { + return false; + } else if (segment.position === "odd" && counter % 2 !== 1) { + return false; + } else if (segment.position === "even" && counter % 2 !== 0) { + return false; + } else if (segment.position === "nth" && counter !== segment.positionValue) { + return false; + } + } + return true; + } + /** + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this); + } + /** + * Create a snapshot of current state. + * @returns {Object} + */ + snapshot() { + return { + path: this.path.map((node) => ({ ...node })), + siblingStacks: this.siblingStacks.map((map) => new Map(map)), + keptAttrs: this._keptAttrs.map((entry) => ({ ...entry })) + }; + } + /** + * Restore state from snapshot. + * @param {Object} snapshot + */ + restore(snapshot2) { + this._pathStringCache = null; + this.path = snapshot2.path.map((node) => ({ ...node })); + this.siblingStacks = snapshot2.siblingStacks.map((map) => new Map(map)); + this._keptAttrs = (snapshot2.keptAttrs || []).map((entry) => ({ ...entry })); + } + /** + * Return the read-only {@link MatcherView} for this matcher. + * + * The same instance is returned on every call — no allocation occurs. + * It always reflects the current parser state and is safe to pass to + * user callbacks without risk of accidental mutation. + * + * @returns {MatcherView} + * + * @example + * const view = matcher.readOnly(); + * // pass view to callbacks — it stays in sync automatically + * view.matches(expr); // ✓ + * view.getCurrentTag(); // ✓ + * // view.push(...) // ✗ method does not exist — caught by TypeScript + */ + readOnly() { + return this._view; + } +}; + +// node_modules/is-unsafe/src/contexts/html.js +var HTML_PATTERNS = [ + { + id: "html-script-open", + description: "]/i + }, + { + id: "html-javascript-protocol", + description: "javascript: URI scheme (with optional whitespace/encoding)", + // Handles javascript:, j\u0061vascript:, and whitespace variants + pattern: /j[\t\n\r ]*a[\t\n\r ]*v[\t\n\r ]*a[\t\n\r ]*s[\t\n\r ]*c[\t\n\r ]*r[\t\n\r ]*i[\t\n\r ]*p[\t\n\r ]*t[\t\n\r ]*:/i + }, + { + id: "html-vbscript-protocol", + description: "vbscript: URI scheme", + pattern: /vbscript[\t\n\r ]*:/i + }, + { + id: "html-data-html", + description: "data:text/html URI \u2014 can execute scripts in browsers", + pattern: /data[\t\n\r ]*:[\t\n\r ]*text\/html/i + }, + { + id: "html-data-xhtml", + description: "data:application/xhtml+xml URI", + pattern: /data[\t\n\r ]*:[\t\n\r ]*application\/xhtml/i + }, + { + id: "html-data-svg", + description: "data:image/svg+xml URI \u2014 can execute scripts", + pattern: /data[\t\n\r ]*:[\t\n\r ]*image\/svg\+xml/i + }, + { + id: "html-inline-event-handler", + description: "Inline event handler attributes: onclick=, onerror=, onload=, etc.", + // \bon ensures we match a word boundary so "phonetic=" is not caught + pattern: /\bon\w{1,30}\s*=/i + }, + { + id: "html-entity-obfuscated-script", + description: "HTML-entity-encoded ]/i + }, + { + id: "html-javascript-protocol", + description: "javascript: URI scheme (with optional whitespace/encoding)", + // Handles javascript:, j\u0061vascript:, and whitespace variants + pattern: /j[\t\n\r ]*a[\t\n\r ]*v[\t\n\r ]*a[\t\n\r ]*s[\t\n\r ]*c[\t\n\r ]*r[\t\n\r ]*i[\t\n\r ]*p[\t\n\r ]*t[\t\n\r ]*:/i + }, + { + id: "html-vbscript-protocol", + description: "vbscript: URI scheme", + pattern: /vbscript[\t\n\r ]*:/i + }, + { + id: "html-data-html", + description: "data:text/html URI \u2014 can execute scripts in browsers", + pattern: /data[\t\n\r ]*:[\t\n\r ]*text\/html/i + }, + { + id: "html-data-xhtml", + description: "data:application/xhtml+xml URI", + pattern: /data[\t\n\r ]*:[\t\n\r ]*application\/xhtml/i + }, + { + id: "html-data-svg", + description: "data:image/svg+xml URI \u2014 can execute scripts", + pattern: /data[\t\n\r ]*:[\t\n\r ]*image\/svg\+xml/i + }, + { + id: "html-inline-event-handler", + description: "Inline event handler attributes: onclick=, onerror=, onload=, etc.", + // \bon ensures we match a word boundary so "phonetic=" is not caught + pattern: /\bon\w{1,30}\s*=/i + }, + { + id: "html-entity-obfuscated-script", + description: "HTML-entity-encoded