From 743fe92b0d512a10cd78657958e2367b822fe7ec Mon Sep 17 00:00:00 2001 From: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:56:12 -0500 Subject: [PATCH 1/2] chore: upgrade checkout/setup-go refs to v7 --- .github/workflows/microsoft-validation.yml | 8 +- .../workflows/publish-immutable-actions.yml | 2 +- .github/workflows/versions.yml | 24 ++-- .github/workflows/windows-validation.yml | 6 +- README.md | 6 +- docs/advanced-usage.md | 106 +++++++++--------- 6 files changed, 76 insertions(+), 76 deletions(-) diff --git a/.github/workflows/microsoft-validation.yml b/.github/workflows/microsoft-validation.yml index 76eb9ec3f..3ae013a59 100644 --- a/.github/workflows/microsoft-validation.yml +++ b/.github/workflows/microsoft-validation.yml @@ -20,7 +20,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] go-version: ['1.25', '1.24'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Microsoft build of Go ${{ matrix.go-version }} uses: ./ @@ -59,7 +59,7 @@ jobs: env: GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Microsoft build of Go via environment variable uses: ./ @@ -95,7 +95,7 @@ jobs: - os: macos-latest architecture: arm64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Microsoft build of Go with architecture uses: ./ @@ -116,7 +116,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Microsoft build of Go with caching uses: ./ diff --git a/.github/workflows/publish-immutable-actions.yml b/.github/workflows/publish-immutable-actions.yml index 7f49c641c..e613686a4 100644 --- a/.github/workflows/publish-immutable-actions.yml +++ b/.github/workflows/publish-immutable-actions.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checking out - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Publish id: publish uses: actions/publish-immutable-action@v0.0.4 diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml index dbad277c1..0f2fb3a88 100644 --- a/.github/workflows/versions.yml +++ b/.github/workflows/versions.yml @@ -20,7 +20,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go Stable uses: ./ with: @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go oldStable uses: ./ with: @@ -57,7 +57,7 @@ jobs: - os: macos-latest-large architecture: x32 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go ${{ matrix.version }} ${{ matrix.architecture }} uses: ./ with: @@ -82,7 +82,7 @@ jobs: go: 1.23.2 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: setup-go ${{ matrix.go }} uses: ./ @@ -101,7 +101,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] go-version: ['1.20', '1.21', '1.22', '1.23'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go and check latest uses: ./ with: @@ -117,7 +117,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go and check latest uses: ./ with: @@ -133,7 +133,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go and check latest uses: ./ with: @@ -149,7 +149,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go and check latest uses: ./ with: @@ -165,7 +165,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-latest-large] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go from .go-version file uses: ./ with: @@ -183,7 +183,7 @@ jobs: go: [1.20.14, 1.21.10, 1.22.8, 1.23.2] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: setup-go ${{ matrix.go }} uses: ./ @@ -203,7 +203,7 @@ jobs: go: [1.11.12] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: setup-go ${{ matrix.go }} uses: ./ @@ -231,7 +231,7 @@ jobs: - os: macos-latest-large architecture: x64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go and check latest uses: ./ with: diff --git a/.github/workflows/windows-validation.yml b/.github/workflows/windows-validation.yml index 93830772a..7240a9e1a 100644 --- a/.github/workflows/windows-validation.yml +++ b/.github/workflows/windows-validation.yml @@ -19,7 +19,7 @@ jobs: cache: [false, true] go: [1.20.1] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Setup ${{ matrix.cache }}, cache: ${{ matrix.go }}' uses: ./ @@ -88,7 +88,7 @@ jobs: matrix: cache: [false, true] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Setup default go, cache: ${{ matrix.cache }}' uses: ./ @@ -121,7 +121,7 @@ jobs: cache: [false] go: [1.20.1] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Setup ${{ matrix.go }}, cache: ${{ matrix.cache }}' uses: ./ diff --git a/README.md b/README.md index 180330906..ecee210e2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ See [action.yml](action.yml). ```yaml -- uses: actions/setup-go@v6 +- uses: actions/setup-go@v7 with: # Version or version range of Go to use go-version: '1.23' @@ -70,8 +70,8 @@ See [action.yml](action.yml). ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' # The Go version to download (if necessary) and use. - run: go run hello.go diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 91e8ffa9d..e0932f38a 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -23,8 +23,8 @@ For repeatable builds, specify the **exact major, minor, and patch version** (su ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25.5' - run: go run hello.go @@ -36,8 +36,8 @@ You can specify **only a major and minor version** if you are okay with the most ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' - run: go run hello.go @@ -53,8 +53,8 @@ Download and use beta or RC Go versions as needed. ```yaml # RC version steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25.0-rc.2' - run: go version @@ -63,8 +63,8 @@ steps: ```yaml # Beta version steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.19.0-beta.1' - run: go version @@ -80,8 +80,8 @@ If `oldstable` is provided, the action resolves it to the latest patch release o ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: 'stable' - run: go run hello.go @@ -89,8 +89,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: 'oldstable' - run: go run hello.go @@ -100,8 +100,8 @@ You can also use **SemVer's version range syntax**, for instance: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '^1.25.1' - run: go version @@ -109,8 +109,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '>=1.24.0-rc.1' - run: go version @@ -129,9 +129,9 @@ jobs: go: [ '1.24', '1.25' ] name: Go ${{ matrix.go }} sample steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup go - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go }} - run: go run hello.go @@ -154,9 +154,9 @@ jobs: - os: windows-latest go: '1.25' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup go - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go }} - run: go run hello.go @@ -170,8 +170,8 @@ If both the `go-version` and the `go-version-file` inputs are provided then the ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: 'path/to/go.mod' # Read Go version from go.mod - run: go version @@ -187,8 +187,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: '.go-version' # Read Go version from .go-version - run: go version @@ -196,8 +196,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: '.tool-versions' # Read Go version from .tool-versions - run: go version @@ -205,8 +205,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: 'go.work' # Read Go version from go.work - run: go version @@ -228,8 +228,8 @@ want the most up-to-date Go version to always be used. It supports major (e.g., ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' check-latest: true @@ -242,8 +242,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' cache-dependency-path: subdir/go.sum @@ -256,8 +256,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' cache-dependency-path: '**/go.sum' @@ -266,8 +266,8 @@ steps: ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' cache-dependency-path: | @@ -288,8 +288,8 @@ env: steps: - run: echo "$GOOS $GOARCH" > env.txt - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' cache-dependency-path: | @@ -306,8 +306,8 @@ Besides dependencies, the action can also cache build outputs (the [`GOCACHE`](h ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' cache-dependency-path: | @@ -328,10 +328,10 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup go id: setup-go - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: '1.25.5' cache: false @@ -394,8 +394,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 id: go124 with: go-version: '^1.24' @@ -411,8 +411,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 id: go124 with: go-version: '1.24' @@ -430,8 +430,8 @@ When a custom base URL is provided, the action skips the `actions/go-versions` m ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' go-download-base-url: 'https://aka.ms/golang/release/latest' @@ -445,8 +445,8 @@ env: GO_DOWNLOAD_BASE_URL: 'https://aka.ms/golang/release/latest' steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' - run: go version @@ -462,8 +462,8 @@ If your custom download source requires authentication, the `token` input is for ```yaml steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version: '1.25' go-download-base-url: 'https://private-mirror.example.com/golang' @@ -485,7 +485,7 @@ If that fails as well the action will try to download versions directly from [go If that fails as well you can get a higher rate limit with [generating a personal access token on github.com](https://github.com/settings/tokens/new) and passing it as the `token` input to the action: ```yaml -uses: actions/setup-go@v6 +uses: actions/setup-go@v7 with: token: ${{ secrets.GH_DOTCOM_TOKEN }} go-version: '1.25' From 95cee676f111238eab23d80b9c03f512f7db474d Mon Sep 17 00:00:00 2001 From: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:58:05 -0500 Subject: [PATCH 2/2] chore: commit remaining dependency, license, and dist updates --- .licenses/npm/@nodable/entities.dep.yml | 2 +- ...dep.yml => brace-expansion-1.1.16.dep.yml} | 2 +- ....dep.yml => brace-expansion-5.0.8.dep.yml} | 2 +- .licenses/npm/fast-xml-parser.dep.yml | 2 +- .licenses/npm/is-unsafe.dep.yml | 2 +- .licenses/npm/path-expression-matcher.dep.yml | 2 +- ...aming.dep.yml => xml-naming-0.1.0.dep.yml} | 0 .licenses/npm/xml-naming-0.3.0.dep.yml | 35 + dist/cache-save/index.js | 1141 +++++++++----- dist/setup/index.js | 1359 +++++++++++------ package-lock.json | 103 +- 11 files changed, 1757 insertions(+), 893 deletions(-) rename .licenses/npm/{brace-expansion-1.1.15.dep.yml => brace-expansion-1.1.16.dep.yml} (99%) rename .licenses/npm/{brace-expansion-5.0.6.dep.yml => brace-expansion-5.0.8.dep.yml} (98%) rename .licenses/npm/{xml-naming.dep.yml => xml-naming-0.1.0.dep.yml} (100%) create mode 100644 .licenses/npm/xml-naming-0.3.0.dep.yml diff --git a/.licenses/npm/@nodable/entities.dep.yml b/.licenses/npm/@nodable/entities.dep.yml index 286588316..b02eaad68 100644 --- a/.licenses/npm/@nodable/entities.dep.yml +++ b/.licenses/npm/@nodable/entities.dep.yml @@ -1,6 +1,6 @@ --- name: "@nodable/entities" -version: 2.2.0 +version: 3.0.0 type: npm summary: Entity parser for XML, HTML, External entites with security and NCR control homepage: diff --git a/.licenses/npm/brace-expansion-1.1.15.dep.yml b/.licenses/npm/brace-expansion-1.1.16.dep.yml similarity index 99% rename from .licenses/npm/brace-expansion-1.1.15.dep.yml rename to .licenses/npm/brace-expansion-1.1.16.dep.yml index cc67fe759..0dd03769c 100644 --- a/.licenses/npm/brace-expansion-1.1.15.dep.yml +++ b/.licenses/npm/brace-expansion-1.1.16.dep.yml @@ -1,6 +1,6 @@ --- name: brace-expansion -version: 1.1.15 +version: 1.1.16 type: npm summary: Brace expansion as known from sh/bash homepage: https://github.com/juliangruber/brace-expansion diff --git a/.licenses/npm/brace-expansion-5.0.6.dep.yml b/.licenses/npm/brace-expansion-5.0.8.dep.yml similarity index 98% rename from .licenses/npm/brace-expansion-5.0.6.dep.yml rename to .licenses/npm/brace-expansion-5.0.8.dep.yml index af66077e3..f40e8707d 100644 --- a/.licenses/npm/brace-expansion-5.0.6.dep.yml +++ b/.licenses/npm/brace-expansion-5.0.8.dep.yml @@ -1,6 +1,6 @@ --- name: brace-expansion -version: 5.0.6 +version: 5.0.8 type: npm summary: Brace expansion as known from sh/bash homepage: diff --git a/.licenses/npm/fast-xml-parser.dep.yml b/.licenses/npm/fast-xml-parser.dep.yml index 8f13da7e0..07fd88dd4 100644 --- a/.licenses/npm/fast-xml-parser.dep.yml +++ b/.licenses/npm/fast-xml-parser.dep.yml @@ -1,6 +1,6 @@ --- name: fast-xml-parser -version: 5.9.3 +version: 5.10.1 type: npm summary: Validate XML, Parse XML, Build XML without C/C++ based libraries homepage: diff --git a/.licenses/npm/is-unsafe.dep.yml b/.licenses/npm/is-unsafe.dep.yml index 8b7d7e655..a0e275d48 100644 --- a/.licenses/npm/is-unsafe.dep.yml +++ b/.licenses/npm/is-unsafe.dep.yml @@ -1,6 +1,6 @@ --- name: is-unsafe -version: 1.0.1 +version: 2.0.0 type: npm summary: Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts diff --git a/.licenses/npm/path-expression-matcher.dep.yml b/.licenses/npm/path-expression-matcher.dep.yml index dcb076d56..a671e7d7c 100644 --- a/.licenses/npm/path-expression-matcher.dep.yml +++ b/.licenses/npm/path-expression-matcher.dep.yml @@ -1,6 +1,6 @@ --- name: path-expression-matcher -version: 1.6.0 +version: 1.6.2 type: npm summary: Efficient path tracking and pattern matching for XML/JSON parsers homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme diff --git a/.licenses/npm/xml-naming.dep.yml b/.licenses/npm/xml-naming-0.1.0.dep.yml similarity index 100% rename from .licenses/npm/xml-naming.dep.yml rename to .licenses/npm/xml-naming-0.1.0.dep.yml diff --git a/.licenses/npm/xml-naming-0.3.0.dep.yml b/.licenses/npm/xml-naming-0.3.0.dep.yml new file mode 100644 index 000000000..bfc32c71c --- /dev/null +++ b/.licenses/npm/xml-naming-0.3.0.dep.yml @@ -0,0 +1,35 @@ +--- +name: xml-naming +version: 0.3.0 +type: npm +summary: Validates XML name productions — Name, NCName, QName, NMToken, NMTokens — + for XML 1.0 and 1.1 +homepage: +license: mit +licenses: +- sources: LICENSE + text: | + MIT License + + Copyright (c) 2026 Natural Intelligence + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +- sources: README.md + text: MIT +notices: [] diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js index 99cb1545b..59f8a5a5a 100644 --- a/dist/cache-save/index.js +++ b/dist/cache-save/index.js @@ -178,103 +178,109 @@ function gte(i, y) { function expand(str, max, isTop) { var expansions = []; - var m = balanced('{', '}', str); - if (!m || /\$$/.test(m.pre)) return [str]; - - var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - var isSequence = isNumericSequence || isAlphaSequence; - var isOptions = m.body.indexOf(',') >= 0; - if (!isSequence && !isOptions) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; - return expand(str, max, true); + // The `{a},b}` rewrite below restarts expansion on a rewritten string with + // the same `max` and `isTop = true`. Loop instead of recursing so a long run + // of non-expanding `{}` groups can't exhaust the call stack. + for (;;) { + var m = balanced('{', '}', str); + if (!m || /\$$/.test(m.pre)) return [str]; + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + isTop = true + continue + } + return [str]; } - return [str]; - } - var n; - if (isSequence) { - n = m.body.split(/\.\./); - } else { - n = parseCommaParts(m.body); - if (n.length === 1) { - // x{{a,b}}y ==> x{a}y x{b}y - n = expand(n[0], max, false).map(embrace); + var n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); if (n.length === 1) { - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - return post.map(function(p) { - return m.pre + n[0] + p; - }); + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], max, false).map(embrace); + if (n.length === 1) { + var post = m.post.length + ? expand(m.post, max, false) + : ['']; + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } } } - } - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + 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.max(Math.abs(numeric(n[2])), 1) + : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); - // no need to expand pre, since it is guaranteed to be free of brace-sets - var pre = m.pre; - 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.max(Math.abs(numeric(n[2])), 1) - : 1; - var test = lte; - var reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - var pad = n.some(isPadded); - - N = []; - - for (var i = x; test(i, y) && N.length < max; i += incr) { - var c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') - c = ''; - } else { - c = String(i); - if (pad) { - var need = width - c.length; - if (need > 0) { - var z = new Array(need + 1).join('0'); - if (i < 0) - c = '-' + z + c.slice(1); - else - c = z + c; + N = []; + + for (var i = x; test(i, y) && N.length < max; i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } } } + N.push(c); } - N.push(c); + } else { + N = concatMap(n, function(el) { return expand(el, max, false) }); } - } else { - N = concatMap(n, function(el) { return expand(el, max, false) }); - } - for (var j = 0; j < N.length; j++) { - 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); + for (var j = 0; j < N.length; j++) { + 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); + } } - } - return expansions; + return expansions; + } } @@ -52353,26 +52359,29 @@ class Matcher { // Get or create sibling tracking for current level const currentLevel = this.path.length; - if (!this.siblingStacks[currentLevel]) { - this.siblingStacks[currentLevel] = new Map(); + let level = this.siblingStacks[currentLevel]; + if (!level) { + // `counts` tells same-name siblings apart (the "counter" — nth + // among other s). `total` is every child seen at this level so + // far, kept as a running number instead of re-added from `counts` on + // every push — a parent with many differently-named children would + // otherwise cost more per child the more distinct names it has. + level = { counts: new Map(), total: 0 }; + this.siblingStacks[currentLevel] = level; } - const siblings = this.siblingStacks[currentLevel]; - // Create a unique key for sibling tracking that includes namespace const siblingKey = namespace ? `${namespace}:${tagName}` : tagName; // Calculate counter (how many times this tag appeared at this level) - const counter = siblings.get(siblingKey) || 0; + const counter = level.counts.get(siblingKey) || 0; - // Calculate position (total children at this level so far) - let position = 0; - for (const count of siblings.values()) { - position += count; - } + // Position = total children at this level seen before this one. + const position = level.total; - // Update sibling count for this tag - siblings.set(siblingKey, counter + 1); + // Update sibling count for this tag, and the level's running total. + level.counts.set(siblingKey, counter + 1); + level.total++; // Create new node const node = { @@ -52748,7 +52757,7 @@ class Matcher { snapshot() { return { path: this.path.map(node => ({ ...node })), - siblingStacks: this.siblingStacks.map(map => new Map(map)), + siblingStacks: this.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level), keptAttrs: this._keptAttrs.map(entry => ({ ...entry })) }; } @@ -52760,7 +52769,7 @@ class Matcher { restore(snapshot) { this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); - this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); + this.siblingStacks = snapshot.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level); this._keptAttrs = (snapshot.keptAttrs || []).map(entry => ({ ...entry })); } @@ -54766,6 +54775,389 @@ class XmlNode { } } +;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/node_modules/xml-naming/src/index.js +/** + * xml-naming + * Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications. + * Covers: Name, NCName, QName, NMToken, NMTokens + * + * XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name + * XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar + * XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName + */ + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.0 +// +// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] +// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] +// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487 +// | [#x200C-#x200D] +// | [#x2070-#x218F] | [#x2C00-#x2FEF] +// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] +// +// NameChar ::= NameStartChar | "-" | "." | [0-9] +// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] +// +// Note: \u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0, +// after XML 1.0 was defined against Unicode 2.0. It falls inside the range +// \u037F-\u1FFF but must be excluded. We split that range into +// \u037F-\u0486 and \u0488-\u1FFF to exclude it explicitly. +// --------------------------------------------------------------------------- + +const src_nameStartChar10 = + ':A-Za-z_' + + '\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF' + + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD'; + +const src_nameChar10 = + src_nameStartChar10 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.1 +// +// Differences from XML 1.0: +// +// NameStartChar: +// 1.0 has split ranges: \u00C0-\u00D6, \u00D8-\u00F6, \u00F8-\u02FF +// 1.1 merges them into: \u00C0-\u02FF +// (\u00D7 x and \u00F7 / are division symbols, excluded in both versions) +// +// 1.0 tops out at \uFFFD (BMP only) +// 1.1 adds \u{10000}-\u{EFFFF} (supplementary planes) +// These require the /u flag on the RegExp — see buildRegexes below. +// +// NameChar: +// 1.1 adds \u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0) +// --------------------------------------------------------------------------- + +const src_nameStartChar11 = + ':A-Za-z_' + + '\u00C0-\u02FF' + // merged — 1.0 had three split ranges here + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 (combining mark, never a NameStartChar) + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD' + + '\u{10000}-\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp + +const src_nameChar11 = + src_nameStartChar11 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0 + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Regex builders +// +// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour. +// XML 1.1 regexes: /u flag — required for \u{10000}-\u{EFFFF} to match actual +// supplementary code points rather than lone surrogates (which are illegal XML). +// --------------------------------------------------------------------------- + +const src_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), + }; +}; + +const src_regexes10 = src_buildRegexes(src_nameStartChar10, src_nameChar10); // no /u — BMP only +const src_regexes11 = src_buildRegexes(src_nameStartChar11, src_nameChar11, 'u'); // /u — enables \u{10000}-\u{EFFFF} + +// --------------------------------------------------------------------------- +// ASCII-only fast path (opt-in, off by default) +// +// The XML 1.0 vs 1.1 NameStartChar/NameChar productions differ *only* in +// their non-ASCII ranges (merged vs split Latin-1 ranges, \u0487, and +// supplementary planes). Restricted to ASCII, both versions collapse to the +// same character classes, so a single regex pair covers both xmlVersion +// values — no /u flag needed. +// +// Rationale: unicode-aware regexes (the /u flag, required for XML 1.1's +// supplementary-plane range) are measurably slower in V8 than plain +// non-unicode regexes on the same input, even when the input is pure ASCII. +// For the common case — HTML/SVG ids, XML tags — names are ASCII, so callers +// who know this can opt in to skip the unicode-aware matching path entirely. +// This is a real but *conditional* win: mainly for XML 1.1 input (avoids /u), +// or at scale where the larger unicode character classes add engine +// overhead. It also changes behaviour (rejects legitimate non-ASCII XML +// 1.0/1.1 names), so it must never be silently enabled — hence off by +// default. +// --------------------------------------------------------------------------- + +const nameStartCharAscii = ':A-Za-z_'; +const nameCharAscii = nameStartCharAscii + '\\-\\.\\d'; + +const regexesAscii = src_buildRegexes(nameStartCharAscii, nameCharAscii); // no /u — ASCII only + +const src_getRegexes = (xmlVersion = '1.0', asciiOnly = false) => { + if (asciiOnly) return regexesAscii; + return xmlVersion === '1.1' ? src_regexes11 : src_regexes10; +}; + +// --------------------------------------------------------------------------- +// Boolean validators +// --------------------------------------------------------------------------- + +/** + * Returns true if the string is a valid XML Name. + * Colons are allowed anywhere (Name production). + * Used for: DOCTYPE entity names, notation names, DTD element declarations. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const xml_naming_src_name = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).name.test(str); + +/** + * Returns true if the string is a valid NCName (Non-Colonized Name). + * Colons are not permitted. + * Used for: namespace prefixes, local names, SVG id attributes. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_ncName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).ncName.test(str); + +/** + * Returns true if the string is a valid QName (Qualified Name). + * Allows exactly one colon as a prefix separator: prefix:localName. + * Used for: element and attribute names in namespace-aware XML/SVG. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_qName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).qName.test(str); + +/** + * Returns true if the string is a valid NMToken. + * Like Name but no restriction on the first character. + * Used for: DTD NMTOKEN attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmToken = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmToken.test(str); + +/** + * Returns true if the string is a valid NMTokens value. + * A whitespace-separated list of NMToken values. + * Used for: DTD NMTOKENS attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmTokens = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmTokens.test(str); + +// --------------------------------------------------------------------------- +// Memoized validator factory +// +// Real documents reuse a small vocabulary of tag/attribute names across many +// siblings (e.g. `id`, `class`, `href` repeated across hundreds of elements). +// The plain boolean validators above re-run the regex on every call +// regardless of repeats. `createValidator` returns a closure with a private +// string -> boolean cache, so repeated names after the first become O(1) +// lookups instead of regex tests. +// +// - opts (xmlVersion, asciiOnly) are fixed at creation time, so the regex is +// resolved once, not on every call. +// - The cache is private to the returned closure — no shared/global state, +// no cross-caller pollution. +// - `maxCacheSize` bounds memory: once the cache reaches this many entries, +// it stops accepting new ones (existing entries keep serving hits; new +// misses just fall through to the regex, uncached). This avoids unbounded +// growth against adversarial/high-cardinality input (e.g. validating +// attacker-supplied names with no repeats) without the cost/complexity of +// a full LRU, and without the perf cliff of reset-and-refill thrashing. +// - Call `.reset()` on the returned function to clear the cache manually +// (e.g. between unrelated parse calls). +// --------------------------------------------------------------------------- + +const src_PRODUCTIONS = (/* unused pure expression or super */ null && (['name', 'ncName', 'qName', 'nmToken', 'nmTokens'])); + +/** + * Returns a memoized boolean validator function for a single production, + * with opts fixed at creation time. + * + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean, maxCacheSize?: number }} [opts] + * maxCacheSize: max number of distinct strings to cache (default 2048). + * Once reached, new strings are validated but not cached; existing cached + * entries keep being served. + * @returns {((str: string) => boolean) & { reset: () => void }} + */ +const createValidator = (production, { xmlVersion = '1.0', asciiOnly = false, maxCacheSize = 2048 } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const regex = src_getRegexes(xmlVersion, asciiOnly)[production]; + let cache = new Map(); + + const validator = (str) => { + const cached = cache.get(str); + if (cached !== undefined) return cached; + + const result = regex.test(str); + if (cache.size < maxCacheSize) cache.set(str, result); + return result; + }; + + validator.reset = () => { cache = new Map(); }; + + return validator; +}; + +// --------------------------------------------------------------------------- +// Diagnostic validator +// --------------------------------------------------------------------------- + +/** + * Validates a string against a named production and returns a detailed result. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {{ valid: boolean, production: string, input: string, reason?: string, position?: number }} + */ +const src_validate = (str, production, { xmlVersion = '1.0', asciiOnly = false } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const validators = { name: xml_naming_src_name, ncName: src_ncName, qName: src_qName, nmToken: src_nmToken, nmTokens: src_nmTokens }; + const isValid = validators[production](str, { xmlVersion, asciiOnly }); + + if (isValid) return { valid: true, production, input: str }; + + let reason = 'Does not match the production rules'; + let position; + + // Diagnostic fallback char checks must mirror the same character set the + // boolean validator above used, or the reported reason/position could + // contradict the `valid: false` result (e.g. flagging a char as illegal + // that the unicode-aware check would have accepted). + const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\u00C0-\uFFFD]/; + const namePattern = asciiOnly ? /[\w\-\\.:]/ : /[\w\-\\.:\u00B7\u00C0-\uFFFD]/; + + if (str.length === 0) { + reason = 'Input is empty'; + } else if (production === 'ncName' && str.includes(':')) { + position = str.indexOf(':'); + reason = 'Colon is not allowed in NCName'; + } else if (production === 'qName' && str.startsWith(':')) { + reason = 'QName cannot start with a colon'; + position = 0; + } else if (production === 'qName' && str.endsWith(':')) { + reason = 'QName cannot end with a colon'; + position = str.length - 1; + } else if (production === 'qName' && (str.match(/:/g) || []).length > 1) { + reason = 'QName can have at most one colon'; + position = str.lastIndexOf(':'); + } else if ( + ['name', 'ncName', 'qName'].includes(production) && + !startCharPattern.test(str[0]) + ) { + reason = `First character "${str[0]}" is not a valid NameStartChar`; + position = 0; + } else { + for (let i = 0; i < str.length; i++) { + if (!namePattern.test(str[i])) { + reason = `Character "${str[i]}" at position ${i} is not a valid NameChar`; + position = i; + break; + } + } + } + + return { valid: false, production, input: str, reason, position }; +}; + +// --------------------------------------------------------------------------- +// Batch validator +// --------------------------------------------------------------------------- + +/** + * Validates an array of strings against a named production. + * + * @param {string[]} strings + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {Array<{ valid: boolean, production: string, input: string, reason?: string, position?: number }>} + */ +const src_validateAll = (strings, production, opts = {}) => + strings.map(str => src_validate(str, production, opts)); + +// --------------------------------------------------------------------------- +// Sanitizer +// --------------------------------------------------------------------------- + +/** + * Transforms an invalid string into the nearest valid XML name for the given production. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ replacement?: string, asciiOnly?: boolean }} [opts] + * asciiOnly: also replace any non-ASCII character, not just XML-illegal + * ones (default false). + * @returns {string} + */ +const src_sanitize = (str, production = 'name', { replacement = '_', asciiOnly = false } = {}) => { + if (!str) return replacement; + + let result = str; + + // Strip colons for NCName + if (production === 'ncName') { + result = result.replace(/:/g, ''); + } + + // Replace illegal characters + const allowedCharPattern = asciiOnly ? /[^\w\-\.:]/g : /[^\w\-\.:\u00B7\u00C0-\uFFFD]/g; + result = result.replace(allowedCharPattern, replacement); + + // Fix invalid start character for Name / NCName / QName + if (production !== 'nmToken' && production !== 'nmTokens') { + if (/^[\-\.\d]/.test(result)) { + result = replacement + result; + } + } + + return result || replacement; +}; ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js @@ -54999,7 +55391,7 @@ class DocTypeReader { let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { + if (!this.suppressValidationErr && !src_qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -55173,7 +55565,7 @@ function hasSeq(data, seq, i) { } function validateEntityName(name, xmlVersion) { - if (qName(name, { xmlVersion: xmlVersion })) + if (src_qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); @@ -56957,26 +57349,6 @@ const MISC_SYMBOLS = { nVDash: '⊯', }; -/** - * All entities combined (if you need everything) - * @type {Record} - */ -const ALL_ENTITIES = { - ...BASIC_LATIN, - ...LATIN_ACCENTS, - ...LATIN_EXTENDED, - ...GREEK, - ...CYRILLIC, - ...MATH, - ...MATH_ADVANCED, - ...ARROWS, - ...SHAPES, - ...PUNCTUATION, - ...CURRENCY, - ...FRACTIONS, - ...MISC_SYMBOLS, -}; - const XML = { amp: "&", apos: "'", @@ -57655,6 +58027,152 @@ class EntityDecoder { return this._applyNCRAction(effective, token, cp); } } +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js +/** + * SQL context patterns — high-precision rules only. + * + * These rules have very low false-positive risk and are safe to apply to + * general user text (names, descriptions, search queries, etc.). + * All patterns are ReDoS-safe — unlike the `sql-injection` npm package + * which has an active CVE on its own detection regexes. + * + * For exhaustive coverage including noisier heuristics (comment sequences, + * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. + * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, + * not to general free-text fields. + */ + +const SQL_PATTERNS = [ + { + id: 'sql-block-comment-open', + description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', + pattern: /\/\*/, + }, + { + id: 'sql-union-select', + description: 'UNION SELECT — most common SQL injection aggregation attack', + pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, + }, + { + id: 'sql-drop-table', + description: 'DROP TABLE — destructive DDL injection', + pattern: /\bDROP\s{1,20}TABLE\b/i, + }, + { + id: 'sql-drop-database', + description: 'DROP DATABASE — destructive DDL injection', + pattern: /\bDROP\s{1,20}DATABASE\b/i, + }, + { + id: 'sql-insert-into', + description: 'INSERT INTO — data injection', + pattern: /\bINSERT\s{1,20}INTO\b/i, + }, + { + id: 'sql-delete-from', + description: 'DELETE FROM — data deletion injection', + pattern: /\bDELETE\s{1,20}FROM\b/i, + }, + { + id: 'sql-update-set', + description: 'UPDATE ... SET — data modification injection', + // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) + pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, + }, + { + id: 'sql-exec-xp', + description: 'EXEC xp_ — MSSQL extended stored procedure execution', + pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, + }, + { + id: 'sql-tautology-string', + description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", + // Last quote is optional — injection may truncate it: ' OR '1'='1-- + pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, + }, + { + id: 'sql-tautology-numeric', + description: 'Numeric tautology: OR 1=1', + pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, + }, + { + id: 'sql-always-true-zero', + description: 'Numeric tautology: OR 0=0', + pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, + }, + { + id: 'sql-sleep-benchmark', + description: 'Time-based blind injection: SLEEP() or BENCHMARK()', + pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, + }, + { + id: 'sql-waitfor-delay', + description: 'MSSQL time-based blind injection: WAITFOR DELAY', + pattern: /\bWAITFOR\s{1,20}DELAY\b/i, + }, + { + id: 'sql-char-function', + description: 'CHAR() function — used to obfuscate injected strings', + pattern: /\bCHAR\s*\(\s*\d{1,3}/i, + }, + { + id: 'sql-information-schema', + description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', + pattern: /\bINFORMATION_SCHEMA\b/i, + }, +]; + +/* harmony default export */ const sql = (SQL_PATTERNS); + +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js +/** + * SQL-STRICT context patterns. + * + * Extends the base 'SQL' context with three additional rules that are + * effective at detecting real injections but carry a higher false-positive + * risk on general free-text input. + * + * Use 'SQL-STRICT' when: + * - The string is specifically a SQL fragment or database identifier + * - You control the input domain (e.g. a dedicated SQL search field) + * - You can tolerate occasional false positives in exchange for broader coverage + * + * Use 'SQL' (not STRICT) when: + * - The field is general user text (names, descriptions, comments) + * - False positives would block legitimate content (e.g. "see note -- above") + * + * Rules moved here from 'SQL' due to false-positive risk: + * + * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) + * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words + * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output + */ + + + +const SQL_STRICT_EXTRA = [ + { + id: 'sql-line-comment', + description: 'SQL line comment: -- followed by whitespace or end of string', + pattern: /--(?:\s|$)/, + }, + { + id: 'sql-stacked-query', + description: 'Stacked queries: semicolon immediately followed by a SQL keyword', + pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, + }, + { + id: 'sql-hex-encoding', + description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', + pattern: /\b0x[0-9a-f]{4,}/i, + }, +]; + +// SQL-STRICT = all base SQL rules + the three noisy extras +const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; + +/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); + ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/html.js /** * HTML context patterns. @@ -57922,152 +58440,6 @@ const SVG_PATTERNS = [ /* harmony default export */ const svg = (SVG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js -/** - * SQL context patterns — high-precision rules only. - * - * These rules have very low false-positive risk and are safe to apply to - * general user text (names, descriptions, search queries, etc.). - * All patterns are ReDoS-safe — unlike the `sql-injection` npm package - * which has an active CVE on its own detection regexes. - * - * For exhaustive coverage including noisier heuristics (comment sequences, - * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. - * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, - * not to general free-text fields. - */ - -const SQL_PATTERNS = [ - { - id: 'sql-block-comment-open', - description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', - pattern: /\/\*/, - }, - { - id: 'sql-union-select', - description: 'UNION SELECT — most common SQL injection aggregation attack', - pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, - }, - { - id: 'sql-drop-table', - description: 'DROP TABLE — destructive DDL injection', - pattern: /\bDROP\s{1,20}TABLE\b/i, - }, - { - id: 'sql-drop-database', - description: 'DROP DATABASE — destructive DDL injection', - pattern: /\bDROP\s{1,20}DATABASE\b/i, - }, - { - id: 'sql-insert-into', - description: 'INSERT INTO — data injection', - pattern: /\bINSERT\s{1,20}INTO\b/i, - }, - { - id: 'sql-delete-from', - description: 'DELETE FROM — data deletion injection', - pattern: /\bDELETE\s{1,20}FROM\b/i, - }, - { - id: 'sql-update-set', - description: 'UPDATE ... SET — data modification injection', - // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) - pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, - }, - { - id: 'sql-exec-xp', - description: 'EXEC xp_ — MSSQL extended stored procedure execution', - pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, - }, - { - id: 'sql-tautology-string', - description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", - // Last quote is optional — injection may truncate it: ' OR '1'='1-- - pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, - }, - { - id: 'sql-tautology-numeric', - description: 'Numeric tautology: OR 1=1', - pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, - }, - { - id: 'sql-always-true-zero', - description: 'Numeric tautology: OR 0=0', - pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, - }, - { - id: 'sql-sleep-benchmark', - description: 'Time-based blind injection: SLEEP() or BENCHMARK()', - pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, - }, - { - id: 'sql-waitfor-delay', - description: 'MSSQL time-based blind injection: WAITFOR DELAY', - pattern: /\bWAITFOR\s{1,20}DELAY\b/i, - }, - { - id: 'sql-char-function', - description: 'CHAR() function — used to obfuscate injected strings', - pattern: /\bCHAR\s*\(\s*\d{1,3}/i, - }, - { - id: 'sql-information-schema', - description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', - pattern: /\bINFORMATION_SCHEMA\b/i, - }, -]; - -/* harmony default export */ const sql = (SQL_PATTERNS); - -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js -/** - * SQL-STRICT context patterns. - * - * Extends the base 'SQL' context with three additional rules that are - * effective at detecting real injections but carry a higher false-positive - * risk on general free-text input. - * - * Use 'SQL-STRICT' when: - * - The string is specifically a SQL fragment or database identifier - * - You control the input domain (e.g. a dedicated SQL search field) - * - You can tolerate occasional false positives in exchange for broader coverage - * - * Use 'SQL' (not STRICT) when: - * - The field is general user text (names, descriptions, comments) - * - False positives would block legitimate content (e.g. "see note -- above") - * - * Rules moved here from 'SQL' due to false-positive risk: - * - * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) - * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words - * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output - */ - - - -const SQL_STRICT_EXTRA = [ - { - id: 'sql-line-comment', - description: 'SQL line comment: -- followed by whitespace or end of string', - pattern: /--(?:\s|$)/, - }, - { - id: 'sql-stacked-query', - description: 'Stacked queries: semicolon immediately followed by a SQL keyword', - pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, - }, - { - id: 'sql-hex-encoding', - description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', - pattern: /\b0x[0-9a-f]{4,}/i, - }, -]; - -// SQL-STRICT = all base SQL rules + the three noisy extras -const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; - -/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); - ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/shell.js /** * SHELL context patterns. @@ -58453,32 +58825,66 @@ const LOG_PATTERNS = [ /* harmony default export */ const contexts_log = (LOG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/registry.js +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js /** - * Context registry — maps context name strings to their pattern arrays. + * is-unsafe v2 + * + * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings + * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. * - * Adding a new context: create a file in ./contexts/, export a default array - * of pattern objects, and register it here. + * v2 change: contexts are imported as named pattern arrays rather than resolved + * via a string-keyed registry. This makes each context independently + * tree-shakeable — bundlers can drop any context you never import. * - * Context name guide: - * SQL — high-precision rules; safe for general text fields - * SQL-STRICT — SQL + three noisier rules (line comments, stacked queries, hex); - * use only for SQL-specific inputs - * REDOS — detects ReDoS-prone patterns when string will be compiled as RegExp + * @module is-unsafe */ +// ─── Context pattern arrays (named exports) ──────────────────────────────── +// Import only the ones you need. Each is independently tree-shakeable. + + + + + + + + + +// SQL-STRICT needs a quoted identifier because of the hyphen +// ─── VALID_CONTEXTS convenience re-export ───────────────────────────────── +// Importing this pulls in ALL contexts. Use it only when you need all of them +// (e.g. for validation UI, tooling, or exhaustive audits). +// If you only need a subset, import the named contexts directly instead. -/** @type {Record>} */ -const registry_CONTEXT_REGISTRY = { + + + + +// ─── Attach labels to named contexts ────────────────────────────────────── +// Each built-in PatternList carries its canonical name so matchList can read +// list.label directly — no registry lookup needed at match time. +// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label. + +html.label = 'HTML'; +xml.label = 'XML'; +svg.label = 'SVG'; +sql.label = 'SQL'; +sql_strict.label = 'SQL-STRICT'; +shell.label = 'SHELL'; +redos.label = 'REDOS'; +nosql.label = 'NOSQL'; +contexts_log.label = 'LOG'; + +const VALID_CONTEXTS = Object.freeze({ HTML: html, XML: xml, SVG: svg, @@ -58488,45 +58894,29 @@ const registry_CONTEXT_REGISTRY = { REDOS: redos, NOSQL: nosql, LOG: contexts_log, -}; +}); -/* harmony default export */ const registry = (registry_CONTEXT_REGISTRY); +// ─── Types ──────────────────────────────────────────────────────────────── /** - * Enum of valid context names — e.g. `VALID_CONTEXTS.HTML === 'HTML'`. - * @type {Record} - */ -const VALID_CONTEXTS = Object.freeze( - Object.fromEntries(Object.keys(registry_CONTEXT_REGISTRY).map((k) => [k, k])) -); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js -/** - * is-unsafe - * - * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings - * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. - * - * @module is-unsafe + * @typedef {{ id: string, description: string, pattern: RegExp }} Rule */ - - /** - * @typedef {'HTML'|'XML'|'SVG'|'SQL'|'SQL-STRICT'|'SHELL'|'REDOS'|'NOSQL'|'LOG'} ContextName + * @typedef {Rule[]} PatternList */ /** * @typedef {Object} MatchResult - * @property {string} context - The context in which the match was found - * @property {string} id - Rule identifier + * @property {string} context - Label identifying which context matched ('HTML', 'CUSTOM', etc.) + * @property {string} id - Rule identifier * @property {string} description - Human-readable description of what was matched - * @property {RegExp} pattern - The pattern that matched + * @property {RegExp} pattern - The pattern that matched */ -// ─── Validation helpers ──────────────────────────────────────────────────── +// ─── Internal helpers ────────────────────────────────────────────────────── /** - * Validate that `value` is a string. Throws TypeError if not. * @param {unknown} value */ function assertString(value) { @@ -58538,56 +58928,61 @@ function assertString(value) { } /** - * Validate that `context` is a recognised context name, an array of them, - * or a RegExp instance. Throws TypeError if not. - * @param {ContextName|ContextName[]|RegExp} context + * @param {unknown} context */ function assertContext(context) { if (context instanceof RegExp) return; - if (typeof context === 'string') { - if (!registry[context]) { - throw new TypeError( - `is-unsafe: unknown context "${context}". Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); - } - return; - } - if (Array.isArray(context)) { if (context.length === 0) { - throw new TypeError('is-unsafe: context array must not be empty'); + throw new TypeError('is-unsafe: context must not be an empty array'); } - for (const c of context) { - if (typeof c !== 'string' || !registry[c]) { - throw new TypeError( - `is-unsafe: unknown context "${c}" in array. Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); + // Detect array-of-arrays vs flat pattern list + if (Array.isArray(context[0])) { + // Array of PatternLists + for (const list of context) { + if (!Array.isArray(list) || list.length === 0) { + throw new TypeError( + 'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)' + ); + } } } + // else: flat PatternList — trust it, no deep validation needed return; } throw new TypeError( - `is-unsafe: second argument must be a context string, array of context strings, or RegExp. Got: ${typeof context}` + `is-unsafe: second argument must be a PatternList (e.g. HTML), ` + + `an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}` ); } -// ─── Core matching logic ─────────────────────────────────────────────────── +/** + * Normalise any valid context arg into an array of PatternLists. + * + * @param {Rule[]|Rule[][]|RegExp} context + * @returns {{ lists: Rule[][]|null, regex: RegExp|null }} + */ +function normalise(context) { + if (context instanceof RegExp) return { lists: null, regex: context }; + // Distinguish PatternList (array of rule objects) from array of PatternLists + if (Array.isArray(context[0])) return { lists: context, regex: null }; + return { lists: [context], regex: null }; +} /** - * Test a single value against one named context's patterns. - * Returns the first matching MatchResult, or null if nothing matched. + * Test value against a single PatternList. Returns the first MatchResult or null. * * @param {string} value - * @param {string} contextName + * @param {Rule[]} list * @returns {MatchResult|null} */ -function matchContext(value, contextName) { - const patterns = registry[contextName]; - for (const rule of patterns) { +function matchList(value, list) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - return { context: contextName, id: rule.id, description: rule.description, pattern: rule.pattern }; + return { context: label, id: rule.id, description: rule.description, pattern: rule.pattern }; } } return null; @@ -58598,103 +58993,95 @@ function matchContext(value, contextName) { /** * Returns `true` if `value` is unsafe in the given context(s), `false` otherwise. * - * @param {string} value - The string to test - * @param {ContextName|ContextName[]|RegExp} context - * - A named context ('HTML', 'XML', 'SVG', 'SQL', 'SQL-STRICT', 'SHELL', 'REDOS', 'NOSQL', 'LOG') - * - An array of named contexts — returns true if unsafe in **any** of them + * @param {string} value - The string to test + * @param {PatternList | PatternList[] | RegExp} context + * - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`) + * - An array of PatternLists — returns true if unsafe in **any** of them * - A custom RegExp — returns true if the pattern matches * @returns {boolean} * * @example - * isUnsafe('', 'HTML') // true - * isUnsafe('hello world', 'HTML') // false - * isUnsafe('value', ['HTML', 'SQL']) // false - * isUnsafe('value', /my-pattern/i) // false + * import { isUnsafe, HTML, SQL } from 'is-unsafe'; + * + * isUnsafe('', HTML) // true + * isUnsafe('hello world', HTML) // false + * isUnsafe('value', [HTML, SQL]) // false + * isUnsafe('value', /my-pattern/i) // false */ function isUnsafe(value, context) { assertString(value); assertContext(context); - // Custom RegExp — caller-supplied pattern - if (context instanceof RegExp) { - return context.test(value); - } + const { lists, regex } = normalise(context); - // Single named context - if (typeof context === 'string') { - return matchContext(value, context) !== null; - } + if (regex) return regex.test(value); - // Array of named contexts — unsafe if ANY context matches - for (const c of context) { - if (matchContext(value, c) !== null) return true; + for (const list of lists) { + if (matchList(value, list) !== null) return true; } return false; } /** - * Like `isUnsafe`, but instead of a boolean returns the first `MatchResult` - * describing **why** the value was flagged, or `null` if it is safe. - * - * Useful for logging, error messages, or policy reporting. + * Like `isUnsafe`, but returns the first `MatchResult` describing **why** + * the value was flagged, or `null` if it is safe. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult|null} * * @example - * whyUnsafe('', 'HTML') + * import { whyUnsafe, HTML } from 'is-unsafe'; + * + * whyUnsafe('', HTML) * // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ } */ function whyUnsafe(value, context) { assertString(value); assertContext(context); - if (context instanceof RegExp) { - return context.test(value) - ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context } - : null; - } + const { lists, regex } = normalise(context); - if (typeof context === 'string') { - return matchContext(value, context); + if (regex) { + return regex.test(value) + ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex } + : null; } - for (const c of context) { - const result = matchContext(value, c); + for (const list of lists) { + const result = matchList(value, list); if (result !== null) return result; } return null; } /** - * Returns all matching rules across the given context(s), or an empty array - * if the value is safe. Useful for comprehensive auditing. + * Returns **all** matching rules across the given context(s), or an empty + * array if the value is safe. Useful for comprehensive auditing. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult[]} */ function allUnsafe(value, context) { assertString(value); assertContext(context); + const { lists, regex } = normalise(context); const results = []; - if (context instanceof RegExp) { - if (context.test(value)) { - results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context }); + if (regex) { + if (regex.test(value)) { + results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }); } return results; } - const contexts = typeof context === 'string' ? [context] : context; - - for (const c of contexts) { - const patterns = CONTEXT_REGISTRY[c]; - for (const rule of patterns) { + for (const list of lists) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - results.push({ context: c, id: rule.id, description: rule.description, pattern: rule.pattern }); + results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern }); } } } @@ -58704,6 +59091,7 @@ function allUnsafe(value, context) { /* harmony default export */ const src = ((/* unused pure expression or super */ null && (isUnsafe))); + ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -58794,6 +59182,7 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; let namedEntities = { ...XML }; if (this.options.entityDecoder) { this.entityDecoder = this.options.entityDecoder @@ -58811,8 +59200,7 @@ class OrderedObjParser { // onExternalEntity: (name, value) => isUnsafe(value) ? 'block' : 'allow', onInputEntity: (name, value) => //TODO: VALID_CONTEXTS.HTML should be set only if this.options.htmlEntities - isUnsafe(value, [VALID_CONTEXTS.HTML, VALID_CONTEXTS.XML]) - ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, + isUnsafe(value, [html, xml]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, //postCheck: resolved => resolved }); @@ -59002,6 +59390,7 @@ const parseXml = function (xmlData) { // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; const options = this.options; const docTypeReader = new DocTypeReader(options.processEntities); const xmlLen = xmlData.length; @@ -59086,6 +59475,8 @@ const parseXml = function (xmlData) { i = endIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 68) { //'!D' + if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found."); + this.doctypefound = true; const result = docTypeReader.readDocType(xmlData, i); this.entityDecoder.addInputEntities(result.entities); i = result.i; diff --git a/dist/setup/index.js b/dist/setup/index.js index 187253e6f..07ab08e52 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -178,103 +178,109 @@ function gte(i, y) { function expand(str, max, isTop) { var expansions = []; - var m = balanced('{', '}', str); - if (!m || /\$$/.test(m.pre)) return [str]; - - var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - var isSequence = isNumericSequence || isAlphaSequence; - var isOptions = m.body.indexOf(',') >= 0; - if (!isSequence && !isOptions) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; - return expand(str, max, true); + // The `{a},b}` rewrite below restarts expansion on a rewritten string with + // the same `max` and `isTop = true`. Loop instead of recursing so a long run + // of non-expanding `{}` groups can't exhaust the call stack. + for (;;) { + var m = balanced('{', '}', str); + if (!m || /\$$/.test(m.pre)) return [str]; + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + isTop = true + continue + } + return [str]; } - return [str]; - } - var n; - if (isSequence) { - n = m.body.split(/\.\./); - } else { - n = parseCommaParts(m.body); - if (n.length === 1) { - // x{{a,b}}y ==> x{a}y x{b}y - n = expand(n[0], max, false).map(embrace); + var n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); if (n.length === 1) { - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - return post.map(function(p) { - return m.pre + n[0] + p; - }); + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], max, false).map(embrace); + if (n.length === 1) { + var post = m.post.length + ? expand(m.post, max, false) + : ['']; + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } } } - } - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. - // no need to expand pre, since it is guaranteed to be free of brace-sets - var pre = m.pre; - 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.max(Math.abs(numeric(n[2])), 1) - : 1; - var test = lte; - var reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - var pad = n.some(isPadded); + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + 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.max(Math.abs(numeric(n[2])), 1) + : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); - N = []; + N = []; - for (var i = x; test(i, y) && N.length < max; i += incr) { - var c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') - c = ''; - } else { - c = String(i); - if (pad) { - var need = width - c.length; - if (need > 0) { - var z = new Array(need + 1).join('0'); - if (i < 0) - c = '-' + z + c.slice(1); - else - c = z + c; + for (var i = x; test(i, y) && N.length < max; i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } } } + N.push(c); } - N.push(c); + } else { + N = concatMap(n, function(el) { return expand(el, max, false) }); } - } else { - N = concatMap(n, function(el) { return expand(el, max, false) }); - } - for (var j = 0; j < N.length; j++) { - 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); + for (var j = 0; j < N.length; j++) { + 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); + } } - } - return expansions; + return expansions; + } } @@ -53678,26 +53684,29 @@ class Matcher { // Get or create sibling tracking for current level const currentLevel = this.path.length; - if (!this.siblingStacks[currentLevel]) { - this.siblingStacks[currentLevel] = new Map(); + let level = this.siblingStacks[currentLevel]; + if (!level) { + // `counts` tells same-name siblings apart (the "counter" — nth + // among other s). `total` is every child seen at this level so + // far, kept as a running number instead of re-added from `counts` on + // every push — a parent with many differently-named children would + // otherwise cost more per child the more distinct names it has. + level = { counts: new Map(), total: 0 }; + this.siblingStacks[currentLevel] = level; } - const siblings = this.siblingStacks[currentLevel]; - // Create a unique key for sibling tracking that includes namespace const siblingKey = namespace ? `${namespace}:${tagName}` : tagName; // Calculate counter (how many times this tag appeared at this level) - const counter = siblings.get(siblingKey) || 0; + const counter = level.counts.get(siblingKey) || 0; - // Calculate position (total children at this level so far) - let position = 0; - for (const count of siblings.values()) { - position += count; - } + // Position = total children at this level seen before this one. + const position = level.total; - // Update sibling count for this tag - siblings.set(siblingKey, counter + 1); + // Update sibling count for this tag, and the level's running total. + level.counts.set(siblingKey, counter + 1); + level.total++; // Create new node const node = { @@ -54073,7 +54082,7 @@ class Matcher { snapshot() { return { path: this.path.map(node => ({ ...node })), - siblingStacks: this.siblingStacks.map(map => new Map(map)), + siblingStacks: this.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level), keptAttrs: this._keptAttrs.map(entry => ({ ...entry })) }; } @@ -54085,7 +54094,7 @@ class Matcher { restore(snapshot) { this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); - this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); + this.siblingStacks = snapshot.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level); this._keptAttrs = (snapshot.keptAttrs || []).map(entry => ({ ...entry })); } @@ -56091,6 +56100,389 @@ class XmlNode { } } +;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/node_modules/xml-naming/src/index.js +/** + * xml-naming + * Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications. + * Covers: Name, NCName, QName, NMToken, NMTokens + * + * XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name + * XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar + * XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName + */ + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.0 +// +// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] +// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] +// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487 +// | [#x200C-#x200D] +// | [#x2070-#x218F] | [#x2C00-#x2FEF] +// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] +// +// NameChar ::= NameStartChar | "-" | "." | [0-9] +// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] +// +// Note: \u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0, +// after XML 1.0 was defined against Unicode 2.0. It falls inside the range +// \u037F-\u1FFF but must be excluded. We split that range into +// \u037F-\u0486 and \u0488-\u1FFF to exclude it explicitly. +// --------------------------------------------------------------------------- + +const src_nameStartChar10 = + ':A-Za-z_' + + '\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF' + + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD'; + +const src_nameChar10 = + src_nameStartChar10 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.1 +// +// Differences from XML 1.0: +// +// NameStartChar: +// 1.0 has split ranges: \u00C0-\u00D6, \u00D8-\u00F6, \u00F8-\u02FF +// 1.1 merges them into: \u00C0-\u02FF +// (\u00D7 x and \u00F7 / are division symbols, excluded in both versions) +// +// 1.0 tops out at \uFFFD (BMP only) +// 1.1 adds \u{10000}-\u{EFFFF} (supplementary planes) +// These require the /u flag on the RegExp — see buildRegexes below. +// +// NameChar: +// 1.1 adds \u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0) +// --------------------------------------------------------------------------- + +const src_nameStartChar11 = + ':A-Za-z_' + + '\u00C0-\u02FF' + // merged — 1.0 had three split ranges here + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 (combining mark, never a NameStartChar) + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD' + + '\u{10000}-\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp + +const src_nameChar11 = + src_nameStartChar11 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0 + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Regex builders +// +// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour. +// XML 1.1 regexes: /u flag — required for \u{10000}-\u{EFFFF} to match actual +// supplementary code points rather than lone surrogates (which are illegal XML). +// --------------------------------------------------------------------------- + +const src_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), + }; +}; + +const src_regexes10 = src_buildRegexes(src_nameStartChar10, src_nameChar10); // no /u — BMP only +const src_regexes11 = src_buildRegexes(src_nameStartChar11, src_nameChar11, 'u'); // /u — enables \u{10000}-\u{EFFFF} + +// --------------------------------------------------------------------------- +// ASCII-only fast path (opt-in, off by default) +// +// The XML 1.0 vs 1.1 NameStartChar/NameChar productions differ *only* in +// their non-ASCII ranges (merged vs split Latin-1 ranges, \u0487, and +// supplementary planes). Restricted to ASCII, both versions collapse to the +// same character classes, so a single regex pair covers both xmlVersion +// values — no /u flag needed. +// +// Rationale: unicode-aware regexes (the /u flag, required for XML 1.1's +// supplementary-plane range) are measurably slower in V8 than plain +// non-unicode regexes on the same input, even when the input is pure ASCII. +// For the common case — HTML/SVG ids, XML tags — names are ASCII, so callers +// who know this can opt in to skip the unicode-aware matching path entirely. +// This is a real but *conditional* win: mainly for XML 1.1 input (avoids /u), +// or at scale where the larger unicode character classes add engine +// overhead. It also changes behaviour (rejects legitimate non-ASCII XML +// 1.0/1.1 names), so it must never be silently enabled — hence off by +// default. +// --------------------------------------------------------------------------- + +const nameStartCharAscii = ':A-Za-z_'; +const nameCharAscii = nameStartCharAscii + '\\-\\.\\d'; + +const regexesAscii = src_buildRegexes(nameStartCharAscii, nameCharAscii); // no /u — ASCII only + +const src_getRegexes = (xmlVersion = '1.0', asciiOnly = false) => { + if (asciiOnly) return regexesAscii; + return xmlVersion === '1.1' ? src_regexes11 : src_regexes10; +}; + +// --------------------------------------------------------------------------- +// Boolean validators +// --------------------------------------------------------------------------- + +/** + * Returns true if the string is a valid XML Name. + * Colons are allowed anywhere (Name production). + * Used for: DOCTYPE entity names, notation names, DTD element declarations. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const xml_naming_src_name = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).name.test(str); + +/** + * Returns true if the string is a valid NCName (Non-Colonized Name). + * Colons are not permitted. + * Used for: namespace prefixes, local names, SVG id attributes. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_ncName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).ncName.test(str); + +/** + * Returns true if the string is a valid QName (Qualified Name). + * Allows exactly one colon as a prefix separator: prefix:localName. + * Used for: element and attribute names in namespace-aware XML/SVG. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_qName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).qName.test(str); + +/** + * Returns true if the string is a valid NMToken. + * Like Name but no restriction on the first character. + * Used for: DTD NMTOKEN attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmToken = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmToken.test(str); + +/** + * Returns true if the string is a valid NMTokens value. + * A whitespace-separated list of NMToken values. + * Used for: DTD NMTOKENS attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmTokens = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmTokens.test(str); + +// --------------------------------------------------------------------------- +// Memoized validator factory +// +// Real documents reuse a small vocabulary of tag/attribute names across many +// siblings (e.g. `id`, `class`, `href` repeated across hundreds of elements). +// The plain boolean validators above re-run the regex on every call +// regardless of repeats. `createValidator` returns a closure with a private +// string -> boolean cache, so repeated names after the first become O(1) +// lookups instead of regex tests. +// +// - opts (xmlVersion, asciiOnly) are fixed at creation time, so the regex is +// resolved once, not on every call. +// - The cache is private to the returned closure — no shared/global state, +// no cross-caller pollution. +// - `maxCacheSize` bounds memory: once the cache reaches this many entries, +// it stops accepting new ones (existing entries keep serving hits; new +// misses just fall through to the regex, uncached). This avoids unbounded +// growth against adversarial/high-cardinality input (e.g. validating +// attacker-supplied names with no repeats) without the cost/complexity of +// a full LRU, and without the perf cliff of reset-and-refill thrashing. +// - Call `.reset()` on the returned function to clear the cache manually +// (e.g. between unrelated parse calls). +// --------------------------------------------------------------------------- + +const src_PRODUCTIONS = (/* unused pure expression or super */ null && (['name', 'ncName', 'qName', 'nmToken', 'nmTokens'])); + +/** + * Returns a memoized boolean validator function for a single production, + * with opts fixed at creation time. + * + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean, maxCacheSize?: number }} [opts] + * maxCacheSize: max number of distinct strings to cache (default 2048). + * Once reached, new strings are validated but not cached; existing cached + * entries keep being served. + * @returns {((str: string) => boolean) & { reset: () => void }} + */ +const createValidator = (production, { xmlVersion = '1.0', asciiOnly = false, maxCacheSize = 2048 } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const regex = src_getRegexes(xmlVersion, asciiOnly)[production]; + let cache = new Map(); + + const validator = (str) => { + const cached = cache.get(str); + if (cached !== undefined) return cached; + + const result = regex.test(str); + if (cache.size < maxCacheSize) cache.set(str, result); + return result; + }; + + validator.reset = () => { cache = new Map(); }; + + return validator; +}; + +// --------------------------------------------------------------------------- +// Diagnostic validator +// --------------------------------------------------------------------------- + +/** + * Validates a string against a named production and returns a detailed result. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {{ valid: boolean, production: string, input: string, reason?: string, position?: number }} + */ +const src_validate = (str, production, { xmlVersion = '1.0', asciiOnly = false } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const validators = { name: xml_naming_src_name, ncName: src_ncName, qName: src_qName, nmToken: src_nmToken, nmTokens: src_nmTokens }; + const isValid = validators[production](str, { xmlVersion, asciiOnly }); + + if (isValid) return { valid: true, production, input: str }; + + let reason = 'Does not match the production rules'; + let position; + + // Diagnostic fallback char checks must mirror the same character set the + // boolean validator above used, or the reported reason/position could + // contradict the `valid: false` result (e.g. flagging a char as illegal + // that the unicode-aware check would have accepted). + const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\u00C0-\uFFFD]/; + const namePattern = asciiOnly ? /[\w\-\\.:]/ : /[\w\-\\.:\u00B7\u00C0-\uFFFD]/; + + if (str.length === 0) { + reason = 'Input is empty'; + } else if (production === 'ncName' && str.includes(':')) { + position = str.indexOf(':'); + reason = 'Colon is not allowed in NCName'; + } else if (production === 'qName' && str.startsWith(':')) { + reason = 'QName cannot start with a colon'; + position = 0; + } else if (production === 'qName' && str.endsWith(':')) { + reason = 'QName cannot end with a colon'; + position = str.length - 1; + } else if (production === 'qName' && (str.match(/:/g) || []).length > 1) { + reason = 'QName can have at most one colon'; + position = str.lastIndexOf(':'); + } else if ( + ['name', 'ncName', 'qName'].includes(production) && + !startCharPattern.test(str[0]) + ) { + reason = `First character "${str[0]}" is not a valid NameStartChar`; + position = 0; + } else { + for (let i = 0; i < str.length; i++) { + if (!namePattern.test(str[i])) { + reason = `Character "${str[i]}" at position ${i} is not a valid NameChar`; + position = i; + break; + } + } + } + + return { valid: false, production, input: str, reason, position }; +}; + +// --------------------------------------------------------------------------- +// Batch validator +// --------------------------------------------------------------------------- + +/** + * Validates an array of strings against a named production. + * + * @param {string[]} strings + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {Array<{ valid: boolean, production: string, input: string, reason?: string, position?: number }>} + */ +const src_validateAll = (strings, production, opts = {}) => + strings.map(str => src_validate(str, production, opts)); + +// --------------------------------------------------------------------------- +// Sanitizer +// --------------------------------------------------------------------------- + +/** + * Transforms an invalid string into the nearest valid XML name for the given production. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ replacement?: string, asciiOnly?: boolean }} [opts] + * asciiOnly: also replace any non-ASCII character, not just XML-illegal + * ones (default false). + * @returns {string} + */ +const src_sanitize = (str, production = 'name', { replacement = '_', asciiOnly = false } = {}) => { + if (!str) return replacement; + + let result = str; + + // Strip colons for NCName + if (production === 'ncName') { + result = result.replace(/:/g, ''); + } + + // Replace illegal characters + const allowedCharPattern = asciiOnly ? /[^\w\-\.:]/g : /[^\w\-\.:\u00B7\u00C0-\uFFFD]/g; + result = result.replace(allowedCharPattern, replacement); + + // Fix invalid start character for Name / NCName / QName + if (production !== 'nmToken' && production !== 'nmTokens') { + if (/^[\-\.\d]/.test(result)) { + result = replacement + result; + } + } + + return result || replacement; +}; ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js @@ -56324,7 +56716,7 @@ class DocTypeReader { let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { + if (!this.suppressValidationErr && !src_qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -56498,7 +56890,7 @@ function hasSeq(data, seq, i) { } function validateEntityName(name, xmlVersion) { - if (qName(name, { xmlVersion: xmlVersion })) + if (src_qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); @@ -58282,26 +58674,6 @@ const MISC_SYMBOLS = { nVDash: '⊯', }; -/** - * All entities combined (if you need everything) - * @type {Record} - */ -const ALL_ENTITIES = { - ...BASIC_LATIN, - ...LATIN_ACCENTS, - ...LATIN_EXTENDED, - ...GREEK, - ...CYRILLIC, - ...MATH, - ...MATH_ADVANCED, - ...ARROWS, - ...SHAPES, - ...PUNCTUATION, - ...CURRENCY, - ...FRACTIONS, - ...MISC_SYMBOLS, -}; - const XML = { amp: "&", apos: "'", @@ -58980,6 +59352,152 @@ class EntityDecoder { return this._applyNCRAction(effective, token, cp); } } +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js +/** + * SQL context patterns — high-precision rules only. + * + * These rules have very low false-positive risk and are safe to apply to + * general user text (names, descriptions, search queries, etc.). + * All patterns are ReDoS-safe — unlike the `sql-injection` npm package + * which has an active CVE on its own detection regexes. + * + * For exhaustive coverage including noisier heuristics (comment sequences, + * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. + * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, + * not to general free-text fields. + */ + +const SQL_PATTERNS = [ + { + id: 'sql-block-comment-open', + description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', + pattern: /\/\*/, + }, + { + id: 'sql-union-select', + description: 'UNION SELECT — most common SQL injection aggregation attack', + pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, + }, + { + id: 'sql-drop-table', + description: 'DROP TABLE — destructive DDL injection', + pattern: /\bDROP\s{1,20}TABLE\b/i, + }, + { + id: 'sql-drop-database', + description: 'DROP DATABASE — destructive DDL injection', + pattern: /\bDROP\s{1,20}DATABASE\b/i, + }, + { + id: 'sql-insert-into', + description: 'INSERT INTO — data injection', + pattern: /\bINSERT\s{1,20}INTO\b/i, + }, + { + id: 'sql-delete-from', + description: 'DELETE FROM — data deletion injection', + pattern: /\bDELETE\s{1,20}FROM\b/i, + }, + { + id: 'sql-update-set', + description: 'UPDATE ... SET — data modification injection', + // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) + pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, + }, + { + id: 'sql-exec-xp', + description: 'EXEC xp_ — MSSQL extended stored procedure execution', + pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, + }, + { + id: 'sql-tautology-string', + description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", + // Last quote is optional — injection may truncate it: ' OR '1'='1-- + pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, + }, + { + id: 'sql-tautology-numeric', + description: 'Numeric tautology: OR 1=1', + pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, + }, + { + id: 'sql-always-true-zero', + description: 'Numeric tautology: OR 0=0', + pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, + }, + { + id: 'sql-sleep-benchmark', + description: 'Time-based blind injection: SLEEP() or BENCHMARK()', + pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, + }, + { + id: 'sql-waitfor-delay', + description: 'MSSQL time-based blind injection: WAITFOR DELAY', + pattern: /\bWAITFOR\s{1,20}DELAY\b/i, + }, + { + id: 'sql-char-function', + description: 'CHAR() function — used to obfuscate injected strings', + pattern: /\bCHAR\s*\(\s*\d{1,3}/i, + }, + { + id: 'sql-information-schema', + description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', + pattern: /\bINFORMATION_SCHEMA\b/i, + }, +]; + +/* harmony default export */ const sql = (SQL_PATTERNS); + +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js +/** + * SQL-STRICT context patterns. + * + * Extends the base 'SQL' context with three additional rules that are + * effective at detecting real injections but carry a higher false-positive + * risk on general free-text input. + * + * Use 'SQL-STRICT' when: + * - The string is specifically a SQL fragment or database identifier + * - You control the input domain (e.g. a dedicated SQL search field) + * - You can tolerate occasional false positives in exchange for broader coverage + * + * Use 'SQL' (not STRICT) when: + * - The field is general user text (names, descriptions, comments) + * - False positives would block legitimate content (e.g. "see note -- above") + * + * Rules moved here from 'SQL' due to false-positive risk: + * + * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) + * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words + * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output + */ + + + +const SQL_STRICT_EXTRA = [ + { + id: 'sql-line-comment', + description: 'SQL line comment: -- followed by whitespace or end of string', + pattern: /--(?:\s|$)/, + }, + { + id: 'sql-stacked-query', + description: 'Stacked queries: semicolon immediately followed by a SQL keyword', + pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, + }, + { + id: 'sql-hex-encoding', + description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', + pattern: /\b0x[0-9a-f]{4,}/i, + }, +]; + +// SQL-STRICT = all base SQL rules + the three noisy extras +const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; + +/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); + ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/html.js /** * HTML context patterns. @@ -59247,152 +59765,6 @@ const SVG_PATTERNS = [ /* harmony default export */ const svg = (SVG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js -/** - * SQL context patterns — high-precision rules only. - * - * These rules have very low false-positive risk and are safe to apply to - * general user text (names, descriptions, search queries, etc.). - * All patterns are ReDoS-safe — unlike the `sql-injection` npm package - * which has an active CVE on its own detection regexes. - * - * For exhaustive coverage including noisier heuristics (comment sequences, - * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. - * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, - * not to general free-text fields. - */ - -const SQL_PATTERNS = [ - { - id: 'sql-block-comment-open', - description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', - pattern: /\/\*/, - }, - { - id: 'sql-union-select', - description: 'UNION SELECT — most common SQL injection aggregation attack', - pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, - }, - { - id: 'sql-drop-table', - description: 'DROP TABLE — destructive DDL injection', - pattern: /\bDROP\s{1,20}TABLE\b/i, - }, - { - id: 'sql-drop-database', - description: 'DROP DATABASE — destructive DDL injection', - pattern: /\bDROP\s{1,20}DATABASE\b/i, - }, - { - id: 'sql-insert-into', - description: 'INSERT INTO — data injection', - pattern: /\bINSERT\s{1,20}INTO\b/i, - }, - { - id: 'sql-delete-from', - description: 'DELETE FROM — data deletion injection', - pattern: /\bDELETE\s{1,20}FROM\b/i, - }, - { - id: 'sql-update-set', - description: 'UPDATE ... SET — data modification injection', - // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) - pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, - }, - { - id: 'sql-exec-xp', - description: 'EXEC xp_ — MSSQL extended stored procedure execution', - pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, - }, - { - id: 'sql-tautology-string', - description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", - // Last quote is optional — injection may truncate it: ' OR '1'='1-- - pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, - }, - { - id: 'sql-tautology-numeric', - description: 'Numeric tautology: OR 1=1', - pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, - }, - { - id: 'sql-always-true-zero', - description: 'Numeric tautology: OR 0=0', - pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, - }, - { - id: 'sql-sleep-benchmark', - description: 'Time-based blind injection: SLEEP() or BENCHMARK()', - pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, - }, - { - id: 'sql-waitfor-delay', - description: 'MSSQL time-based blind injection: WAITFOR DELAY', - pattern: /\bWAITFOR\s{1,20}DELAY\b/i, - }, - { - id: 'sql-char-function', - description: 'CHAR() function — used to obfuscate injected strings', - pattern: /\bCHAR\s*\(\s*\d{1,3}/i, - }, - { - id: 'sql-information-schema', - description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', - pattern: /\bINFORMATION_SCHEMA\b/i, - }, -]; - -/* harmony default export */ const sql = (SQL_PATTERNS); - -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js -/** - * SQL-STRICT context patterns. - * - * Extends the base 'SQL' context with three additional rules that are - * effective at detecting real injections but carry a higher false-positive - * risk on general free-text input. - * - * Use 'SQL-STRICT' when: - * - The string is specifically a SQL fragment or database identifier - * - You control the input domain (e.g. a dedicated SQL search field) - * - You can tolerate occasional false positives in exchange for broader coverage - * - * Use 'SQL' (not STRICT) when: - * - The field is general user text (names, descriptions, comments) - * - False positives would block legitimate content (e.g. "see note -- above") - * - * Rules moved here from 'SQL' due to false-positive risk: - * - * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) - * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words - * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output - */ - - - -const SQL_STRICT_EXTRA = [ - { - id: 'sql-line-comment', - description: 'SQL line comment: -- followed by whitespace or end of string', - pattern: /--(?:\s|$)/, - }, - { - id: 'sql-stacked-query', - description: 'Stacked queries: semicolon immediately followed by a SQL keyword', - pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, - }, - { - id: 'sql-hex-encoding', - description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', - pattern: /\b0x[0-9a-f]{4,}/i, - }, -]; - -// SQL-STRICT = all base SQL rules + the three noisy extras -const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; - -/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); - ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/shell.js /** * SHELL context patterns. @@ -59778,21 +60150,40 @@ const LOG_PATTERNS = [ /* harmony default export */ const contexts_log = (LOG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/registry.js +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js /** - * Context registry — maps context name strings to their pattern arrays. + * is-unsafe v2 + * + * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings + * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. * - * Adding a new context: create a file in ./contexts/, export a default array - * of pattern objects, and register it here. + * v2 change: contexts are imported as named pattern arrays rather than resolved + * via a string-keyed registry. This makes each context independently + * tree-shakeable — bundlers can drop any context you never import. * - * Context name guide: - * SQL — high-precision rules; safe for general text fields - * SQL-STRICT — SQL + three noisier rules (line comments, stacked queries, hex); - * use only for SQL-specific inputs - * REDOS — detects ReDoS-prone patterns when string will be compiled as RegExp + * @module is-unsafe */ +// ─── Context pattern arrays (named exports) ──────────────────────────────── +// Import only the ones you need. Each is independently tree-shakeable. + + + + + + + + + + +// SQL-STRICT needs a quoted identifier because of the hyphen + + +// ─── VALID_CONTEXTS convenience re-export ───────────────────────────────── +// Importing this pulls in ALL contexts. Use it only when you need all of them +// (e.g. for validation UI, tooling, or exhaustive audits). +// If you only need a subset, import the named contexts directly instead. @@ -59802,8 +60193,23 @@ const LOG_PATTERNS = [ -/** @type {Record>} */ -const registry_CONTEXT_REGISTRY = { + +// ─── Attach labels to named contexts ────────────────────────────────────── +// Each built-in PatternList carries its canonical name so matchList can read +// list.label directly — no registry lookup needed at match time. +// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label. + +html.label = 'HTML'; +xml.label = 'XML'; +svg.label = 'SVG'; +sql.label = 'SQL'; +sql_strict.label = 'SQL-STRICT'; +shell.label = 'SHELL'; +redos.label = 'REDOS'; +nosql.label = 'NOSQL'; +contexts_log.label = 'LOG'; + +const VALID_CONTEXTS = Object.freeze({ HTML: html, XML: xml, SVG: svg, @@ -59813,45 +60219,29 @@ const registry_CONTEXT_REGISTRY = { REDOS: redos, NOSQL: nosql, LOG: contexts_log, -}; +}); -/* harmony default export */ const registry = (registry_CONTEXT_REGISTRY); +// ─── Types ──────────────────────────────────────────────────────────────── /** - * Enum of valid context names — e.g. `VALID_CONTEXTS.HTML === 'HTML'`. - * @type {Record} - */ -const VALID_CONTEXTS = Object.freeze( - Object.fromEntries(Object.keys(registry_CONTEXT_REGISTRY).map((k) => [k, k])) -); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js -/** - * is-unsafe - * - * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings - * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. - * - * @module is-unsafe + * @typedef {{ id: string, description: string, pattern: RegExp }} Rule */ - - /** - * @typedef {'HTML'|'XML'|'SVG'|'SQL'|'SQL-STRICT'|'SHELL'|'REDOS'|'NOSQL'|'LOG'} ContextName + * @typedef {Rule[]} PatternList */ /** * @typedef {Object} MatchResult - * @property {string} context - The context in which the match was found - * @property {string} id - Rule identifier + * @property {string} context - Label identifying which context matched ('HTML', 'CUSTOM', etc.) + * @property {string} id - Rule identifier * @property {string} description - Human-readable description of what was matched - * @property {RegExp} pattern - The pattern that matched + * @property {RegExp} pattern - The pattern that matched */ -// ─── Validation helpers ──────────────────────────────────────────────────── +// ─── Internal helpers ────────────────────────────────────────────────────── /** - * Validate that `value` is a string. Throws TypeError if not. * @param {unknown} value */ function assertString(value) { @@ -59863,56 +60253,61 @@ function assertString(value) { } /** - * Validate that `context` is a recognised context name, an array of them, - * or a RegExp instance. Throws TypeError if not. - * @param {ContextName|ContextName[]|RegExp} context + * @param {unknown} context */ function assertContext(context) { if (context instanceof RegExp) return; - if (typeof context === 'string') { - if (!registry[context]) { - throw new TypeError( - `is-unsafe: unknown context "${context}". Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); - } - return; - } - if (Array.isArray(context)) { if (context.length === 0) { - throw new TypeError('is-unsafe: context array must not be empty'); + throw new TypeError('is-unsafe: context must not be an empty array'); } - for (const c of context) { - if (typeof c !== 'string' || !registry[c]) { - throw new TypeError( - `is-unsafe: unknown context "${c}" in array. Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); + // Detect array-of-arrays vs flat pattern list + if (Array.isArray(context[0])) { + // Array of PatternLists + for (const list of context) { + if (!Array.isArray(list) || list.length === 0) { + throw new TypeError( + 'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)' + ); + } } } + // else: flat PatternList — trust it, no deep validation needed return; } throw new TypeError( - `is-unsafe: second argument must be a context string, array of context strings, or RegExp. Got: ${typeof context}` + `is-unsafe: second argument must be a PatternList (e.g. HTML), ` + + `an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}` ); } -// ─── Core matching logic ─────────────────────────────────────────────────── +/** + * Normalise any valid context arg into an array of PatternLists. + * + * @param {Rule[]|Rule[][]|RegExp} context + * @returns {{ lists: Rule[][]|null, regex: RegExp|null }} + */ +function normalise(context) { + if (context instanceof RegExp) return { lists: null, regex: context }; + // Distinguish PatternList (array of rule objects) from array of PatternLists + if (Array.isArray(context[0])) return { lists: context, regex: null }; + return { lists: [context], regex: null }; +} /** - * Test a single value against one named context's patterns. - * Returns the first matching MatchResult, or null if nothing matched. + * Test value against a single PatternList. Returns the first MatchResult or null. * * @param {string} value - * @param {string} contextName + * @param {Rule[]} list * @returns {MatchResult|null} */ -function matchContext(value, contextName) { - const patterns = registry[contextName]; - for (const rule of patterns) { +function matchList(value, list) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - return { context: contextName, id: rule.id, description: rule.description, pattern: rule.pattern }; + return { context: label, id: rule.id, description: rule.description, pattern: rule.pattern }; } } return null; @@ -59923,103 +60318,95 @@ function matchContext(value, contextName) { /** * Returns `true` if `value` is unsafe in the given context(s), `false` otherwise. * - * @param {string} value - The string to test - * @param {ContextName|ContextName[]|RegExp} context - * - A named context ('HTML', 'XML', 'SVG', 'SQL', 'SQL-STRICT', 'SHELL', 'REDOS', 'NOSQL', 'LOG') - * - An array of named contexts — returns true if unsafe in **any** of them + * @param {string} value - The string to test + * @param {PatternList | PatternList[] | RegExp} context + * - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`) + * - An array of PatternLists — returns true if unsafe in **any** of them * - A custom RegExp — returns true if the pattern matches * @returns {boolean} * * @example - * isUnsafe('', 'HTML') // true - * isUnsafe('hello world', 'HTML') // false - * isUnsafe('value', ['HTML', 'SQL']) // false - * isUnsafe('value', /my-pattern/i) // false + * import { isUnsafe, HTML, SQL } from 'is-unsafe'; + * + * isUnsafe('', HTML) // true + * isUnsafe('hello world', HTML) // false + * isUnsafe('value', [HTML, SQL]) // false + * isUnsafe('value', /my-pattern/i) // false */ function isUnsafe(value, context) { assertString(value); assertContext(context); - // Custom RegExp — caller-supplied pattern - if (context instanceof RegExp) { - return context.test(value); - } + const { lists, regex } = normalise(context); - // Single named context - if (typeof context === 'string') { - return matchContext(value, context) !== null; - } + if (regex) return regex.test(value); - // Array of named contexts — unsafe if ANY context matches - for (const c of context) { - if (matchContext(value, c) !== null) return true; + for (const list of lists) { + if (matchList(value, list) !== null) return true; } return false; } /** - * Like `isUnsafe`, but instead of a boolean returns the first `MatchResult` - * describing **why** the value was flagged, or `null` if it is safe. - * - * Useful for logging, error messages, or policy reporting. + * Like `isUnsafe`, but returns the first `MatchResult` describing **why** + * the value was flagged, or `null` if it is safe. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult|null} * * @example - * whyUnsafe('', 'HTML') + * import { whyUnsafe, HTML } from 'is-unsafe'; + * + * whyUnsafe('', HTML) * // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ } */ function whyUnsafe(value, context) { assertString(value); assertContext(context); - if (context instanceof RegExp) { - return context.test(value) - ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context } - : null; - } + const { lists, regex } = normalise(context); - if (typeof context === 'string') { - return matchContext(value, context); + if (regex) { + return regex.test(value) + ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex } + : null; } - for (const c of context) { - const result = matchContext(value, c); + for (const list of lists) { + const result = matchList(value, list); if (result !== null) return result; } return null; } /** - * Returns all matching rules across the given context(s), or an empty array - * if the value is safe. Useful for comprehensive auditing. + * Returns **all** matching rules across the given context(s), or an empty + * array if the value is safe. Useful for comprehensive auditing. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult[]} */ function allUnsafe(value, context) { assertString(value); assertContext(context); + const { lists, regex } = normalise(context); const results = []; - if (context instanceof RegExp) { - if (context.test(value)) { - results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context }); + if (regex) { + if (regex.test(value)) { + results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }); } return results; } - const contexts = typeof context === 'string' ? [context] : context; - - for (const c of contexts) { - const patterns = CONTEXT_REGISTRY[c]; - for (const rule of patterns) { + for (const list of lists) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - results.push({ context: c, id: rule.id, description: rule.description, pattern: rule.pattern }); + results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern }); } } } @@ -60029,6 +60416,7 @@ function allUnsafe(value, context) { /* harmony default export */ const src = ((/* unused pure expression or super */ null && (isUnsafe))); + ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -60119,6 +60507,7 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; let namedEntities = { ...XML }; if (this.options.entityDecoder) { this.entityDecoder = this.options.entityDecoder @@ -60136,8 +60525,7 @@ class OrderedObjParser { // onExternalEntity: (name, value) => isUnsafe(value) ? 'block' : 'allow', onInputEntity: (name, value) => //TODO: VALID_CONTEXTS.HTML should be set only if this.options.htmlEntities - isUnsafe(value, [VALID_CONTEXTS.HTML, VALID_CONTEXTS.XML]) - ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, + isUnsafe(value, [html, xml]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, //postCheck: resolved => resolved }); @@ -60327,6 +60715,7 @@ const parseXml = function (xmlData) { // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; const options = this.options; const docTypeReader = new DocTypeReader(options.processEntities); const xmlLen = xmlData.length; @@ -60411,6 +60800,8 @@ const parseXml = function (xmlData) { i = endIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 68) { //'!D' + if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found."); + this.doctypefound = true; const result = docTypeReader.readDocType(xmlData, i); this.entityDecoder.addInputEntities(result.entities); i = result.i; @@ -96635,6 +97026,17 @@ const closePattern = /\\}/g; const commaPattern = /\\,/g; const periodPattern = /\\\./g; const EXPANSION_MAX = 100_000; +// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An +// input like `'{a,b}'.repeat(1500)` stays under that count - its output is +// truncated to 100k results - while making every result ~1500 characters +// long. The result set, and the intermediate arrays built while combining +// brace sets, then grow large enough to exhaust memory and crash the process +// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of +// characters the accumulator may hold at any point, so memory stays flat no +// matter how many brace groups are chained. The limit sits well above any +// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M +// characters) so legitimate input is unaffected. +const EXPANSION_MAX_LENGTH = 4_000_000; function numeric(str) { return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); } @@ -96684,7 +97086,7 @@ function expand(str, options = {}) { if (!str) { return []; } - const { max = EXPANSION_MAX } = options; + const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options; // I don't know why Bash 4.3 does this, but it does. // Anything starting with {} will have the first two bytes preserved // but *only* at the top level, so {},a}b will not expand to anything, @@ -96694,7 +97096,7 @@ function expand(str, options = {}) { if (str.slice(0, 2) === '{}') { str = '\\{\\}' + str.slice(2); } - return expand_(escapeBraces(str), max, true).map(unescapeBraces); + return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces); } function embrace(str) { return '{' + str + '}'; @@ -96708,22 +97110,113 @@ function lte(i, y) { function gte(i, y) { return i >= y; } -function expand_(str, max, isTop) { - /** @type {string[]} */ - const expansions = []; - const m = balanced('{', '}', str); - if (!m) - return [str]; - // no need to expand pre, since it is guaranteed to be free of brace-sets - const pre = m.pre; - const post = m.post.length ? expand_(m.post, max, false) : ['']; - if (/\$$/.test(m.pre)) { - for (let k = 0; k < post.length && k < max; k++) { - const expansion = pre + '{' + m.body + '}' + post[k]; - expansions.push(expansion); +// Build `{ acc[a] + pre + values[v] }` for every combination, capping the +// number of results at `max` and the total number of characters at `maxLength`. +// This is the one place output grows, so bounding it here keeps the single +// accumulator - and therefore memory - flat regardless of how many brace groups +// are combined (CVE-2026-14257). +function combine(acc, pre, values, max, maxLength, dropEmpties) { + const out = []; + let length = 0; + for (let a = 0; a < acc.length; a++) { + for (let v = 0; v < values.length; v++) { + if (out.length >= max) + return out; + const expansion = acc[a] + pre + values[v]; + // Bash drops empty results at the top level. Skip them before they count + // against `max`, so `max` bounds the number of *kept* results. + if (dropEmpties && !expansion) + continue; + if (length + expansion.length > maxLength) + return out; + out.push(expansion); + length += expansion.length; } } - else { + return out; +} +// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`) +// sequence body. +function expandSequence(body, isAlphaSequence, max) { + const n = body.split(/\.\./); + const N = []; + // A sequence body always splits into two or three parts, but the compiler + // can't know that. + /* c8 ignore start */ + if (n[0] === undefined || n[1] === undefined) { + return N; + } + /* c8 ignore stop */ + const x = numeric(n[0]); + const y = numeric(n[1]); + const width = Math.max(n[0].length, n[1].length); + let incr = n.length === 3 && n[2] !== undefined ? + Math.max(Math.abs(numeric(n[2])), 1) + : 1; + let test = lte; + const reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + const pad = n.some(isPadded); + for (let i = x; test(i, y) && N.length < max; i += incr) { + let c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') { + c = ''; + } + } + else { + c = String(i); + if (pad) { + const need = width - c.length; + if (need > 0) { + const z = new Array(need + 1).join('0'); + if (i < 0) { + c = '-' + z + c.slice(1); + } + else { + c = z + c; + } + } + } + } + N.push(c); + } + return N; +} +function expand_(str, max, maxLength, isTop) { + // Consume the string's top-level brace groups left to right, threading a + // running set of combined prefixes (`acc`). Expanding the tail iteratively - + // rather than recursing on `m.post` once per group - keeps the native stack + // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no + // longer overflow the stack, and leaves a single accumulator whose size + // `maxLength` bounds directly (CVE-2026-14257). + let acc = ['']; + // Bash drops empty results, but only when the *first* top-level group is a + // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop + // is on the final strings, so it is applied to whichever `combine` produces + // them (the one with no brace set left in the tail). + let dropEmpties = false; + let firstGroup = true; + for (;;) { + const m = balanced('{', '}', str); + // No brace set left: the rest of the string is literal. + if (!m) { + return combine(acc, str, [''], max, maxLength, dropEmpties); + } + // no need to expand pre, since it is guaranteed to be free of brace-sets + const pre = m.pre; + if (/\$$/.test(pre)) { + acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length); + firstGroup = false; + if (!m.post.length) + break; + str = m.post; + continue; + } const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); const isSequence = isNumericSequence || isAlphaSequence; @@ -96732,87 +97225,47 @@ function expand_(str, max, isTop) { // {a},b} if (m.post.match(/,(?!,).*\}/)) { str = m.pre + '{' + m.body + escClose + m.post; - return expand_(str, max, true); + isTop = true; + continue; } - return [str]; + // Nothing here expands, so the whole remaining string is literal. + return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties); } - let n; + if (firstGroup) { + dropEmpties = isTop && !isSequence; + firstGroup = false; + } + let values; if (isSequence) { - n = m.body.split(/\.\./); + values = expandSequence(m.body, isAlphaSequence, max); } else { - n = parseCommaParts(m.body); + let n = parseCommaParts(m.body); if (n.length === 1 && n[0] !== undefined) { // x{{a,b}}y ==> x{a}y x{b}y - n = expand_(n[0], max, false).map(embrace); + n = expand_(n[0], max, maxLength, false).map(embrace); //XXX is this necessary? Can't seem to hit it in tests. /* c8 ignore start */ if (n.length === 1) { - return post.map(p => m.pre + n[0] + p); + acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length); + if (!m.post.length) + break; + str = m.post; + continue; } /* c8 ignore stop */ } - } - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. - let N; - if (isSequence && n[0] !== undefined && n[1] !== undefined) { - const x = numeric(n[0]); - const y = numeric(n[1]); - const width = Math.max(n[0].length, n[1].length); - let incr = n.length === 3 && n[2] !== undefined ? - Math.max(Math.abs(numeric(n[2])), 1) - : 1; - let test = lte; - const reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - const pad = n.some(isPadded); - N = []; - for (let i = x; test(i, y) && N.length < max; i += incr) { - let c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') { - c = ''; - } - } - else { - c = String(i); - if (pad) { - const need = width - c.length; - if (need > 0) { - const z = new Array(need + 1).join('0'); - if (i < 0) { - c = '-' + z + c.slice(1); - } - else { - c = z + c; - } - } - } - } - N.push(c); - } - } - else { - N = []; + values = []; for (let j = 0; j < n.length; j++) { - N.push.apply(N, expand_(n[j], max, false)); - } - } - for (let j = 0; j < N.length; j++) { - for (let k = 0; k < post.length && expansions.length < max; k++) { - const expansion = pre + N[j] + post[k]; - if (!isTop || isSequence || expansion) { - expansions.push(expansion); - } + values.push.apply(values, expand_(n[j], max, maxLength, false)); } } + acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length); + if (!m.post.length) + break; + str = m.post; } - return expansions; + return acc; } //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/minimatch/dist/esm/assert-valid-pattern.js diff --git a/package-lock.json b/package-lock.json index d288f60bc..b245ad69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,9 +75,9 @@ "license": "MIT" }, "node_modules/@actions/cache/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1627,9 +1627,9 @@ } }, "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "funding": [ { "type": "github", @@ -2234,9 +2234,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2251,9 +2248,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2268,9 +2262,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2285,9 +2276,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2302,9 +2290,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2319,9 +2304,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2336,9 +2318,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2353,9 +2332,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2370,9 +2346,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2387,9 +2360,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2746,15 +2716,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -3650,9 +3620,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", - "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "funding": [ { "type": "github", @@ -3661,17 +3631,32 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^2.2.0", + "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", - "is-unsafe": "^1.0.1", - "path-expression-matcher": "^1.5.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", - "xml-naming": "^0.1.0" + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, + "node_modules/fast-xml-parser/node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -3889,9 +3874,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -4146,9 +4131,9 @@ } }, "node_modules/is-unsafe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", - "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", "funding": [ { "type": "github", @@ -5293,9 +5278,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.0.tgz", - "integrity": "sha512-e5y7RCLHKjemsgQ4eqGJtPyr10ILz25HO7flzxhTV8bgvd5yHx98DGtCAtbVW9f2TqnYI/gEVZd+vz7snrdPTw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "funding": [ { "type": "github", @@ -5972,9 +5957,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": {