Compare commits

..
7 Commits
Author SHA1 Message Date
d0e6e4dbf7 chore(deps): bump the monthly-npm-updates group with 13 updates (#1276)
* chore(deps): bump the monthly-npm-updates group with 13 updates

Bumps the monthly-npm-updates group with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | `5.11.0` | `5.11.1` |
| [@jest/globals](https://github.com/jestjs/jest/tree/HEAD/packages/jest-globals) | `30.4.1` | `30.5.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.6.2` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.67.0` | `8.70.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.67.0` | `8.70.1` |
| [eslint](https://github.com/eslint/eslint) | `10.8.1` | `10.11.0` |
| [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) | `29.16.1` | `29.16.6` |
| [globals](https://github.com/sindresorhus/globals) | `17.11.0` | `17.12.0` |
| [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) | `30.4.2` | `30.5.2` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.3.0` | `17.5.1` |
| [prettier](https://github.com/prettier/prettier) | `3.9.6` | `3.9.9` |
| [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.4.12` | `29.4.13` |
| [typescript](https://github.com/microsoft/TypeScript) | `6.0.3` | `7.0.2` |


Updates `fast-xml-parser` from 5.11.0 to 5.11.1
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.11.0...v5.11.1)

Updates `@jest/globals` from 30.4.1 to 30.5.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest-globals)

Updates `@types/node` from 26.2.0 to 26.6.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 8.67.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.67.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/parser)

Updates `eslint` from 10.8.1 to 10.11.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.11.0)

Updates `eslint-plugin-jest` from 29.16.1 to 29.16.6
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v29.16.1...v29.16.6)

Updates `globals` from 17.11.0 to 17.12.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.11.0...v17.12.0)

Updates `jest` from 30.4.2 to 30.5.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest)

Updates `lint-staged` from 17.3.0 to 17.5.1
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.3.0...v17.5.1)

Updates `prettier` from 3.9.6 to 3.9.9
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.9)

Updates `ts-jest` from 29.4.12 to 29.4.13
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.12...v29.4.13)

Updates `typescript` from 6.0.3 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-version: 5.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: monthly-npm-updates
- dependency-name: "@jest/globals"
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: "@types/node"
  dependency-version: 26.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: eslint-plugin-jest
  dependency-version: 29.16.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: monthly-npm-updates
- dependency-name: globals
  dependency-version: 17.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: jest
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: lint-staged
  dependency-version: 17.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: monthly-npm-updates
- dependency-name: prettier
  dependency-version: 3.9.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: monthly-npm-updates
- dependency-name: ts-jest
  dependency-version: 29.4.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: monthly-npm-updates
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: monthly-npm-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix monthly npm update checks

Keep TypeScript on the compatible 6.x line for the current @typescript-eslint peer range, rebuild dist, and refresh the fast-xml-parser license cache.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix monthly npm validation failures

Update vulnerable transitive packages in the lockfile, rebuild dist, and correct the JetBrains test expectation for mocked Windows availability.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* chore: update licensed cache for npm updates

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bruno Borges <brborges@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-02 01:24:19 -04:00
b24925bc49 chore(deps): bump undici from 6.28.0 to 6.29.0 (#1274)
* chore(deps): bump undici from 6.28.0 to 6.29.0

Bumps [undici](https://github.com/nodejs/undici) from 6.28.0 to 6.29.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.28.0...v6.29.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): update generated undici artifacts

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* chore(deps): bump brace-expansion to 5.0.12 to fix high-severity audit

Resolves GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p.
Regenerates dist/ and updates licensed cache.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bruno Borges <brborges@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-02 00:37:36 -04:00
Bruno BorgesandCopilot App 47b36480ae Support Temurin JEP 322 patch versions like 26.0.2.1+1 (#1270) (#1279)
Accept 4-part versions with build metadata (X.Y.Z.P+B -> X.Y.Z+P.B) and
match exact Temurin requests against OpenJDK-derived version keys, since the
Adoptium API semver folds the patch into the build number (26.0.2+101) and
adds LTS metadata (25.0.4+7.0.LTS).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-02 00:09:56 -04:00
Bruno BorgesandCopilot App 74920aab4a Prefer Zulu 4-segment hotfix builds when resolving version ranges (#1278)
Azul reports hotfix releases like 25.0.4.1 as java_version=[25,0,4,1]
with openjdk_build_number=1 (SDKMAN '25.0.4+1.1'). Sorting candidates via
semver.compareBuild ranked 25.0.4+7 above 25.0.4+1.1, so ranges such as
'25' resolved to the older build. Compare java_version, build number and
distro_version numerically instead.

Fixes: #1275

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-02 00:07:25 -04:00
Copilotandbrunoborges a78ec39f12 Stop GPG agent before removing signature-verification home (#1273)
* Initial plan

* Stop GPG agent before verification home cleanup

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
2026-09-28 21:26:40 -04:00
97c5a5e13a Support Temurin on Linux RISC-V (#1269)
* Document Temurin RISC-V compatibility

Co-Authored-By: multicode <multicode@yawk.at>

* Expose RISC-V as a canonical architecture

Co-Authored-By: multicode <multicode@yawk.at>

* Support Temurin on Linux RISC-V

Co-Authored-By: multicode <multicode@yawk.at>

* Address RISC-V review feedback

Co-Authored-By: multicode <multicode@yawk.at>

* Fix casing for RISC-V architecture in tests

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update __tests__/java-platform-contract.test.ts

---------

Co-authored-by: multicode <multicode@yawk.at>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-25 10:04:21 -03:00
Copilotandbrunoborges 1aa87b638d Preserve compound Liberica build versions from .sdkmanrc (#1268)
* Initial plan

* Preserve compound Liberica build versions from release metadata

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
2026-09-10 23:15:52 -04:00
37 changed files with 2759 additions and 1178 deletions
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -150,7 +150,7 @@ steps:
| `java-version-file` | Path to `.java-version`, `.tool-versions`, or `.sdkmanrc`. Used when `java-version` is not set. | | | `java-version-file` | Path to `.java-version`, `.tool-versions`, or `.sdkmanrc`. Used when `java-version` is not set. | |
| `distribution` | Java distribution keyword. Values are case-sensitive and must match one of the supported keywords below. Required unless `java-version-file` points to `.sdkmanrc` with a recognized distribution suffix. | | | `distribution` | Java distribution keyword. Values are case-sensitive and must match one of the supported keywords below. Required unless `java-version-file` points to `.sdkmanrc` with a recognized distribution suffix. | |
| `java-package` | Package variant such as `jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, or `jre+ft`. Support varies by distribution. | `jdk` | | `java-package` | Package variant such as `jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, or `jre+ft`. Support varies by distribution. | `jdk` |
| `architecture` | Package architecture. Canonical values are `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`, `ppc64`, and `s390x`. Aliases `ia32`, `amd64`, `arm`, and `arm64` are normalized. | Runner architecture | | `architecture` | Package architecture. Canonical values are `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`, `ppc64`, `riscv64`, and `s390x`. Aliases `ia32`, `amd64`, `arm`, and `arm64` are normalized. | Runner architecture |
| `jdk-file` | Local compressed JDK archive. Requires `distribution: jdkfile`. | | | `jdk-file` | Local compressed JDK archive. Requires `distribution: jdkfile`. | |
| `check-latest` | Check remote metadata for the latest version satisfying the version spec before using the runner tool cache. | `false` | | `check-latest` | Check remote metadata for the latest version satisfying the version spec before using the runner tool cache. | `false` |
| `force-download` | Always download Java and replace any matching version in the tool cache. | `false` | | `force-download` | Always download Java and replace any matching version in the tool cache. | `false` |
@@ -233,7 +233,7 @@ Additional distribution notes:
| --- | --- | | --- | --- |
| Major version | `8`, `11`, `17`, `21`, `25` | | Major version | `8`, `11`, `17`, `21`, `25` |
| Specific feature or patch version | `11.0`, `11.0.4`, `17.0`, `8.0.282+8` | | Specific feature or patch version | `11.0`, `11.0.4`, `17.0`, `8.0.282+8` |
| JEP 322 multi-field versions | `11.0.9.1`, `18.0.1.1` | | JEP 322 multi-field versions | `11.0.9.1`, `18.0.1.1`, `26.0.2.1+1` |
| Early access | `15-ea`, `15.0.0-ea`, `27-ea` | | Early access | `15-ea`, `15.0.0-ea`, `27-ea` |
| Latest stable GA release | `latest` | | Latest stable GA release | `latest` |
@@ -1758,6 +1758,8 @@ describe('normalizeVersion', () => {
['11.0.9.1', {version: '11.0.9+1', stable: true, latest: false}], ['11.0.9.1', {version: '11.0.9+1', stable: true, latest: false}],
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true, latest: false}], ['12.0.2.1.0', {version: '12.0.2+1.0', stable: true, latest: false}],
['18.0.1.1-ea', {version: '18.0.1+1', stable: false, latest: false}], ['18.0.1.1-ea', {version: '18.0.1+1', stable: false, latest: false}],
['26.0.2.1+1', {version: '26.0.2+1.1', stable: true, latest: false}],
['25.0.4.1+1', {version: '25.0.4+1.1', stable: true, latest: false}],
['latest', {version: 'x', stable: true, latest: true}], ['latest', {version: 'x', stable: true, latest: true}],
['LATEST', {version: 'x', stable: true, latest: true}], ['LATEST', {version: 'x', stable: true, latest: true}],
[' Latest ', {version: 'x', stable: true, latest: true}] [' Latest ', {version: 'x', stable: true, latest: true}]
@@ -82,7 +82,7 @@ describe('getJavaDistribution', () => {
); );
}); });
it.each(['8', '23.x', '23.0.1.1', '<24'])( it.each(['8', '23.x', '23.0.1.1', '23.0.1.1+1', '<24'])(
"rejects Temurin java-package 'jdk+jmods' for version %s", "rejects Temurin java-package 'jdk+jmods' for version %s",
async version => { async version => {
await expect( await expect(
@@ -96,7 +96,7 @@ describe('getJavaDistribution', () => {
} }
); );
it.each(['24', '24.0.1.1', '25-ea', '>=21', 'latest'])( it.each(['24', '24.0.1.1', '25.0.4.1+1', '25-ea', '>=21', 'latest'])(
"accepts Temurin java-package 'jdk+jmods' for version %s", "accepts Temurin java-package 'jdk+jmods' for version %s",
async version => { async version => {
expect( expect(
@@ -14,7 +14,6 @@ import type {IncomingMessage} from 'http';
import {Readable} from 'stream'; import {Readable} from 'stream';
import manifestData from '../data/jetbrains.json' with {type: 'json'}; import manifestData from '../data/jetbrains.json' with {type: 'json'};
import os from 'os';
// Mock @actions/core before importing source modules that depend on it // Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({ jest.unstable_mockModule('@actions/core', () => ({
@@ -81,6 +80,7 @@ describe('getAvailableVersions', () => {
jest.setTimeout(10_000); jest.setTimeout(10_000);
let spyHttpClient: any; let spyHttpClient: any;
let spyHttpClientHead: any;
let spyCoreError: any; let spyCoreError: any;
const originalGitHubToken = process.env.GITHUB_TOKEN; const originalGitHubToken = process.env.GITHUB_TOKEN;
@@ -93,6 +93,10 @@ describe('getAvailableVersions', () => {
headers: {}, headers: {},
result: [] result: []
}); });
spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head');
spyHttpClientHead.mockResolvedValue({
message: {statusCode: 200}
} as any);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = core.error as jest.Mock; spyCoreError = core.error as jest.Mock;
@@ -133,9 +137,7 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
const length = expect(availableVersions.length).toBe(manifestData.length + 2);
os.platform() === 'win32' ? manifestData.length : manifestData.length + 2;
expect(availableVersions.length).toBe(length);
}, 10_000); }, 10_000);
it('continues a stable request after an all-prerelease page', async () => { it('continues a stable request after an all-prerelease page', async () => {
@@ -358,6 +360,7 @@ describe('getAvailableVersions', () => {
it('retries a GitHub rate limit using Retry-After', async () => { it('retries a GitHub rate limit using Retry-After', async () => {
spyHttpClient.mockRestore(); spyHttpClient.mockRestore();
spyHttpClientHead.mockRestore();
const sleep = jest.fn(async () => undefined); const sleep = jest.fn(async () => undefined);
const requestRaw = jest const requestRaw = jest
.spyOn(HttpClient.prototype, 'requestRaw') .spyOn(HttpClient.prototype, 'requestRaw')
@@ -253,6 +253,39 @@ describe('findPackageForDownload', () => {
expect(result.version).toBe(expected); expect(result.version).toBe(expected);
}); });
describe('compound build versions', () => {
beforeEach(() => {
distribution['getAvailableVersions'] = async () =>
['1', '1.1', '1.2', '1.10'].map(build => ({
featureVersion: 25,
interimVersion: 0,
updateVersion: 4,
buildVersion: 1,
version: `25.0.4+${build}`,
downloadUrl: `https://download.bell-sw.com/java/25.0.4+${build}/bellsoft-jdk25.0.4+${build}-macos-aarch64.tar.gz`
}));
});
it.each([
['25.0.4+1.1', '25.0.4+1.1'],
['25.0.4+1', '25.0.4+1'],
['25.0.4', '25.0.4+1.10'],
['25', '25.0.4+1.10']
])('version is %s -> %s', async (input, expected) => {
const result = await distribution['findPackageForDownload'](input);
expect(result).toEqual({
version: expected,
url: `https://download.bell-sw.com/java/${expected}/bellsoft-jdk${expected}-macos-aarch64.tar.gz`
});
});
it('does not substitute a different compound build', async () => {
await expect(
distribution['findPackageForDownload']('25.0.4+1.3')
).rejects.toThrow(/No matching version found for SemVer/);
});
});
it('should throw an error', async () => { it('should throw an error', async () => {
await expect(distribution['findPackageForDownload']('17')).rejects.toThrow( await expect(distribution['findPackageForDownload']('17')).rejects.toThrow(
/No matching version found for SemVer/ /No matching version found for SemVer/
@@ -335,6 +368,36 @@ describe('convertVersionToSemver', () => {
buildVersion: 13 buildVersion: 13
}, },
'11.0.0+13' '11.0.0+13'
],
[
{
version: '25.0.4+1.1',
featureVersion: 25,
interimVersion: 0,
updateVersion: 4,
buildVersion: 1
},
'25.0.4+1.1'
],
[
{
version: '25+36',
featureVersion: 25,
interimVersion: 0,
updateVersion: 0,
buildVersion: 36
},
'25.0.0+36'
],
[
{
version: '8u202',
featureVersion: 8,
interimVersion: 0,
updateVersion: 202,
buildVersion: 8
},
'8.0.202+8'
] ]
])('%s -> %s', (input, expected) => { ])('%s -> %s', (input, expected) => {
const actual = distributions['convertVersionToSemver']({ const actual = distributions['convertVersionToSemver']({
@@ -293,7 +293,8 @@ describe('getAvailableVersions', () => {
it.each([ it.each([
['amd64', 'x64'], ['amd64', 'x64'],
['arm', 'arm'], ['arm', 'arm'],
['arm64', 'aarch64'] ['arm64', 'aarch64'],
['riscv64', 'riscv64']
])( ])(
'defaults to os.arch(): %s mapped to distro arch: %s', 'defaults to os.arch(): %s mapped to distro arch: %s',
async (osArch: string, distroArch: string) => { async (osArch: string, distroArch: string) => {
@@ -377,6 +378,97 @@ describe('findPackageForDownload', () => {
expect(resolvedVersion.version).toBe('16.0.2+7'); expect(resolvedVersion.version).toBe('16.0.2+7');
}); });
describe('OpenJDK patch (respin) versions', () => {
const makeRelease = (
semverVersion: string,
openjdkVersion: string,
versionData: Record<string, number>
) => ({
binaries: [
{
package: {
link: `https://example.com/${openjdkVersion}.tar.gz`,
checksum: `checksum-${openjdkVersion}`,
checksum_link: `https://example.com/${openjdkVersion}.sha256.txt`
}
}
],
version_data: {
semver: semverVersion,
openjdk_version: openjdkVersion,
minor: 0,
...versionData
}
});
const respinManifest = [
makeRelease('26.0.2+101', '26.0.2.1+1', {
major: 26,
security: 2,
patch: 1,
build: 1
}),
makeRelease('26.0.2+10', '26.0.2+10', {
major: 26,
security: 2,
build: 10
}),
makeRelease('25.0.4+101.0.LTS', '25.0.4.1+1-LTS', {
major: 25,
security: 4,
patch: 1,
build: 1
}),
makeRelease('25.0.4+7.0.LTS', '25.0.4+7-LTS', {
major: 25,
security: 4,
build: 7
})
];
it.each([
['26.0.2.1+1', '26.0.2+101'],
['26.0.2+10', '26.0.2+10'],
['26', '26.0.2+101'],
['26.0.2', '26.0.2+101'],
['25.0.4.1+1', '25.0.4+101.0.LTS'],
['25.0.4+7', '25.0.4+7.0.LTS'],
['25.0.4', '25.0.4+101.0.LTS']
])('%s resolves to %s', async (input, expected) => {
const distribution = new TemurinDistribution(
{
version: input,
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);
distribution['getAvailableVersions'] = async () => respinManifest as any;
const resolvedVersion = await distribution['findPackageForDownload'](
distribution['version']
);
expect(resolvedVersion.version).toBe(expected);
expect(resolvedVersion).not.toHaveProperty('openjdkVersion');
});
it('does not match a non-existent respin', async () => {
const distribution = new TemurinDistribution(
{
version: '26.0.2.2+1',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);
distribution['getAvailableVersions'] = async () => respinManifest as any;
await expect(
distribution['findPackageForDownload'](distribution['version'])
).rejects.toThrow(/No matching version found for SemVer '26.0.2\+2.1'/);
});
});
it('version is found but binaries list is empty', async () => { it('version is found but binaries list is empty', async () => {
const distribution = new TemurinDistribution( const distribution = new TemurinDistribution(
{ {
@@ -334,6 +334,69 @@ describe('findPackageForDownload', () => {
); );
}); });
describe('hotfix builds with a 4-segment java_version', () => {
// Mirrors the Azul Metadata API: 25.0.4.1 hotfix is reported as
// java_version=[25,0,4,1], openjdk_build_number=1 (SDKMAN '25.0.4+1.1').
const hotfixManifest = [
{
package_uuid: 'uuid-25.0.4+7',
name: 'zulu25.36.15-ca-jdk25.0.4-linux_x64.tar.gz',
download_url:
'https://cdn.azul.com/zulu/bin/zulu25.36.15-ca-jdk25.0.4-linux_x64.tar.gz',
java_version: [25, 0, 4],
openjdk_build_number: 7,
distro_version: [25, 36, 15, 0],
latest: false,
availability_type: 'ca'
},
{
package_uuid: 'uuid-25.0.4+1.1',
name: 'zulu25.36.205-ca-jdk25.0.4.1-linux_x64.tar.gz',
download_url:
'https://cdn.azul.com/zulu/bin/zulu25.36.205-ca-jdk25.0.4.1-linux_x64.tar.gz',
java_version: [25, 0, 4, 1],
openjdk_build_number: 1,
distro_version: [25, 36, 205, 0],
latest: true,
availability_type: 'ca'
},
{
package_uuid: 'uuid-25.0.3+9',
name: 'zulu25.34.17-ca-jdk25.0.3-linux_x64.tar.gz',
download_url:
'https://cdn.azul.com/zulu/bin/zulu25.34.17-ca-jdk25.0.3-linux_x64.tar.gz',
java_version: [25, 0, 3],
openjdk_build_number: 9,
distro_version: [25, 34, 17, 0],
latest: false,
availability_type: 'ca'
}
] as IZuluVersions[];
it.each([
['25.0.4+1.1', '25.0.4+1.1', 'uuid-25.0.4+1.1'],
['25', '25.0.4+1.1', 'uuid-25.0.4+1.1'],
['25.0.4', '25.0.4+1.1', 'uuid-25.0.4+1.1'],
['25.0.4+7', '25.0.4+7', 'uuid-25.0.4+7'],
['25.0.3', '25.0.3+9', 'uuid-25.0.3+9']
])('version is %s -> %s', async (input, expected, uuid) => {
const distribution = new ZuluDistribution({
version: input,
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
distribution['getAvailableVersions'] = async () => hotfixManifest;
const result = await distribution['findPackageForDownload'](
distribution['version']
);
expect(result.version).toBe(expected);
expect(result.url).toBe(
hotfixManifest.find(item => item.package_uuid === uuid)!.download_url
);
});
});
it('should throw an error', async () => { it('should throw an error', async () => {
const distribution = new ZuluDistribution({ const distribution = new ZuluDistribution({
version: '18', version: '18',
+22 -5
View File
@@ -236,7 +236,12 @@ describe('gpg tests', () => {
process.env['RUNNER_TEMP'] = tempDir; process.env['RUNNER_TEMP'] = tempDir;
}); });
it.each(['success', 'import failure', 'verification failure'])( it.each([
'success',
'import failure',
'verification failure',
'gpgconf unavailable'
])(
'uses a short macOS home or RUNNER_TEMP elsewhere and cleans up after %s', 'uses a short macOS home or RUNNER_TEMP elsewhere and cleans up after %s',
async outcome => { async outcome => {
const longRunnerTemp = path.join( const longRunnerTemp = path.join(
@@ -257,9 +262,16 @@ describe('gpg tests', () => {
fs.writeFileSync(signaturePath, 'signature'); fs.writeFileSync(signaturePath, 'signature');
(tc.downloadTool as jest.Mock<any>).mockResolvedValue(signaturePath); (tc.downloadTool as jest.Mock<any>).mockResolvedValue(signaturePath);
(exec.exec as jest.Mock<any>).mockImplementation( (exec.exec as jest.Mock<any>).mockImplementation(
async (_command: string, args: string[]) => { async (command: string, args: string[]) => {
gpgHome = path.join(expectedParent, path.posix.basename(args[1])); gpgHome = path.join(expectedParent, path.posix.basename(args[1]));
expect(args[1]).toBe(gpg.toGpgPath(gpgHome)); expect(args[1]).toBe(gpg.toGpgPath(gpgHome));
if (command === 'gpgconf') {
expect(fs.existsSync(gpgHome)).toBe(true);
if (outcome === 'gpgconf unavailable') {
throw new Error('gpgconf unavailable');
}
return 0;
}
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
expect( expect(
Buffer.byteLength(path.join(gpgHome, 'S.gpg-agent.browser')) Buffer.byteLength(path.join(gpgHome, 'S.gpg-agent.browser'))
@@ -287,13 +299,18 @@ describe('gpg tests', () => {
'https://example.com/jdk.tar.gz.sig', 'https://example.com/jdk.tar.gz.sig',
'public key' 'public key'
); );
if (outcome === 'success') { if (outcome === 'success' || outcome === 'gpgconf unavailable') {
await verification; await verification;
} else { } else {
await expect(verification).rejects.toThrow(outcome); await expect(verification).rejects.toThrow(outcome);
} }
expect(exec.exec).toHaveBeenCalledTimes( expect(exec.exec).toHaveBeenCalledTimes(
outcome === 'import failure' ? 1 : 2 outcome === 'import failure' ? 2 : 3
);
expect(exec.exec).toHaveBeenLastCalledWith(
'gpgconf',
['--homedir', gpg.toGpgPath(gpgHome), '--kill', 'gpg-agent'],
{silent: true, ignoreReturnCode: true}
); );
expect(fs.existsSync(gpgHome)).toBe(false); expect(fs.existsSync(gpgHome)).toBe(false);
expect(fs.existsSync(signaturePath)).toBe(false); expect(fs.existsSync(signaturePath)).toBe(false);
@@ -368,7 +385,7 @@ describe('gpg tests', () => {
], ],
expect.objectContaining({silent: true}) expect.objectContaining({silent: true})
); );
expect(exec.exec).toHaveBeenCalledTimes(2); expect(exec.exec).toHaveBeenCalledTimes(3);
}); });
}); });
}); });
+8
View File
@@ -25,6 +25,7 @@ describe('Java platform capabilities', () => {
['aarch64', 'aarch64'], ['aarch64', 'aarch64'],
['arm64', 'aarch64'], ['arm64', 'aarch64'],
['ppc64le', 'ppc64le'], ['ppc64le', 'ppc64le'],
['RiScV64', 'riscv64'],
['s390x', 's390x'] ['s390x', 's390x']
])('normalizes architecture %s to %s', (input, expected) => { ])('normalizes architecture %s to %s', (input, expected) => {
expect(normalizeArchitecture(input)).toBe(expected); expect(normalizeArchitecture(input)).toBe(expected);
@@ -68,6 +69,12 @@ describe('Java platform capabilities', () => {
); );
}); });
it('allows Temurin on Linux riscv64', () => {
expect(validateJavaPlatform('temurin', 'linux', 'riscv64', '25')).toBe(
'riscv64'
);
});
it('rejects OS-specific restrictions with a consistent diagnostic', () => { it('rejects OS-specific restrictions with a consistent diagnostic', () => {
expect(() => expect(() =>
validateJavaPlatform('oracle', 'win32', 'arm64', '21') validateJavaPlatform('oracle', 'win32', 'arm64', '21')
@@ -115,6 +122,7 @@ describe('Java platform capabilities', () => {
'aarch64', 'aarch64',
'ppc64le', 'ppc64le',
'ppc64', 'ppc64',
'riscv64',
's390x' 's390x'
]) { ]) {
expect(content).toContain(architecture); expect(content).toContain(architecture);
+19
View File
@@ -43,6 +43,7 @@ const core = await import('@actions/core');
const { const {
convertVersionToSemver, convertVersionToSemver,
normalizeJavaVersionToSemver,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
getVersionFromFileContent, getVersionFromFileContent,
isVersionSatisfies, isVersionSatisfies,
@@ -188,6 +189,22 @@ describe('convertVersionToSemver', () => {
}); });
}); });
describe('normalizeJavaVersionToSemver', () => {
it.each([
['17', '17'],
['17.0.8', '17.0.8'],
['17.0.8+7', '17.0.8+7'],
['11.0.9.1', '11.0.9+1'],
['12.0.2.1.0', '12.0.2+1.0'],
['26.0.2.1+1', '26.0.2+1.1'],
['17.0.8.1+1080.1', '17.0.8+1.1080.1'],
['>=11.0.9.1', '>=11.0.9.1'],
['17.x', '17.x']
])('%s -> %s', (input: string, expected: string) => {
expect(normalizeJavaVersionToSemver(input)).toBe(expected);
});
});
describe('getNextPageUrlFromLinkHeader', () => { describe('getNextPageUrlFromLinkHeader', () => {
it.each([ it.each([
[ [
@@ -276,6 +293,8 @@ describe('getVersionFromFileContent', () => {
['java=21.0.5-graal', '21.0.5', 'graalvm'], ['java=21.0.5-graal', '21.0.5', 'graalvm'],
['java=17.0.9-graalce', '17.0.9', 'graalvm'], ['java=17.0.9-graalce', '17.0.9', 'graalvm'],
['java=11.0.25-librca', '11.0.25', 'liberica'], ['java=11.0.25-librca', '11.0.25', 'liberica'],
['java=25.0.4+1.1-librca', '25.0.4+1.1', 'liberica'],
['java=25.0.4+1.1-zulu', '25.0.4+1.1', 'zulu'],
['java=11.0.25-ms', '11.0.25', 'microsoft'], ['java=11.0.25-ms', '11.0.25', 'microsoft'],
['java=21.0.5-oracle', '21.0.5', 'oracle'], ['java=21.0.5-oracle', '21.0.5', 'oracle'],
['java=11.0.25-sapmchn', '11.0.25', 'sapmachine'], ['java=11.0.25-sapmchn', '11.0.25', 'sapmachine'],
+1 -1
View File
@@ -17,7 +17,7 @@ inputs:
required: false required: false
default: 'jdk' default: 'jdk'
architecture: architecture:
description: "The architecture of the package (`x86`, `x64`, `armv7`, `aarch64`, `ppc64le`, `ppc64`, or `s390x`). Aliases `ia32`, `amd64`, `arm`, and `arm64` are normalized to `x86`, `x64`, `armv7`, and `aarch64`. Supported values vary by distribution and operating system. Defaults to the action runner's architecture." description: "The architecture of the package (`x86`, `x64`, `armv7`, `aarch64`, `ppc64le`, `ppc64`, `riscv64`, or `s390x`). Aliases `ia32`, `amd64`, `arm`, and `arm64` are normalized to `x86`, `x64`, `armv7`, and `aarch64`. Supported values vary by distribution and operating system. Defaults to the action runner's architecture."
required: false required: false
jdk-file: jdk-file:
description: 'Path to where the compressed JDK is located' description: 'Path to where the compressed JDK is located'
+164 -28
View File
@@ -18360,9 +18360,102 @@ function readAttributeStr(xmlData, i) {
} }
/** /**
* Select all the attributes whether valid or invalid. * Walk `attrStr` once, left to right, splitting it into attribute tokens.
*
* This replaces a regex that used to do the same job
* (`(\s*)([^\s=]+)(\s*=)?(\s*(['"])(([\s\S])*?)\5)?`). That regex led with an
* optional whitespace group followed by a required "non-whitespace" group.
* On a long run of whitespace that never resolves into an attribute name
* (e.g. a tag with thousands of trailing spaces before `>`), the engine
* backtracks the whitespace group one character at a time before giving up
* and moving to the next starting position — one full backtrack per
* position, which is quadratic in the length of the run.
*
* A single forward-only scan can never backtrack, so it can't be made slow
* this way no matter how much whitespace the input contains — it's always
* proportional to the length of the string, once.
*
* Each returned token mirrors the shape the old regex match array had, so
* the validation logic below (which reads token[1]..token[6]) didn't need
* to change:
* token.startIndex - where this token begins in attrStr
* token[1] - leading whitespace before the name
* token[2] - the attribute name
* token[3] - whitespace + '=' if present, else undefined
* token[4] - marker (any defined value) if a quoted value was found
* token[5] - the quote character used ('"' or "'")
* token[6] - the value's text, without the surrounding quotes
*
* A malformed leading character (e.g. a stray '=' with no name before it)
* is simply skipped over, one character at a time — the same outcome the
* old regex produced by failing to match at that position and retrying at
* the next one.
*/ */
const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g'); function scanAttributeTokens(attrStr) {
const tokens = [];
const len = attrStr.length;
let i = 0;
while (i < len) {
const tokenStart = i;
// Leading whitespace before the name.
while (i < len && isWhiteSpace(attrStr[i])) i++;
if (i >= len) break; // trailing whitespace only — nothing left to read
if (attrStr[i] === '=') {
// No name before this '=' — not a valid attribute start. Move past
// just this one character and try again from the next position.
i = tokenStart + 1;
continue;
}
const leadingWs = attrStr.slice(tokenStart, i);
// Attribute name — everything up to the next whitespace or '='.
const nameStart = i;
while (i < len && !isWhiteSpace(attrStr[i]) && attrStr[i] !== '=') i++;
const name = attrStr.slice(nameStart, i);
// Optional whitespace + '='.
let equalsGroup; // whitespace + '=' text, or undefined if absent
let j = i;
while (j < len && isWhiteSpace(attrStr[j])) j++;
if (j < len && attrStr[j] === '=') {
equalsGroup = attrStr.slice(i, j + 1);
i = j + 1;
}
// Optional whitespace + quoted value.
let quoteChar;
let value;
let k = i;
while (k < len && isWhiteSpace(attrStr[k])) k++;
if (k < len && (attrStr[k] === '"' || attrStr[k] === "'")) {
const valueStart = k + 1;
const closeIdx = attrStr.indexOf(attrStr[k], valueStart);
if (closeIdx !== -1) {
quoteChar = attrStr[k];
value = attrStr.slice(valueStart, closeIdx);
i = closeIdx + 1;
}
// No closing quote found anywhere in the rest of the string — leave
// quoteChar/value undefined, same as the old regex's group failing
// to match a backreference-less run.
}
const token = { startIndex: tokenStart };
token[1] = leadingWs;
token[2] = name;
token[3] = equalsGroup;
token[4] = quoteChar !== undefined ? true : undefined;
token[5] = quoteChar;
token[6] = value;
tokens.push(token);
}
return tokens;
}
//attr, ="sd", a="amit's", a="sd"b="saf", ab cd="" //attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
@@ -18371,7 +18464,7 @@ function validateAttributeString(attrStr, options) {
//if(attrStr.trim().length === 0) return true; //empty string //if(attrStr.trim().length === 0) return true; //empty string
const matches = getAllMatches(attrStr, validAttrStrRegxp); const matches = scanAttributeTokens(attrStr);
const attrNames = {}; const attrNames = {};
for (let i = 0; i < matches.length; i++) { for (let i = 0; i < matches.length; i++) {
@@ -18473,7 +18566,6 @@ function getLineNumberForPosition(xmlData, index) {
function getPositionFromMatch(match) { function getPositionFromMatch(match) {
return match.startIndex + match[1].length; return match.startIndex + match[1].length;
} }
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/fxp.js ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/fxp.js
@@ -59498,6 +59590,24 @@ const EXPANSION_MAX = 100_000;
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M // realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
// characters) so legitimate input is unaffected. // characters) so legitimate input is unaffected.
const EXPANSION_MAX_LENGTH = 4_000_000; const EXPANSION_MAX_LENGTH = 4_000_000;
// `expand_` recurses once per level of brace *nesting* - both when expanding a
// set's comma members and when re-wrapping a set whose body is a single part.
// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
// level per chained group), which left nesting depth unbounded: about 3,100
// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
// will follow nesting. It sits far above any realistic pattern and well below
// the depth at which the stack runs out.
const EXPANSION_MAX_DEPTH = 1_000;
// Bash keeps a quirk where a brace group followed by a comma set still expands
// (`{a},b}`). The parser implements it by rewriting the string and restarting
// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
// full passes over a string that itself grows by one `escClose` sentinel each
// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
// times the scan may restart. Real `{a},b}` input needs a handful.
const EXPANSION_MAX_REWRITES = 1_000;
function numeric(str) { function numeric(str) {
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
} }
@@ -59517,37 +59627,52 @@ function unescapeBraces(str) {
.replace(escCommaPattern, ',') .replace(escCommaPattern, ',')
.replace(escPeriodPattern, '.'); .replace(escPeriodPattern, '.');
} }
// Like `target.push(...items)` but doesn't overflow the stack
function pushAll(target, items) {
for (let i = 0; i < items.length; i++) {
target.push(items[i]);
}
}
/** /**
* Basically just str.split(","), but handling cases * Basically just str.split(","), but handling cases
* where we have nested braced sections, which should be * where we have nested braced sections, which should be
* treated as individual members, like {a,{b,c},d} * treated as individual members, like {a,{b,c},d}
*/ */
function parseCommaParts(str) { function parseCommaParts(str) {
if (!str) {
return [''];
}
const parts = []; const parts = [];
const m = balanced('{', '}', str); // Walk the brace groups iteratively. Recursing on `post` once per group let a
if (!m) { // chain of them exhaust the stack - the parsing-side counterpart to
return str.split(','); // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
// `maxLength` can bound, since it happens before expansion.
//
// The part the next chunk continues
let carry = '';
for (;;) {
const m = balanced('{', '}', str);
if (!m) {
const tail = str.split(',');
tail[0] = carry + tail[0];
pushAll(parts, tail);
return parts;
}
const { pre, body, post } = m;
const p = pre.split(',');
p[0] = carry + p[0];
p[p.length - 1] += '{' + body + '}';
if (!post.length) {
pushAll(parts, p);
return parts;
}
carry = p.pop();
pushAll(parts, p);
str = post;
} }
const { pre, body, post } = m;
const p = pre.split(',');
p[p.length - 1] += '{' + body + '}';
const postParts = parseCommaParts(post);
if (post.length) {
;
p[p.length - 1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
} }
function expand(str, options = {}) { function expand(str, options = {}) {
if (!str) { if (!str) {
return []; return [];
} }
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options; const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
// I don't know why Bash 4.3 does this, but it does. // I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved // Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything, // but *only* at the top level, so {},a}b will not expand to anything,
@@ -59557,7 +59682,7 @@ function expand(str, options = {}) {
if (str.slice(0, 2) === '{}') { if (str.slice(0, 2) === '{}') {
str = '\\{\\}' + str.slice(2); str = '\\{\\}' + str.slice(2);
} }
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces); return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
} }
function embrace(str) { function embrace(str) {
return '{' + str + '}'; return '{' + str + '}';
@@ -59652,7 +59777,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
} }
return N; return N;
} }
function expand_(str, max, maxLength, isTop) { function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
// Too deeply nested to keep following: treat the rest as literal, the same
// way a group that cannot expand is already handled. Truncating rather than
// throwing keeps `expand` total, matching `max` and `maxLength`.
if (depth > maxDepth) {
return [str];
}
// Consume the string's top-level brace groups left to right, threading a // Consume the string's top-level brace groups left to right, threading a
// running set of combined prefixes (`acc`). Expanding the tail iteratively - // running set of combined prefixes (`acc`). Expanding the tail iteratively -
// rather than recursing on `m.post` once per group - keeps the native stack // rather than recursing on `m.post` once per group - keeps the native stack
@@ -59664,6 +59795,9 @@ function expand_(str, max, maxLength, isTop) {
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
// is on the final strings, so it is applied to whichever `combine` produces // is on the final strings, so it is applied to whichever `combine` produces
// them (the one with no brace set left in the tail). // them (the one with no brace set left in the tail).
// How many times the `{a},b}` rewrite below has restarted the scan. Each pass
// re-reads the whole string, so leaving this unbounded is quadratic.
let rewrites = 0;
let dropEmpties = false; let dropEmpties = false;
let firstGroup = true; let firstGroup = true;
for (;;) { for (;;) {
@@ -59688,7 +59822,8 @@ function expand_(str, max, maxLength, isTop) {
const isOptions = m.body.indexOf(',') >= 0; const isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) { if (!isSequence && !isOptions) {
// {a},b} // {a},b}
if (m.post.match(/,(?!,).*\}/)) { if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
rewrites++;
str = m.pre + '{' + m.body + escClose + m.post; str = m.pre + '{' + m.body + escClose + m.post;
isTop = true; isTop = true;
continue; continue;
@@ -59708,7 +59843,7 @@ function expand_(str, max, maxLength, isTop) {
let n = parseCommaParts(m.body); let n = parseCommaParts(m.body);
if (n.length === 1 && n[0] !== undefined) { if (n.length === 1 && n[0] !== undefined) {
// x{{a,b}}y ==> x{a}y x{b}y // x{{a,b}}y ==> x{a}y x{b}y
n = expand_(n[0], max, maxLength, false).map(embrace); n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
//XXX is this necessary? Can't seem to hit it in tests. //XXX is this necessary? Can't seem to hit it in tests.
/* c8 ignore start */ /* c8 ignore start */
if (n.length === 1) { if (n.length === 1) {
@@ -59734,12 +59869,13 @@ function expand_(str, max, maxLength, isTop) {
values = []; values = [];
let valuesLength = 0; let valuesLength = 0;
outer: for (let j = 0; j < n.length; j++) { outer: for (let j = 0; j < n.length; j++) {
const expanded = expand_(n[j], max, maxLength, false); const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
for (let k = 0; k < expanded.length; k++) { for (let k = 0; k < expanded.length; k++) {
const v = expanded[k]; const v = expanded[k];
if (dropsEmpties && !v) if (dropsEmpties && !v)
continue; continue;
if (values.length >= max || valuesLength + v.length > maxLength) { if (values.length >= max ||
valuesLength + v.length > maxLength) {
break outer; break outer;
} }
values.push(v); values.push(v);
+474 -190
View File
@@ -6009,11 +6009,77 @@ class Request {
} }
} }
onUpgrade (statusCode, headers, socket) { /**
* @param {number|null} statusCode
* @param {Buffer[]|null} headers
* @param {import('node:stream').Duplex} socket
* @param {string} [statusText]
*/
onUpgrade (statusCode, headers, socket, statusText = '') {
this.onFinally()
assert(!this.aborted) assert(!this.aborted)
assert(!this.completed) assert(!this.completed)
return this[kHandler].onUpgrade(statusCode, headers, socket) if (statusCode !== null) {
this.#publishUpgradeHeaders(statusCode, headers, statusText)
}
const result = this[kHandler].onUpgrade(statusCode, headers, socket)
if (!this.aborted) {
this.completed = true
if (statusCode !== null) {
this.#publishUpgradeTrailers()
}
}
return result
}
/**
* @param {number} statusCode
* @param {import('node:http2').IncomingHttpHeaders} headers
* @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders
* @param {string} [statusText]
*/
onUpgradeResponse (statusCode, headers, parseHeaders, statusText = '') {
assert(!this.aborted)
assert(this.completed)
if (channels.headers.hasSubscribers) {
this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText)
}
this.#publishUpgradeTrailers()
}
/**
* @param {Error} error
*/
onUpgradeError (error) {
assert(!this.aborted)
assert(this.completed)
if (channels.error.hasSubscribers) {
channels.error.publish({ request: this, error })
}
}
/**
* @param {number} statusCode
* @param {Buffer[]} headers
* @param {string} statusText
*/
#publishUpgradeHeaders (statusCode, headers, statusText) {
if (channels.headers.hasSubscribers) {
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
}
}
#publishUpgradeTrailers () {
if (channels.trailers.hasSubscribers) {
channels.trailers.publish({ request: this, trailers: [] })
}
} }
onComplete (trailers) { onComplete (trailers) {
@@ -7912,7 +7978,7 @@ class Parser {
} }
onUpgrade (head) { onUpgrade (head) {
const { upgrade, client, socket, headers, statusCode } = this const { upgrade, client, socket, headers, statusCode, statusText } = this
assert(upgrade) assert(upgrade)
assert(client[kSocket] === socket) assert(client[kSocket] === socket)
@@ -7947,9 +8013,10 @@ class Parser {
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
try { try {
request.onUpgrade(statusCode, headers, socket) request.onUpgrade(statusCode, headers, socket, statusText)
} catch (err) { } catch (error) {
util.destroy(socket, err) util.errorRequest(client, request, error)
util.destroy(socket, error)
} }
client[kResume]() client[kResume]()
@@ -8356,7 +8423,7 @@ async function connectH1 (client, socket) {
function clearIdleSocketValidation (socket) { function clearIdleSocketValidation (socket) {
if (socket[kIdleSocketValidationTimeout]) { if (socket[kIdleSocketValidationTimeout]) {
clearTimeout(socket[kIdleSocketValidationTimeout]) clearImmediate(socket[kIdleSocketValidationTimeout])
socket[kIdleSocketValidationTimeout] = null socket[kIdleSocketValidationTimeout] = null
} }
@@ -8365,15 +8432,23 @@ function clearIdleSocketValidation (socket) {
function scheduleIdleSocketValidation (client, socket) { function scheduleIdleSocketValidation (client, socket) {
socket[kIdleSocketValidation] = 1 socket[kIdleSocketValidation] = 1
socket[kIdleSocketValidationTimeout] = setTimeout(() => { // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
// already pending on this idle keep-alive socket are processed before the
// next request is written (GHSA-35p6-xmwp-9g52).
//
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
// A ref'd Immediate both keeps the pending request alive and makes poll
// return immediately — the hybrid those issues asked for.
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
socket[kIdleSocketValidationTimeout] = null socket[kIdleSocketValidationTimeout] = null
socket[kIdleSocketValidation] = 2 socket[kIdleSocketValidation] = 2
if (client[kSocket] === socket && !socket.destroyed) { if (client[kSocket] === socket && !socket.destroyed) {
client[kResume]() client[kResume]()
} }
}, 0) })
socket[kIdleSocketValidationTimeout].unref?.()
} }
/** /**
@@ -8522,12 +8597,22 @@ function writeH1 (client, request) {
const socket = client[kSocket] const socket = client[kSocket]
clearIdleSocketValidation(socket) clearIdleSocketValidation(socket)
const abort = (err) => { /**
if (request.aborted || request.completed) { * @param {Error} [error]
*/
const abort = (error) => {
if (request.aborted) {
return return
} }
util.errorRequest(client, request, err || new RequestAbortedError()) if (request.completed) {
if (request.upgrade || request.method === 'CONNECT') {
util.destroy(socket, new InformationalError('aborted'))
}
return
}
util.errorRequest(client, request, error || new RequestAbortedError())
util.destroy(body) util.destroy(body)
util.destroy(socket, new InformationalError('aborted')) util.destroy(socket, new InformationalError('aborted'))
@@ -8984,6 +9069,7 @@ module.exports = connectH1
const assert = __nccwpck_require__(4589) const assert = __nccwpck_require__(4589)
const { errorMonitor } = __nccwpck_require__(8474)
const { pipeline } = __nccwpck_require__(7075) const { pipeline } = __nccwpck_require__(7075)
const util = __nccwpck_require__(3440) const util = __nccwpck_require__(3440)
const { const {
@@ -9060,6 +9146,15 @@ function parseH2Headers (headers) {
return result return result
} }
/**
* @param {import('node:http2').IncomingHttpHeaders} headers
* @returns {Buffer[]}
*/
function parseH2ResponseHeaders (headers) {
const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers
return parseH2Headers(realHeaders)
}
async function connectH2 (client, socket) { async function connectH2 (client, socket) {
client[kSocket] = socket client[kSocket] = socket
@@ -9280,22 +9375,32 @@ function writeH2 (client, request) {
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}` headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
headers[HTTP2_HEADER_METHOD] = method headers[HTTP2_HEADER_METHOD] = method
const abort = (err) => { /**
if (request.aborted || request.completed) { * @param {Error} [error]
*/
const abort = (error) => {
if (request.aborted) {
return return
} }
err = err || new RequestAbortedError() if (request.completed) {
if (method === 'CONNECT' && stream != null) {
util.destroy(stream, error || new RequestAbortedError())
}
return
}
util.errorRequest(client, request, err) error = error || new RequestAbortedError()
util.errorRequest(client, request, error)
if (stream != null) { if (stream != null) {
util.destroy(stream, err) util.destroy(stream, error)
} }
// We do not destroy the socket as we can continue using the session // We do not destroy the socket as we can continue using the session
// the stream get's destroyed and the session remains to create new streams // the stream get's destroyed and the session remains to create new streams
util.destroy(body, err) util.destroy(body, error)
client[kQueue][client[kRunningIdx]++] = null client[kQueue][client[kRunningIdx]++] = null
client[kResume]() client[kResume]()
} }
@@ -9314,25 +9419,57 @@ function writeH2 (client, request) {
if (method === 'CONNECT') { if (method === 'CONNECT') {
session.ref() session.ref()
// We are already connected, streams are pending, first request
// will create a new stream. We trigger a request to create the stream and wait until
// `ready` event is triggered
// We disabled endStream to allow the user to write to the stream // We disabled endStream to allow the user to write to the stream
stream = session.request(headers, { endStream: false, signal }) stream = session.request(headers, { endStream: false, signal })
let upgradeResponseFinished = false
if (stream.id && !stream.pending) { /**
request.onUpgrade(null, null, stream) * @param {import('node:http2').IncomingHttpHeaders} headers
++session[kOpenStreams] */
client[kQueue][client[kRunningIdx]++] = null const onResponse = (headers) => {
} else { upgradeResponseFinished = true
stream.once('ready', () => { stream.off(errorMonitor, onUpgradeError)
request.onUpgrade(null, null, stream) request.onUpgradeResponse(Number(headers[HTTP2_HEADER_STATUS]), headers, parseH2ResponseHeaders)
++session[kOpenStreams]
client[kQueue][client[kRunningIdx]++] = null
})
} }
/**
* @param {Error} error
*/
const onUpgradeError = (error) => {
upgradeResponseFinished = true
stream.off('response', onResponse)
request.onUpgradeError(error)
}
const onReady = () => {
try {
request.onUpgrade(null, null, stream)
} catch (error) {
stream.off('response', onResponse)
abort(error)
return
}
if (request.aborted) {
return
}
stream.off('error', abort)
stream.once(errorMonitor, onUpgradeError)
client[kQueue][client[kRunningIdx]++] = null
}
stream.once('response', onResponse)
stream.once('error', abort)
++session[kOpenStreams]
onReady()
stream.once('close', () => { stream.once('close', () => {
if (!upgradeResponseFinished && request.completed) {
stream.off('response', onResponse)
stream.off(errorMonitor, onUpgradeError)
request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
}
session[kOpenStreams] -= 1 session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) session.unref() if (session[kOpenStreams] === 0) session.unref()
}) })
@@ -12031,6 +12168,7 @@ class RetryHandler {
this.end = null this.end = null
this.etag = null this.etag = null
this.resume = null this.resume = null
this.headersSent = false
// Handle possible onConnect duplication // Handle possible onConnect duplication
this.handler.onConnect(reason => { this.handler.onConnect(reason => {
@@ -12043,6 +12181,20 @@ class RetryHandler {
}) })
} }
checkpointResponseEnd (headers, resume) {
if (this.end == null && this.opts.method !== 'HEAD') {
const contentLength = headers['content-length']
this.end = contentLength != null ? Number(contentLength) - 1 : null
assert(
this.end == null || Number.isFinite(this.end),
'invalid content-length'
)
}
this.resume = this.end != null ? resume : null
}
onRequestSent () { onRequestSent () {
if (this.handler.onRequestSent) { if (this.handler.onRequestSent) {
this.handler.onRequestSent() this.handler.onRequestSent()
@@ -12131,7 +12283,12 @@ class RetryHandler {
this.retryCount += 1 this.retryCount += 1
if (statusCode >= 300) { if (statusCode >= 300) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) { // Only expose a response if no earlier attempt has reached the caller.
// Otherwise abort this attempt so the error settles the existing body
// instead of replacing it with a new response.
if (!this.headersSent && this.retryOpts.statusCodes.includes(statusCode) === false) {
this.headersSent = true
this.checkpointResponseEnd(headers, resume)
return this.handler.onHeaders( return this.handler.onHeaders(
statusCode, statusCode,
rawHeaders, rawHeaders,
@@ -12200,8 +12357,15 @@ class RetryHandler {
const { start, size, end = size - 1 } = contentRange const { start, size, end = size - 1 } = contentRange
assert(this.start === start, 'content-range mismatch') if (this.start !== start || (this.end != null && this.end !== end)) {
assert(this.end == null || this.end === end, 'content-range mismatch') this.abort(
new RequestRetryError('Content-Range mismatch', statusCode, {
headers,
data: { count: this.retryCount }
})
)
return false
}
this.resume = resume this.resume = resume
return true return true
@@ -12213,6 +12377,7 @@ class RetryHandler {
const range = parseRangeHeader(headers['content-range']) const range = parseRangeHeader(headers['content-range'])
if (range == null) { if (range == null) {
this.headersSent = true
return this.handler.onHeaders( return this.handler.onHeaders(
statusCode, statusCode,
rawHeaders, rawHeaders,
@@ -12251,6 +12416,7 @@ class RetryHandler {
) )
this.resume = resume this.resume = resume
this.headersSent = true
this.etag = headers.etag != null ? headers.etag : null this.etag = headers.etag != null ? headers.etag : null
// Weak etags are not useful for comparison nor cache // Weak etags are not useful for comparison nor cache
@@ -12290,7 +12456,7 @@ class RetryHandler {
} }
onError (err) { onError (err) {
if (this.aborted || isDisturbed(this.opts.body)) { if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
return this.handler.onError(err) return this.handler.onError(err)
} }
@@ -16748,6 +16914,49 @@ const COLON = 0x3A
*/ */
const SPACE = 0x20 const SPACE = 0x20
const DATA = Buffer.from('data')
const EVENT = Buffer.from('event')
const ID = Buffer.from('id')
const RETRY = Buffer.from('retry')
function isASCIINumberBytes (buffer, start) {
if (start >= buffer.length) {
return false
}
for (let i = start; i < buffer.length; i++) {
if (buffer[i] < 0x30 || buffer[i] > 0x39) {
return false
}
}
return true
}
function isValidLastEventIdBytes (buffer, start) {
for (let i = start; i < buffer.length; i++) {
if (buffer[i] === 0x00) {
return false
}
}
return true
}
function isFieldName (line, length, field) {
if (length !== field.length) {
return false
}
for (let i = 0; i < length; i++) {
if (line[i] !== field[i]) {
return false
}
}
return true
}
/** /**
* @typedef {object} EventSourceStreamEvent * @typedef {object} EventSourceStreamEvent
* @type {object} * @type {object}
@@ -16788,11 +16997,14 @@ class EventSourceStream extends Transform {
eventEndCheck = false eventEndCheck = false
/** /**
* @type {Buffer} * @type {Buffer[]}
*/ */
buffer = null chunks = []
chunkIndex = 0
pos = 0 pos = 0
lineChunkIndex = 0
linePos = 0
event = { event = {
data: undefined, data: undefined,
@@ -16831,92 +17043,20 @@ class EventSourceStream extends Transform {
return return
} }
// Cache the chunk in the buffer, as the data might not be complete while this.chunks.push(chunk)
// processing it
// TODO: Investigate if there is a more performant way to handle
// incoming chunks
// see: https://github.com/nodejs/undici/issues/2630
if (this.buffer) {
this.buffer = Buffer.concat([this.buffer, chunk])
} else {
this.buffer = chunk
}
// Strip leading byte-order-mark if we opened the stream and started // Strip leading byte-order-mark if we opened the stream and started
// the processing of the incoming data // the processing of the incoming data
if (this.checkBOM) { if (this.checkBOM) {
switch (this.buffer.length) { if (this.handleBOM()) {
case 1: callback()
// Check if the first byte is the same as the first byte of the BOM return
if (this.buffer[0] === BOM[0]) {
// If it is, we need to wait for more data
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// The buffer only contains one byte so we need to wait for more data
callback()
return
case 2:
// Check if the first two bytes are the same as the first two bytes
// of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1]
) {
// If it is, we need to wait for more data, because the third byte
// is needed to determine if it is the BOM or not
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
break
case 3:
// Check if the first three bytes are the same as the first three
// bytes of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// If it is, we can drop the buffered data, as it is only the BOM
this.buffer = Buffer.alloc(0)
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// Await more data
callback()
return
}
// If it is not the BOM, we can start processing the data
this.checkBOM = false
break
default:
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
// present
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// Remove the BOM from the buffer
this.buffer = this.buffer.subarray(3)
}
// Set the checkBOM flag to false as we don't need to check for the
this.checkBOM = false
break
} }
} }
while (this.pos < this.buffer.length) { while (this.hasCurrentByte()) {
const byte = this.currentByte()
// If the previous line ended with an end-of-line, we need to check // If the previous line ended with an end-of-line, we need to check
// if the next character is also an end-of-line. // if the next character is also an end-of-line.
if (this.eventEndCheck) { if (this.eventEndCheck) {
@@ -16929,10 +17069,9 @@ class EventSourceStream extends Transform {
if (this.crlfCheck) { if (this.crlfCheck) {
// If the current character is a line feed, we can remove it // If the current character is a line feed, we can remove it
// from the buffer and reset the crlfCheck flag // from the buffer and reset the crlfCheck flag
if (this.buffer[this.pos] === LF) { if (byte === LF) {
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
this.crlfCheck = false this.crlfCheck = false
this.consumeCurrentByte()
// It is possible that the line feed is not the end of the // It is possible that the line feed is not the end of the
// event. We need to check if the next character is an // event. We need to check if the next character is an
@@ -16948,19 +17087,17 @@ class EventSourceStream extends Transform {
this.crlfCheck = false this.crlfCheck = false
} }
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { if (byte === LF || byte === CR) {
// If the current character is a carriage return, we need to // If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the // set the crlfCheck flag to true, as we need to check if the
// next character is a line feed so we can remove it from the // next character is a line feed so we can remove it from the
// buffer // buffer
if (this.buffer[this.pos] === CR) { if (byte === CR) {
this.crlfCheck = true this.crlfCheck = true
} }
this.buffer = this.buffer.subarray(this.pos + 1) this.consumeCurrentByte()
this.pos = 0 if (this.hasPendingEvent()) {
if (
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
this.processEvent(this.event) this.processEvent(this.event)
} }
this.clearEvent() this.clearEvent()
@@ -16974,22 +17111,18 @@ class EventSourceStream extends Transform {
// If the current character is an end-of-line, we can process the // If the current character is an end-of-line, we can process the
// line // line
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { if (byte === LF || byte === CR) {
// If the current character is a carriage return, we need to // If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the // set the crlfCheck flag to true, as we need to check if the
// next character is a line feed // next character is a line feed
if (this.buffer[this.pos] === CR) { if (byte === CR) {
this.crlfCheck = true this.crlfCheck = true
} }
// In any case, we can process the line as we reached an // In any case, we can process the line as we reached an
// end-of-line character // end-of-line character
this.parseLine(this.buffer.subarray(0, this.pos), this.event) this.parseLine(this.readLine(), this.event)
this.consumeCurrentByte()
// Remove the processed line from the buffer
this.buffer = this.buffer.subarray(this.pos + 1)
// Reset the position as we removed the processed line from the buffer
this.pos = 0
// A line was processed and this could be the end of the event. We need // A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is // to check if the next line is empty to determine if the event is
// finished. // finished.
@@ -16997,7 +17130,7 @@ class EventSourceStream extends Transform {
continue continue
} }
this.pos++ this.advanceCursor()
} }
callback() callback()
@@ -17022,64 +17155,53 @@ class EventSourceStream extends Transform {
return return
} }
let field = '' let fieldLength = line.length
let value = '' let valueStart = line.length
// If the line contains a U+003A COLON character (:) // If the line contains a U+003A COLON character (:)
if (colonPosition !== -1) { if (colonPosition !== -1) {
// Collect the characters on the line before the first U+003A COLON fieldLength = colonPosition
// character (:), and let field be that string.
// TODO: Investigate if there is a more performant way to extract the
// field
// see: https://github.com/nodejs/undici/issues/2630
field = line.subarray(0, colonPosition).toString('utf8')
// Collect the characters on the line after the first U+003A COLON // Collect the characters on the line after the first U+003A COLON
// character (:), and let value be that string. // character (:), and let value be that string.
// If value starts with a U+0020 SPACE character, remove it from value. // If value starts with a U+0020 SPACE character, remove it from value.
let valueStart = colonPosition + 1 valueStart = colonPosition + 1
if (line[valueStart] === SPACE) { if (line[valueStart] === SPACE) {
++valueStart ++valueStart
} }
// TODO: Investigate if there is a more performant way to extract the
// value
// see: https://github.com/nodejs/undici/issues/2630
value = line.subarray(valueStart).toString('utf8')
// Otherwise, the string is not empty but does not contain a U+003A COLON
// character (:)
} else {
// Process the field using the steps described below, using the whole
// line as the field name, and the empty string as the field value.
field = line.toString('utf8')
value = ''
} }
// Modify the event with the field name and value. The value is also if (isFieldName(line, fieldLength, DATA)) {
// decoded as UTF-8 const value = line.toString('utf8', valueStart)
switch (field) {
case 'data': if (event.data === undefined) {
if (event[field] === undefined) { event.data = value
event[field] = value } else {
} else { event.data += `\n${value}`
event[field] += `\n${value}` }
} return
break }
case 'retry':
if (isASCIINumber(value)) { if (isFieldName(line, fieldLength, RETRY)) {
event[field] = value if (isASCIINumberBytes(line, valueStart)) {
} event.retry = line.toString('utf8', valueStart)
break }
case 'id': return
if (isValidLastEventId(value)) { }
event[field] = value
} if (isFieldName(line, fieldLength, ID)) {
break if (isValidLastEventIdBytes(line, valueStart)) {
case 'event': event.id = line.toString('utf8', valueStart)
if (value.length > 0) { }
event[field] = value return
} }
break
if (isFieldName(line, fieldLength, EVENT)) {
const value = line.toString('utf8', valueStart)
if (value.length > 0) {
event.event = value
}
} }
} }
@@ -17109,13 +17231,152 @@ class EventSourceStream extends Transform {
} }
clearEvent () { clearEvent () {
this.event = { this.event.data = undefined
data: undefined, this.event.event = undefined
event: undefined, this.event.id = undefined
id: undefined, this.event.retry = undefined
retry: undefined }
hasPendingEvent () {
return this.event.data !== undefined ||
this.event.event !== undefined ||
this.event.id !== undefined ||
this.event.retry !== undefined
}
hasCurrentByte () {
return this.chunkIndex < this.chunks.length &&
this.pos < this.chunks[this.chunkIndex].length
}
currentByte () {
return this.chunks[this.chunkIndex][this.pos]
}
consumeCurrentByte () {
this.advanceCursor()
this.syncLineStartToCursor()
}
advanceCursor () {
this.pos++
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
this.chunkIndex++
this.pos = 0
} }
} }
syncLineStartToCursor () {
this.lineChunkIndex = this.chunkIndex
this.linePos = this.pos
this.dropConsumedChunks()
}
dropConsumedChunks () {
while (this.lineChunkIndex > 0) {
this.chunks.shift()
this.lineChunkIndex--
this.chunkIndex--
}
if (this.chunkIndex === this.chunks.length) {
this.chunks.length = 0
this.chunkIndex = 0
this.pos = 0
this.lineChunkIndex = 0
this.linePos = 0
}
}
readLine () {
if (this.lineChunkIndex === this.chunkIndex) {
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
}
const chunks = []
let length = 0
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
const chunk = this.chunks[i]
const start = i === this.lineChunkIndex ? this.linePos : 0
const end = i === this.chunkIndex ? this.pos : chunk.length
const slice = chunk.subarray(start, end)
length += slice.length
chunks.push(slice)
}
return Buffer.concat(chunks, length)
}
peekBufferedByte (offset) {
let chunkIndex = this.lineChunkIndex
let pos = this.linePos
while (chunkIndex < this.chunks.length) {
const chunk = this.chunks[chunkIndex]
const remaining = chunk.length - pos
if (offset < remaining) {
return chunk[pos + offset]
}
offset -= remaining
chunkIndex++
pos = 0
}
}
discardLeadingBytes (count) {
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
const chunk = this.chunks[this.lineChunkIndex]
const remaining = chunk.length - this.linePos
if (count < remaining) {
this.linePos += count
count = 0
} else {
count -= remaining
this.lineChunkIndex++
this.linePos = 0
}
}
this.chunkIndex = this.lineChunkIndex
this.pos = this.linePos
this.dropConsumedChunks()
}
handleBOM () {
const first = this.peekBufferedByte(0)
const second = this.peekBufferedByte(1)
const third = this.peekBufferedByte(2)
if (second === undefined) {
if (first === BOM[0]) {
return true
}
this.checkBOM = false
return true
}
if (third === undefined) {
if (first === BOM[0] && second === BOM[1]) {
return true
}
this.checkBOM = false
return false
}
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
this.discardLeadingBytes(3)
}
this.checkBOM = false
return !this.hasCurrentByte()
}
} }
module.exports = { module.exports = {
@@ -28383,7 +28644,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// is specified, the server needs to include the same field and one of // is specified, the server needs to include the same field and one of
// the selected subprotocol values in its response for the connection to // the selected subprotocol values in its response for the connection to
// be established. // be established.
if (!requestProtocols.includes(secProtocol)) { if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
return return
} }
@@ -29144,7 +29405,12 @@ class PerMessageDeflate {
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
callback(new MessageSizeExceededError()) callback(new MessageSizeExceededError())
// The inflater may still hold buffered input that can emit a late
// zlib error. Remove the data listener, then deterministically stop
// the stream so a subsequent 'error' cannot fire without a listener
// (which would terminate the process as an unhandled error event).
this.#inflate.removeAllListeners() this.#inflate.removeAllListeners()
this.#inflate.destroy()
this.#inflate = null this.#inflate = null
return return
} }
@@ -30848,7 +31114,7 @@ const DISTRIBUTIONS_ONLY_MAJOR_VERSION = (/* unused pure expression or super */
/* harmony export */ Vt: () => (/* binding */ getBooleanInput), /* harmony export */ Vt: () => (/* binding */ getBooleanInput),
/* harmony export */ lN: () => (/* binding */ isJdkCacheEnabled) /* harmony export */ lN: () => (/* binding */ isJdkCacheEnabled)
/* harmony export */ }); /* harmony export */ });
/* unused harmony exports getVersionFromToolcachePath, extractJdkFile, cacheJdkDir, getJavaVersionFromReleaseFile, getDownloadArchiveExtension, isVersionSatisfies, getToolcachePath, isGhes, getVersionFromFileContent, convertVersionToSemver, getArtifactFingerprint, getGitHubToken, getGitHubHttpHeaders, MAX_PAGINATION_PAGES, getNextPageUrlFromLinkHeader, validatePaginationUrl, renameWinArchive, getLatestMajorVersion */ /* unused harmony exports getVersionFromToolcachePath, extractJdkFile, cacheJdkDir, getJavaVersionFromReleaseFile, getDownloadArchiveExtension, isVersionSatisfies, getToolcachePath, isGhes, getVersionFromFileContent, convertVersionToSemver, normalizeJavaVersionToSemver, getArtifactFingerprint, getGitHubToken, getGitHubHttpHeaders, MAX_PAGINATION_PAGES, getNextPageUrlFromLinkHeader, validatePaginationUrl, renameWinArchive, getLatestMajorVersion */
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(857); /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(857);
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__nccwpck_require__.n(os__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__nccwpck_require__.n(os__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(6928);
@@ -31247,6 +31513,20 @@ function convertVersionToSemver(version) {
} }
return mainVersion; return mainVersion;
} }
/**
* Java versions (JEP 322) can contain more numeric fields than SemVer allows,
* e.g. '11.0.9.1' or Temurin respins such as '26.0.2.1+1'. Move the extra
* fields into SemVer build metadata ('11.0.9+1', '26.0.2+1.1'). Any other
* input (ranges, regular SemVer versions) is returned unchanged.
*/
function normalizeJavaVersionToSemver(version) {
const match = /^(\d+(?:\.\d+){3,})(?:\+([0-9A-Za-z.-]+))?$/.exec(version);
if (!match) {
return version;
}
const converted = convertVersionToSemver(match[1]);
return match[2] ? `${converted}.${match[2]}` : converted;
}
/** /**
* Builds a validator for the bytes currently served by a URL from the response * Builds a validator for the bytes currently served by a URL from the response
* headers of a HEAD request. A vendor's `/latest/` URL never changes, so this * headers of a HEAD request. A vendor's `/latest/` URL never changes, so this
@@ -35832,13 +36112,16 @@ async function removeGpgHome(gpgHome) {
if (!external_fs_.existsSync(resolvedGpgHome)) { if (!external_fs_.existsSync(resolvedGpgHome)) {
return; return;
} }
await stopGpgAgent(resolvedGpgHome);
await lib_io/* rmRF */.Yz(resolvedGpgHome);
}
async function stopGpgAgent(gpgHome) {
try { try {
await lib_exec/* exec */.m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); await lib_exec/* exec */.m('gpgconf', ['--homedir', toGpgPath(gpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true });
} }
catch { catch {
// gpgconf may be unavailable, but directory removal must still be attempted. // gpgconf may be unavailable, but directory removal must still be attempted.
} }
await lib_io/* rmRF */.Yz(resolvedGpgHome);
} }
async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) {
const signaturePath = await tc.downloadTool(signatureUrl); const signaturePath = await tc.downloadTool(signatureUrl);
@@ -35884,6 +36167,7 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten
], options); ], options);
} }
finally { finally {
await stopGpgAgent(gpgHome);
await io.rmRF(signaturePath); await io.rmRF(signaturePath);
await io.rmRF(gpgHome); await io.rmRF(gpgHome);
} }
+6 -2
View File
@@ -263,13 +263,16 @@ async function removeGpgHome(gpgHome) {
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) { if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) {
return; return;
} }
await stopGpgAgent(resolvedGpgHome);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome);
}
async function stopGpgAgent(gpgHome) {
try { try {
await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(gpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true });
} }
catch { catch {
// gpgconf may be unavailable, but directory removal must still be attempted. // gpgconf may be unavailable, but directory removal must still be attempted.
} }
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome);
} }
async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) {
const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl); const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl);
@@ -315,6 +318,7 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten
], options); ], options);
} }
finally { finally {
await stopGpgAgent(gpgHome);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome); await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome);
} }
+3 -6
View File
@@ -730,12 +730,9 @@ class JavaBase {
} }
// Java uses a versioning scheme (JEP 322) that can contain more numeric // Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such // fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are // exact versions to SemVer build notation ('18.0.1+1', or '26.0.2+1.1'
// accepted. Ranges and versions that already carry build metadata are // for '26.0.2.1+1') so they are accepted. Ranges are left untouched.
// left untouched. version = (0,util/* normalizeJavaVersionToSemver */.zZ)(version);
if (/^\d+(\.\d+){3,}$/.test(version)) {
version = (0,util/* convertVersionToSemver */.ZY)(version);
}
if (!semver_default().validRange(version)) { if (!semver_default().validRange(version)) {
throw new Error(`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`); throw new Error(`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`);
} }
+37 -5
View File
@@ -109,7 +109,7 @@ class TemurinDistribution extends base_installer/* JavaBase */.O {
const formattedVersion = this.stable const formattedVersion = this.stable
? item.version_data.semver ? item.version_data.semver
: item.version_data.semver.replace('-beta+', '+'); : item.version_data.semver.replace('-beta+', '+');
return { const release = {
version: formattedVersion, version: formattedVersion,
url: item.binaries[0].package.link, url: item.binaries[0].package.link,
signatureUrl: item.binaries[0].package.signature_link, signatureUrl: item.binaries[0].package.signature_link,
@@ -119,15 +119,29 @@ class TemurinDistribution extends base_installer/* JavaBase */.O {
source: item.binaries[0].package.checksum_link source: item.binaries[0].package.checksum_link
} }
}; };
return {
release,
openjdkVersion: getOpenJdkSemverVersion(item.version_data)
};
}); });
// The Adoptium API `semver` folds the JEP 322 patch field into the build
// number ('26.0.2.1+1' -> '26.0.2+101') and appends extra metadata for LTS
// releases ('25.0.4+7' -> '25.0.4+7.0.LTS'). Exact versions requested by
// users follow the OpenJDK notation instead, so also match them against a
// key derived from the OpenJDK version fields ('26.0.2+1.1', '25.0.4+7').
const isExactBuildRequest = (semver_default().parse(version)?.build.length ?? 0) > 0;
const satisfiedVersions = availableVersionsWithBinaries const satisfiedVersions = availableVersionsWithBinaries
.filter(item => (0,util/* isVersionSatisfies */.y)(version, item.version)) .filter(({ release, openjdkVersion }) => (0,util/* isVersionSatisfies */.y)(version, release.version) ||
(isExactBuildRequest &&
openjdkVersion !== null &&
semver_default().compareBuild(version, openjdkVersion) === 0))
.map(({ release }) => release)
.sort((a, b) => { .sort((a, b) => {
return -semver_default().compareBuild(a.version, b.version); return -semver_default().compareBuild(a.version, b.version);
}); });
const resolvedFullVersion = satisfiedVersions.length > 0 ? satisfiedVersions[0] : null; const resolvedFullVersion = satisfiedVersions.length > 0 ? satisfiedVersions[0] : null;
if (!resolvedFullVersion) { if (!resolvedFullVersion) {
const availableVersionStrings = availableVersionsWithBinaries.map(item => item.version); const availableVersionStrings = availableVersionsWithBinaries.map(({ release }) => release.version);
throw this.createVersionNotFoundError(version, availableVersionStrings); throw this.createVersionNotFoundError(version, availableVersionStrings);
} }
return resolvedFullVersion; return resolvedFullVersion;
@@ -279,6 +293,20 @@ class TemurinDistribution extends base_installer/* JavaBase */.O {
return architecture === 'armv7' ? 'arm' : architecture; return architecture === 'armv7' ? 'arm' : architecture;
} }
} }
/**
* Builds a SemVer version from the OpenJDK version fields reported by the
* Adoptium API, e.g. '26.0.2.1+1' -> '26.0.2+1.1' and '25.0.4+7-LTS' ->
* '25.0.4+7'. Returns null if the fields cannot form a valid SemVer version.
*/
function getOpenJdkSemverVersion(versionData) {
const { major, minor, security, patch, build } = versionData;
if (build === undefined || build === null) {
return null;
}
const buildMetadata = patch ? `${patch}.${build}` : `${build}`;
const version = `${major}.${minor}.${security}+${buildMetadata}`;
return semver_default().valid(version) ? version : null;
}
/***/ }), /***/ }),
@@ -375,13 +403,16 @@ async function removeGpgHome(gpgHome) {
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) { if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) {
return; return;
} }
await stopGpgAgent(resolvedGpgHome);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome);
}
async function stopGpgAgent(gpgHome) {
try { try {
await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(gpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true });
} }
catch { catch {
// gpgconf may be unavailable, but directory removal must still be attempted. // gpgconf may be unavailable, but directory removal must still be attempted.
} }
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome);
} }
async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) {
const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl); const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl);
@@ -427,6 +458,7 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten
], options); ], options);
} }
finally { finally {
await stopGpgAgent(gpgHome);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome); await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome);
} }
+2 -1
View File
@@ -128,7 +128,8 @@ class LibericaDistributions extends _base_installer_js__WEBPACK_IMPORTED_MODULE_
} }
} }
convertVersionToSemver(version) { convertVersionToSemver(version) {
const { buildVersion, featureVersion, interimVersion, updateVersion } = version; const { featureVersion, interimVersion, updateVersion } = version;
const buildVersion = version.version.split('+')[1] || version.buildVersion;
const mainVersion = [featureVersion, interimVersion, updateVersion].join('.'); const mainVersion = [featureVersion, interimVersion, updateVersion].join('.');
if (buildVersion != 0) { if (buildVersion != 0) {
return `${mainVersion}+${buildVersion}`; return `${mainVersion}+${buildVersion}`;
+6 -2
View File
@@ -350,13 +350,16 @@ async function removeGpgHome(gpgHome) {
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) { if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) {
return; return;
} }
await stopGpgAgent(resolvedGpgHome);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome);
}
async function stopGpgAgent(gpgHome) {
try { try {
await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(gpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true });
} }
catch { catch {
// gpgconf may be unavailable, but directory removal must still be attempted. // gpgconf may be unavailable, but directory removal must still be attempted.
} }
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome);
} }
async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) {
const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl); const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl);
@@ -402,6 +405,7 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten
], options); ], options);
} }
finally { finally {
await stopGpgAgent(gpgHome);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath);
await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome); await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome);
} }
+96 -4
View File
@@ -418,9 +418,102 @@ function readAttributeStr(xmlData, i) {
} }
/** /**
* Select all the attributes whether valid or invalid. * Walk `attrStr` once, left to right, splitting it into attribute tokens.
*
* This replaces a regex that used to do the same job
* (`(\s*)([^\s=]+)(\s*=)?(\s*(['"])(([\s\S])*?)\5)?`). That regex led with an
* optional whitespace group followed by a required "non-whitespace" group.
* On a long run of whitespace that never resolves into an attribute name
* (e.g. a tag with thousands of trailing spaces before `>`), the engine
* backtracks the whitespace group one character at a time before giving up
* and moving to the next starting position — one full backtrack per
* position, which is quadratic in the length of the run.
*
* A single forward-only scan can never backtrack, so it can't be made slow
* this way no matter how much whitespace the input contains — it's always
* proportional to the length of the string, once.
*
* Each returned token mirrors the shape the old regex match array had, so
* the validation logic below (which reads token[1]..token[6]) didn't need
* to change:
* token.startIndex - where this token begins in attrStr
* token[1] - leading whitespace before the name
* token[2] - the attribute name
* token[3] - whitespace + '=' if present, else undefined
* token[4] - marker (any defined value) if a quoted value was found
* token[5] - the quote character used ('"' or "'")
* token[6] - the value's text, without the surrounding quotes
*
* A malformed leading character (e.g. a stray '=' with no name before it)
* is simply skipped over, one character at a time — the same outcome the
* old regex produced by failing to match at that position and retrying at
* the next one.
*/ */
const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g'); function scanAttributeTokens(attrStr) {
const tokens = [];
const len = attrStr.length;
let i = 0;
while (i < len) {
const tokenStart = i;
// Leading whitespace before the name.
while (i < len && isWhiteSpace(attrStr[i])) i++;
if (i >= len) break; // trailing whitespace only — nothing left to read
if (attrStr[i] === '=') {
// No name before this '=' — not a valid attribute start. Move past
// just this one character and try again from the next position.
i = tokenStart + 1;
continue;
}
const leadingWs = attrStr.slice(tokenStart, i);
// Attribute name — everything up to the next whitespace or '='.
const nameStart = i;
while (i < len && !isWhiteSpace(attrStr[i]) && attrStr[i] !== '=') i++;
const name = attrStr.slice(nameStart, i);
// Optional whitespace + '='.
let equalsGroup; // whitespace + '=' text, or undefined if absent
let j = i;
while (j < len && isWhiteSpace(attrStr[j])) j++;
if (j < len && attrStr[j] === '=') {
equalsGroup = attrStr.slice(i, j + 1);
i = j + 1;
}
// Optional whitespace + quoted value.
let quoteChar;
let value;
let k = i;
while (k < len && isWhiteSpace(attrStr[k])) k++;
if (k < len && (attrStr[k] === '"' || attrStr[k] === "'")) {
const valueStart = k + 1;
const closeIdx = attrStr.indexOf(attrStr[k], valueStart);
if (closeIdx !== -1) {
quoteChar = attrStr[k];
value = attrStr.slice(valueStart, closeIdx);
i = closeIdx + 1;
}
// No closing quote found anywhere in the rest of the string — leave
// quoteChar/value undefined, same as the old regex's group failing
// to match a backreference-less run.
}
const token = { startIndex: tokenStart };
token[1] = leadingWs;
token[2] = name;
token[3] = equalsGroup;
token[4] = quoteChar !== undefined ? true : undefined;
token[5] = quoteChar;
token[6] = value;
tokens.push(token);
}
return tokens;
}
//attr, ="sd", a="amit's", a="sd"b="saf", ab cd="" //attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
@@ -429,7 +522,7 @@ function validateAttributeString(attrStr, options) {
//if(attrStr.trim().length === 0) return true; //empty string //if(attrStr.trim().length === 0) return true; //empty string
const matches = (0,_util_js__WEBPACK_IMPORTED_MODULE_0__/* .getAllMatches */ .Xe)(attrStr, validAttrStrRegxp); const matches = scanAttributeTokens(attrStr);
const attrNames = {}; const attrNames = {};
for (let i = 0; i < matches.length; i++) { for (let i = 0; i < matches.length; i++) {
@@ -532,7 +625,6 @@ function getPositionFromMatch(match) {
return match.startIndex + match[1].length; return match.startIndex + match[1].length;
} }
/***/ }), /***/ }),
/***/ 6009: /***/ 6009:
+68 -24
View File
@@ -52431,6 +52431,24 @@ const EXPANSION_MAX = 100_000;
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M // realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
// characters) so legitimate input is unaffected. // characters) so legitimate input is unaffected.
const EXPANSION_MAX_LENGTH = 4_000_000; const EXPANSION_MAX_LENGTH = 4_000_000;
// `expand_` recurses once per level of brace *nesting* - both when expanding a
// set's comma members and when re-wrapping a set whose body is a single part.
// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
// level per chained group), which left nesting depth unbounded: about 3,100
// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
// will follow nesting. It sits far above any realistic pattern and well below
// the depth at which the stack runs out.
const EXPANSION_MAX_DEPTH = 1_000;
// Bash keeps a quirk where a brace group followed by a comma set still expands
// (`{a},b}`). The parser implements it by rewriting the string and restarting
// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
// full passes over a string that itself grows by one `escClose` sentinel each
// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
// times the scan may restart. Real `{a},b}` input needs a handful.
const EXPANSION_MAX_REWRITES = 1_000;
function numeric(str) { function numeric(str) {
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
} }
@@ -52450,37 +52468,52 @@ function unescapeBraces(str) {
.replace(escCommaPattern, ',') .replace(escCommaPattern, ',')
.replace(escPeriodPattern, '.'); .replace(escPeriodPattern, '.');
} }
// Like `target.push(...items)` but doesn't overflow the stack
function pushAll(target, items) {
for (let i = 0; i < items.length; i++) {
target.push(items[i]);
}
}
/** /**
* Basically just str.split(","), but handling cases * Basically just str.split(","), but handling cases
* where we have nested braced sections, which should be * where we have nested braced sections, which should be
* treated as individual members, like {a,{b,c},d} * treated as individual members, like {a,{b,c},d}
*/ */
function parseCommaParts(str) { function parseCommaParts(str) {
if (!str) {
return [''];
}
const parts = []; const parts = [];
const m = balanced('{', '}', str); // Walk the brace groups iteratively. Recursing on `post` once per group let a
if (!m) { // chain of them exhaust the stack - the parsing-side counterpart to
return str.split(','); // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
// `maxLength` can bound, since it happens before expansion.
//
// The part the next chunk continues
let carry = '';
for (;;) {
const m = balanced('{', '}', str);
if (!m) {
const tail = str.split(',');
tail[0] = carry + tail[0];
pushAll(parts, tail);
return parts;
}
const { pre, body, post } = m;
const p = pre.split(',');
p[0] = carry + p[0];
p[p.length - 1] += '{' + body + '}';
if (!post.length) {
pushAll(parts, p);
return parts;
}
carry = p.pop();
pushAll(parts, p);
str = post;
} }
const { pre, body, post } = m;
const p = pre.split(',');
p[p.length - 1] += '{' + body + '}';
const postParts = parseCommaParts(post);
if (post.length) {
;
p[p.length - 1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
} }
function expand(str, options = {}) { function expand(str, options = {}) {
if (!str) { if (!str) {
return []; return [];
} }
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options; const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
// I don't know why Bash 4.3 does this, but it does. // I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved // Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything, // but *only* at the top level, so {},a}b will not expand to anything,
@@ -52490,7 +52523,7 @@ function expand(str, options = {}) {
if (str.slice(0, 2) === '{}') { if (str.slice(0, 2) === '{}') {
str = '\\{\\}' + str.slice(2); str = '\\{\\}' + str.slice(2);
} }
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces); return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
} }
function embrace(str) { function embrace(str) {
return '{' + str + '}'; return '{' + str + '}';
@@ -52585,7 +52618,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
} }
return N; return N;
} }
function expand_(str, max, maxLength, isTop) { function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
// Too deeply nested to keep following: treat the rest as literal, the same
// way a group that cannot expand is already handled. Truncating rather than
// throwing keeps `expand` total, matching `max` and `maxLength`.
if (depth > maxDepth) {
return [str];
}
// Consume the string's top-level brace groups left to right, threading a // Consume the string's top-level brace groups left to right, threading a
// running set of combined prefixes (`acc`). Expanding the tail iteratively - // running set of combined prefixes (`acc`). Expanding the tail iteratively -
// rather than recursing on `m.post` once per group - keeps the native stack // rather than recursing on `m.post` once per group - keeps the native stack
@@ -52597,6 +52636,9 @@ function expand_(str, max, maxLength, isTop) {
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
// is on the final strings, so it is applied to whichever `combine` produces // is on the final strings, so it is applied to whichever `combine` produces
// them (the one with no brace set left in the tail). // them (the one with no brace set left in the tail).
// How many times the `{a},b}` rewrite below has restarted the scan. Each pass
// re-reads the whole string, so leaving this unbounded is quadratic.
let rewrites = 0;
let dropEmpties = false; let dropEmpties = false;
let firstGroup = true; let firstGroup = true;
for (;;) { for (;;) {
@@ -52621,7 +52663,8 @@ function expand_(str, max, maxLength, isTop) {
const isOptions = m.body.indexOf(',') >= 0; const isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) { if (!isSequence && !isOptions) {
// {a},b} // {a},b}
if (m.post.match(/,(?!,).*\}/)) { if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
rewrites++;
str = m.pre + '{' + m.body + escClose + m.post; str = m.pre + '{' + m.body + escClose + m.post;
isTop = true; isTop = true;
continue; continue;
@@ -52641,7 +52684,7 @@ function expand_(str, max, maxLength, isTop) {
let n = parseCommaParts(m.body); let n = parseCommaParts(m.body);
if (n.length === 1 && n[0] !== undefined) { if (n.length === 1 && n[0] !== undefined) {
// x{{a,b}}y ==> x{a}y x{b}y // x{{a,b}}y ==> x{a}y x{b}y
n = expand_(n[0], max, maxLength, false).map(embrace); n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
//XXX is this necessary? Can't seem to hit it in tests. //XXX is this necessary? Can't seem to hit it in tests.
/* c8 ignore start */ /* c8 ignore start */
if (n.length === 1) { if (n.length === 1) {
@@ -52667,12 +52710,13 @@ function expand_(str, max, maxLength, isTop) {
values = []; values = [];
let valuesLength = 0; let valuesLength = 0;
outer: for (let j = 0; j < n.length; j++) { outer: for (let j = 0; j < n.length; j++) {
const expanded = expand_(n[j], max, maxLength, false); const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
for (let k = 0; k < expanded.length; k++) { for (let k = 0; k < expanded.length; k++) {
const v = expanded[k]; const v = expanded[k];
if (dropsEmpties && !v) if (dropsEmpties && !v)
continue; continue;
if (values.length >= max || valuesLength + v.length > maxLength) { if (values.length >= max ||
valuesLength + v.length > maxLength) {
break outer; break outer;
} }
values.push(v); values.push(v);
+32 -20
View File
@@ -13,19 +13,26 @@ export const modules = {
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9896); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9896);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var semver__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2088); /* harmony import */ var _base_installer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6242);
/* harmony import */ var semver__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(semver__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _platform_types_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7444);
/* harmony import */ var _base_installer_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(6242); /* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4527);
/* harmony import */ var _platform_types_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(7444);
/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(4527);
function compareNumberArrays(a, b) {
class ZuluDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/* .JavaBase */ .O { const length = Math.max(a.length, b.length);
for (let i = 0; i < length; i++) {
const diff = (a[i] ?? 0) - (b[i] ?? 0);
if (diff !== 0) {
return diff;
}
}
return 0;
}
class ZuluDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_3__/* .JavaBase */ .O {
constructor(installerOptions) { constructor(installerOptions) {
super('Zulu', installerOptions); super('Zulu', installerOptions);
} }
@@ -39,19 +46,24 @@ class ZuluDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/*
? [...item.java_version, item.openjdk_build_number] ? [...item.java_version, item.openjdk_build_number]
: item.java_version; : item.java_version;
return { return {
version: (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .convertVersionToSemver */ .ZY)(javaVersion), version: (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .convertVersionToSemver */ .ZY)(javaVersion),
url: item.download_url, url: item.download_url,
zuluVersion: (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .convertVersionToSemver */ .ZY)(item.distro_version), javaVersion: item.java_version,
buildNumber: item.openjdk_build_number ?? 0,
distroVersion: item.distro_version,
packageUuid: item.package_uuid packageUuid: item.package_uuid
}; };
}); });
const satisfiedVersions = availableVersions const satisfiedVersions = availableVersions
.filter(item => (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .isVersionSatisfies */ .y)(version, item.version)) .filter(item => (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .isVersionSatisfies */ .y)(version, item.version))
.sort((a, b) => { .sort((a, b) => {
// Azul provides two versions: java_version and distro_version // Compare numerically rather than via semver build metadata: Azul
// we should sort by both fields by descending // hotfix releases carry a 4th java_version segment (e.g. 25.0.4.1+1,
return (-semver__WEBPACK_IMPORTED_MODULE_3___default().compareBuild(a.version, b.version) || // rendered as '25.0.4+1.1') that must rank above 25.0.4+7, whereas
-semver__WEBPACK_IMPORTED_MODULE_3___default().compareBuild(a.zuluVersion, b.zuluVersion)); // semver.compareBuild would order the build identifiers '7' > '1'.
return (-compareNumberArrays(a.javaVersion, b.javaVersion) ||
b.buildNumber - a.buildNumber ||
-compareNumberArrays(a.distroVersion, b.distroVersion));
}) })
.map((item) => ({ .map((item) => ({
version: item.version, version: item.version,
@@ -85,21 +97,21 @@ class ZuluDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/*
_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); _actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`);
let javaArchivePath = await this.downloadAndVerify(javaRelease); let javaArchivePath = await this.downloadAndVerify(javaRelease);
_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq(`Extracting Java archive...`); _actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq(`Extracting Java archive...`);
const extension = (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getDownloadArchiveExtension */ .ag)(); const extension = (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getDownloadArchiveExtension */ .ag)();
if (process.platform === 'win32') { if (process.platform === 'win32') {
javaArchivePath = (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .renameWinArchive */ .n2)(javaArchivePath); javaArchivePath = (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .renameWinArchive */ .n2)(javaArchivePath);
} }
const extractedJavaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .extractJdkFile */ .PE)(javaArchivePath, extension); const extractedJavaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .extractJdkFile */ .PE)(javaArchivePath, extension);
const archiveName = fs__WEBPACK_IMPORTED_MODULE_2___default().readdirSync(extractedJavaPath)[0]; const archiveName = fs__WEBPACK_IMPORTED_MODULE_2___default().readdirSync(extractedJavaPath)[0];
const archivePath = path__WEBPACK_IMPORTED_MODULE_1___default().join(extractedJavaPath, archiveName); const archivePath = path__WEBPACK_IMPORTED_MODULE_1___default().join(extractedJavaPath, archiveName);
const javaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .cacheJdkDir */ .Vj)(archivePath, this.toolcacheFolderName, this.getToolcacheVersionName(javaRelease.version), this.architecture); const javaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .cacheJdkDir */ .Vj)(archivePath, this.toolcacheFolderName, this.getToolcacheVersionName(javaRelease.version), this.architecture);
return { version: javaRelease.version, path: javaPath }; return { version: javaRelease.version, path: javaPath };
} }
async getAvailableVersions() { async getAvailableVersions() {
const arch = this.getArchitectureOptions(); const arch = this.getArchitectureOptions();
const [bundleType, features] = this.packageType.split('+'); const [bundleType, features] = this.packageType.split('+');
const platform = this.getPlatformOption(); const platform = this.getPlatformOption();
const extension = (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getDownloadArchiveExtension */ .ag)(); const extension = (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getDownloadArchiveExtension */ .ag)();
const javafx = features?.includes('fx') ?? false; const javafx = features?.includes('fx') ?? false;
const crac = features?.includes('crac') ?? false; const crac = features?.includes('crac') ?? false;
const releaseStatus = this.stable ? 'ga' : 'ea'; const releaseStatus = this.stable ? 'ga' : 'ea';
@@ -185,7 +197,7 @@ class ZuluDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/*
// The new Metadata API's "linux" value returns both glibc and musl // The new Metadata API's "linux" value returns both glibc and musl
// packages, so target the libc the runner actually has. A glibc JDK // packages, so target the libc the runner actually has. A glibc JDK
// cannot run on Alpine. // cannot run on Alpine.
return (0,_platform_types_js__WEBPACK_IMPORTED_MODULE_5__/* .isAlpineLinux */ .G6)() ? 'linux_musl' : 'linux_glibc'; return (0,_platform_types_js__WEBPACK_IMPORTED_MODULE_4__/* .isAlpineLinux */ .G6)() ? 'linux_musl' : 'linux_glibc';
default: default:
return process.platform; return process.platform;
} }
+479 -195
View File
@@ -6009,11 +6009,77 @@ class Request {
} }
} }
onUpgrade (statusCode, headers, socket) { /**
* @param {number|null} statusCode
* @param {Buffer[]|null} headers
* @param {import('node:stream').Duplex} socket
* @param {string} [statusText]
*/
onUpgrade (statusCode, headers, socket, statusText = '') {
this.onFinally()
assert(!this.aborted) assert(!this.aborted)
assert(!this.completed) assert(!this.completed)
return this[kHandler].onUpgrade(statusCode, headers, socket) if (statusCode !== null) {
this.#publishUpgradeHeaders(statusCode, headers, statusText)
}
const result = this[kHandler].onUpgrade(statusCode, headers, socket)
if (!this.aborted) {
this.completed = true
if (statusCode !== null) {
this.#publishUpgradeTrailers()
}
}
return result
}
/**
* @param {number} statusCode
* @param {import('node:http2').IncomingHttpHeaders} headers
* @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders
* @param {string} [statusText]
*/
onUpgradeResponse (statusCode, headers, parseHeaders, statusText = '') {
assert(!this.aborted)
assert(this.completed)
if (channels.headers.hasSubscribers) {
this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText)
}
this.#publishUpgradeTrailers()
}
/**
* @param {Error} error
*/
onUpgradeError (error) {
assert(!this.aborted)
assert(this.completed)
if (channels.error.hasSubscribers) {
channels.error.publish({ request: this, error })
}
}
/**
* @param {number} statusCode
* @param {Buffer[]} headers
* @param {string} statusText
*/
#publishUpgradeHeaders (statusCode, headers, statusText) {
if (channels.headers.hasSubscribers) {
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
}
}
#publishUpgradeTrailers () {
if (channels.trailers.hasSubscribers) {
channels.trailers.publish({ request: this, trailers: [] })
}
} }
onComplete (trailers) { onComplete (trailers) {
@@ -7912,7 +7978,7 @@ class Parser {
} }
onUpgrade (head) { onUpgrade (head) {
const { upgrade, client, socket, headers, statusCode } = this const { upgrade, client, socket, headers, statusCode, statusText } = this
assert(upgrade) assert(upgrade)
assert(client[kSocket] === socket) assert(client[kSocket] === socket)
@@ -7947,9 +8013,10 @@ class Parser {
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
try { try {
request.onUpgrade(statusCode, headers, socket) request.onUpgrade(statusCode, headers, socket, statusText)
} catch (err) { } catch (error) {
util.destroy(socket, err) util.errorRequest(client, request, error)
util.destroy(socket, error)
} }
client[kResume]() client[kResume]()
@@ -8356,7 +8423,7 @@ async function connectH1 (client, socket) {
function clearIdleSocketValidation (socket) { function clearIdleSocketValidation (socket) {
if (socket[kIdleSocketValidationTimeout]) { if (socket[kIdleSocketValidationTimeout]) {
clearTimeout(socket[kIdleSocketValidationTimeout]) clearImmediate(socket[kIdleSocketValidationTimeout])
socket[kIdleSocketValidationTimeout] = null socket[kIdleSocketValidationTimeout] = null
} }
@@ -8365,15 +8432,23 @@ function clearIdleSocketValidation (socket) {
function scheduleIdleSocketValidation (client, socket) { function scheduleIdleSocketValidation (client, socket) {
socket[kIdleSocketValidation] = 1 socket[kIdleSocketValidation] = 1
socket[kIdleSocketValidationTimeout] = setTimeout(() => { // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
// already pending on this idle keep-alive socket are processed before the
// next request is written (GHSA-35p6-xmwp-9g52).
//
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
// A ref'd Immediate both keeps the pending request alive and makes poll
// return immediately — the hybrid those issues asked for.
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
socket[kIdleSocketValidationTimeout] = null socket[kIdleSocketValidationTimeout] = null
socket[kIdleSocketValidation] = 2 socket[kIdleSocketValidation] = 2
if (client[kSocket] === socket && !socket.destroyed) { if (client[kSocket] === socket && !socket.destroyed) {
client[kResume]() client[kResume]()
} }
}, 0) })
socket[kIdleSocketValidationTimeout].unref?.()
} }
/** /**
@@ -8522,12 +8597,22 @@ function writeH1 (client, request) {
const socket = client[kSocket] const socket = client[kSocket]
clearIdleSocketValidation(socket) clearIdleSocketValidation(socket)
const abort = (err) => { /**
if (request.aborted || request.completed) { * @param {Error} [error]
*/
const abort = (error) => {
if (request.aborted) {
return return
} }
util.errorRequest(client, request, err || new RequestAbortedError()) if (request.completed) {
if (request.upgrade || request.method === 'CONNECT') {
util.destroy(socket, new InformationalError('aborted'))
}
return
}
util.errorRequest(client, request, error || new RequestAbortedError())
util.destroy(body) util.destroy(body)
util.destroy(socket, new InformationalError('aborted')) util.destroy(socket, new InformationalError('aborted'))
@@ -8984,6 +9069,7 @@ module.exports = connectH1
const assert = __nccwpck_require__(4589) const assert = __nccwpck_require__(4589)
const { errorMonitor } = __nccwpck_require__(8474)
const { pipeline } = __nccwpck_require__(7075) const { pipeline } = __nccwpck_require__(7075)
const util = __nccwpck_require__(3440) const util = __nccwpck_require__(3440)
const { const {
@@ -9060,6 +9146,15 @@ function parseH2Headers (headers) {
return result return result
} }
/**
* @param {import('node:http2').IncomingHttpHeaders} headers
* @returns {Buffer[]}
*/
function parseH2ResponseHeaders (headers) {
const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers
return parseH2Headers(realHeaders)
}
async function connectH2 (client, socket) { async function connectH2 (client, socket) {
client[kSocket] = socket client[kSocket] = socket
@@ -9280,22 +9375,32 @@ function writeH2 (client, request) {
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}` headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
headers[HTTP2_HEADER_METHOD] = method headers[HTTP2_HEADER_METHOD] = method
const abort = (err) => { /**
if (request.aborted || request.completed) { * @param {Error} [error]
*/
const abort = (error) => {
if (request.aborted) {
return return
} }
err = err || new RequestAbortedError() if (request.completed) {
if (method === 'CONNECT' && stream != null) {
util.destroy(stream, error || new RequestAbortedError())
}
return
}
util.errorRequest(client, request, err) error = error || new RequestAbortedError()
util.errorRequest(client, request, error)
if (stream != null) { if (stream != null) {
util.destroy(stream, err) util.destroy(stream, error)
} }
// We do not destroy the socket as we can continue using the session // We do not destroy the socket as we can continue using the session
// the stream get's destroyed and the session remains to create new streams // the stream get's destroyed and the session remains to create new streams
util.destroy(body, err) util.destroy(body, error)
client[kQueue][client[kRunningIdx]++] = null client[kQueue][client[kRunningIdx]++] = null
client[kResume]() client[kResume]()
} }
@@ -9314,25 +9419,57 @@ function writeH2 (client, request) {
if (method === 'CONNECT') { if (method === 'CONNECT') {
session.ref() session.ref()
// We are already connected, streams are pending, first request
// will create a new stream. We trigger a request to create the stream and wait until
// `ready` event is triggered
// We disabled endStream to allow the user to write to the stream // We disabled endStream to allow the user to write to the stream
stream = session.request(headers, { endStream: false, signal }) stream = session.request(headers, { endStream: false, signal })
let upgradeResponseFinished = false
if (stream.id && !stream.pending) { /**
request.onUpgrade(null, null, stream) * @param {import('node:http2').IncomingHttpHeaders} headers
++session[kOpenStreams] */
client[kQueue][client[kRunningIdx]++] = null const onResponse = (headers) => {
} else { upgradeResponseFinished = true
stream.once('ready', () => { stream.off(errorMonitor, onUpgradeError)
request.onUpgrade(null, null, stream) request.onUpgradeResponse(Number(headers[HTTP2_HEADER_STATUS]), headers, parseH2ResponseHeaders)
++session[kOpenStreams]
client[kQueue][client[kRunningIdx]++] = null
})
} }
/**
* @param {Error} error
*/
const onUpgradeError = (error) => {
upgradeResponseFinished = true
stream.off('response', onResponse)
request.onUpgradeError(error)
}
const onReady = () => {
try {
request.onUpgrade(null, null, stream)
} catch (error) {
stream.off('response', onResponse)
abort(error)
return
}
if (request.aborted) {
return
}
stream.off('error', abort)
stream.once(errorMonitor, onUpgradeError)
client[kQueue][client[kRunningIdx]++] = null
}
stream.once('response', onResponse)
stream.once('error', abort)
++session[kOpenStreams]
onReady()
stream.once('close', () => { stream.once('close', () => {
if (!upgradeResponseFinished && request.completed) {
stream.off('response', onResponse)
stream.off(errorMonitor, onUpgradeError)
request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
}
session[kOpenStreams] -= 1 session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) session.unref() if (session[kOpenStreams] === 0) session.unref()
}) })
@@ -12031,6 +12168,7 @@ class RetryHandler {
this.end = null this.end = null
this.etag = null this.etag = null
this.resume = null this.resume = null
this.headersSent = false
// Handle possible onConnect duplication // Handle possible onConnect duplication
this.handler.onConnect(reason => { this.handler.onConnect(reason => {
@@ -12043,6 +12181,20 @@ class RetryHandler {
}) })
} }
checkpointResponseEnd (headers, resume) {
if (this.end == null && this.opts.method !== 'HEAD') {
const contentLength = headers['content-length']
this.end = contentLength != null ? Number(contentLength) - 1 : null
assert(
this.end == null || Number.isFinite(this.end),
'invalid content-length'
)
}
this.resume = this.end != null ? resume : null
}
onRequestSent () { onRequestSent () {
if (this.handler.onRequestSent) { if (this.handler.onRequestSent) {
this.handler.onRequestSent() this.handler.onRequestSent()
@@ -12131,7 +12283,12 @@ class RetryHandler {
this.retryCount += 1 this.retryCount += 1
if (statusCode >= 300) { if (statusCode >= 300) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) { // Only expose a response if no earlier attempt has reached the caller.
// Otherwise abort this attempt so the error settles the existing body
// instead of replacing it with a new response.
if (!this.headersSent && this.retryOpts.statusCodes.includes(statusCode) === false) {
this.headersSent = true
this.checkpointResponseEnd(headers, resume)
return this.handler.onHeaders( return this.handler.onHeaders(
statusCode, statusCode,
rawHeaders, rawHeaders,
@@ -12200,8 +12357,15 @@ class RetryHandler {
const { start, size, end = size - 1 } = contentRange const { start, size, end = size - 1 } = contentRange
assert(this.start === start, 'content-range mismatch') if (this.start !== start || (this.end != null && this.end !== end)) {
assert(this.end == null || this.end === end, 'content-range mismatch') this.abort(
new RequestRetryError('Content-Range mismatch', statusCode, {
headers,
data: { count: this.retryCount }
})
)
return false
}
this.resume = resume this.resume = resume
return true return true
@@ -12213,6 +12377,7 @@ class RetryHandler {
const range = parseRangeHeader(headers['content-range']) const range = parseRangeHeader(headers['content-range'])
if (range == null) { if (range == null) {
this.headersSent = true
return this.handler.onHeaders( return this.handler.onHeaders(
statusCode, statusCode,
rawHeaders, rawHeaders,
@@ -12251,6 +12416,7 @@ class RetryHandler {
) )
this.resume = resume this.resume = resume
this.headersSent = true
this.etag = headers.etag != null ? headers.etag : null this.etag = headers.etag != null ? headers.etag : null
// Weak etags are not useful for comparison nor cache // Weak etags are not useful for comparison nor cache
@@ -12290,7 +12456,7 @@ class RetryHandler {
} }
onError (err) { onError (err) {
if (this.aborted || isDisturbed(this.opts.body)) { if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
return this.handler.onError(err) return this.handler.onError(err)
} }
@@ -16748,6 +16914,49 @@ const COLON = 0x3A
*/ */
const SPACE = 0x20 const SPACE = 0x20
const DATA = Buffer.from('data')
const EVENT = Buffer.from('event')
const ID = Buffer.from('id')
const RETRY = Buffer.from('retry')
function isASCIINumberBytes (buffer, start) {
if (start >= buffer.length) {
return false
}
for (let i = start; i < buffer.length; i++) {
if (buffer[i] < 0x30 || buffer[i] > 0x39) {
return false
}
}
return true
}
function isValidLastEventIdBytes (buffer, start) {
for (let i = start; i < buffer.length; i++) {
if (buffer[i] === 0x00) {
return false
}
}
return true
}
function isFieldName (line, length, field) {
if (length !== field.length) {
return false
}
for (let i = 0; i < length; i++) {
if (line[i] !== field[i]) {
return false
}
}
return true
}
/** /**
* @typedef {object} EventSourceStreamEvent * @typedef {object} EventSourceStreamEvent
* @type {object} * @type {object}
@@ -16788,11 +16997,14 @@ class EventSourceStream extends Transform {
eventEndCheck = false eventEndCheck = false
/** /**
* @type {Buffer} * @type {Buffer[]}
*/ */
buffer = null chunks = []
chunkIndex = 0
pos = 0 pos = 0
lineChunkIndex = 0
linePos = 0
event = { event = {
data: undefined, data: undefined,
@@ -16831,92 +17043,20 @@ class EventSourceStream extends Transform {
return return
} }
// Cache the chunk in the buffer, as the data might not be complete while this.chunks.push(chunk)
// processing it
// TODO: Investigate if there is a more performant way to handle
// incoming chunks
// see: https://github.com/nodejs/undici/issues/2630
if (this.buffer) {
this.buffer = Buffer.concat([this.buffer, chunk])
} else {
this.buffer = chunk
}
// Strip leading byte-order-mark if we opened the stream and started // Strip leading byte-order-mark if we opened the stream and started
// the processing of the incoming data // the processing of the incoming data
if (this.checkBOM) { if (this.checkBOM) {
switch (this.buffer.length) { if (this.handleBOM()) {
case 1: callback()
// Check if the first byte is the same as the first byte of the BOM return
if (this.buffer[0] === BOM[0]) {
// If it is, we need to wait for more data
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// The buffer only contains one byte so we need to wait for more data
callback()
return
case 2:
// Check if the first two bytes are the same as the first two bytes
// of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1]
) {
// If it is, we need to wait for more data, because the third byte
// is needed to determine if it is the BOM or not
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
break
case 3:
// Check if the first three bytes are the same as the first three
// bytes of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// If it is, we can drop the buffered data, as it is only the BOM
this.buffer = Buffer.alloc(0)
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// Await more data
callback()
return
}
// If it is not the BOM, we can start processing the data
this.checkBOM = false
break
default:
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
// present
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// Remove the BOM from the buffer
this.buffer = this.buffer.subarray(3)
}
// Set the checkBOM flag to false as we don't need to check for the
this.checkBOM = false
break
} }
} }
while (this.pos < this.buffer.length) { while (this.hasCurrentByte()) {
const byte = this.currentByte()
// If the previous line ended with an end-of-line, we need to check // If the previous line ended with an end-of-line, we need to check
// if the next character is also an end-of-line. // if the next character is also an end-of-line.
if (this.eventEndCheck) { if (this.eventEndCheck) {
@@ -16929,10 +17069,9 @@ class EventSourceStream extends Transform {
if (this.crlfCheck) { if (this.crlfCheck) {
// If the current character is a line feed, we can remove it // If the current character is a line feed, we can remove it
// from the buffer and reset the crlfCheck flag // from the buffer and reset the crlfCheck flag
if (this.buffer[this.pos] === LF) { if (byte === LF) {
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
this.crlfCheck = false this.crlfCheck = false
this.consumeCurrentByte()
// It is possible that the line feed is not the end of the // It is possible that the line feed is not the end of the
// event. We need to check if the next character is an // event. We need to check if the next character is an
@@ -16948,19 +17087,17 @@ class EventSourceStream extends Transform {
this.crlfCheck = false this.crlfCheck = false
} }
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { if (byte === LF || byte === CR) {
// If the current character is a carriage return, we need to // If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the // set the crlfCheck flag to true, as we need to check if the
// next character is a line feed so we can remove it from the // next character is a line feed so we can remove it from the
// buffer // buffer
if (this.buffer[this.pos] === CR) { if (byte === CR) {
this.crlfCheck = true this.crlfCheck = true
} }
this.buffer = this.buffer.subarray(this.pos + 1) this.consumeCurrentByte()
this.pos = 0 if (this.hasPendingEvent()) {
if (
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
this.processEvent(this.event) this.processEvent(this.event)
} }
this.clearEvent() this.clearEvent()
@@ -16974,22 +17111,18 @@ class EventSourceStream extends Transform {
// If the current character is an end-of-line, we can process the // If the current character is an end-of-line, we can process the
// line // line
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { if (byte === LF || byte === CR) {
// If the current character is a carriage return, we need to // If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the // set the crlfCheck flag to true, as we need to check if the
// next character is a line feed // next character is a line feed
if (this.buffer[this.pos] === CR) { if (byte === CR) {
this.crlfCheck = true this.crlfCheck = true
} }
// In any case, we can process the line as we reached an // In any case, we can process the line as we reached an
// end-of-line character // end-of-line character
this.parseLine(this.buffer.subarray(0, this.pos), this.event) this.parseLine(this.readLine(), this.event)
this.consumeCurrentByte()
// Remove the processed line from the buffer
this.buffer = this.buffer.subarray(this.pos + 1)
// Reset the position as we removed the processed line from the buffer
this.pos = 0
// A line was processed and this could be the end of the event. We need // A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is // to check if the next line is empty to determine if the event is
// finished. // finished.
@@ -16997,7 +17130,7 @@ class EventSourceStream extends Transform {
continue continue
} }
this.pos++ this.advanceCursor()
} }
callback() callback()
@@ -17022,64 +17155,53 @@ class EventSourceStream extends Transform {
return return
} }
let field = '' let fieldLength = line.length
let value = '' let valueStart = line.length
// If the line contains a U+003A COLON character (:) // If the line contains a U+003A COLON character (:)
if (colonPosition !== -1) { if (colonPosition !== -1) {
// Collect the characters on the line before the first U+003A COLON fieldLength = colonPosition
// character (:), and let field be that string.
// TODO: Investigate if there is a more performant way to extract the
// field
// see: https://github.com/nodejs/undici/issues/2630
field = line.subarray(0, colonPosition).toString('utf8')
// Collect the characters on the line after the first U+003A COLON // Collect the characters on the line after the first U+003A COLON
// character (:), and let value be that string. // character (:), and let value be that string.
// If value starts with a U+0020 SPACE character, remove it from value. // If value starts with a U+0020 SPACE character, remove it from value.
let valueStart = colonPosition + 1 valueStart = colonPosition + 1
if (line[valueStart] === SPACE) { if (line[valueStart] === SPACE) {
++valueStart ++valueStart
} }
// TODO: Investigate if there is a more performant way to extract the
// value
// see: https://github.com/nodejs/undici/issues/2630
value = line.subarray(valueStart).toString('utf8')
// Otherwise, the string is not empty but does not contain a U+003A COLON
// character (:)
} else {
// Process the field using the steps described below, using the whole
// line as the field name, and the empty string as the field value.
field = line.toString('utf8')
value = ''
} }
// Modify the event with the field name and value. The value is also if (isFieldName(line, fieldLength, DATA)) {
// decoded as UTF-8 const value = line.toString('utf8', valueStart)
switch (field) {
case 'data': if (event.data === undefined) {
if (event[field] === undefined) { event.data = value
event[field] = value } else {
} else { event.data += `\n${value}`
event[field] += `\n${value}` }
} return
break }
case 'retry':
if (isASCIINumber(value)) { if (isFieldName(line, fieldLength, RETRY)) {
event[field] = value if (isASCIINumberBytes(line, valueStart)) {
} event.retry = line.toString('utf8', valueStart)
break }
case 'id': return
if (isValidLastEventId(value)) { }
event[field] = value
} if (isFieldName(line, fieldLength, ID)) {
break if (isValidLastEventIdBytes(line, valueStart)) {
case 'event': event.id = line.toString('utf8', valueStart)
if (value.length > 0) { }
event[field] = value return
} }
break
if (isFieldName(line, fieldLength, EVENT)) {
const value = line.toString('utf8', valueStart)
if (value.length > 0) {
event.event = value
}
} }
} }
@@ -17109,13 +17231,152 @@ class EventSourceStream extends Transform {
} }
clearEvent () { clearEvent () {
this.event = { this.event.data = undefined
data: undefined, this.event.event = undefined
event: undefined, this.event.id = undefined
id: undefined, this.event.retry = undefined
retry: undefined }
hasPendingEvent () {
return this.event.data !== undefined ||
this.event.event !== undefined ||
this.event.id !== undefined ||
this.event.retry !== undefined
}
hasCurrentByte () {
return this.chunkIndex < this.chunks.length &&
this.pos < this.chunks[this.chunkIndex].length
}
currentByte () {
return this.chunks[this.chunkIndex][this.pos]
}
consumeCurrentByte () {
this.advanceCursor()
this.syncLineStartToCursor()
}
advanceCursor () {
this.pos++
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
this.chunkIndex++
this.pos = 0
} }
} }
syncLineStartToCursor () {
this.lineChunkIndex = this.chunkIndex
this.linePos = this.pos
this.dropConsumedChunks()
}
dropConsumedChunks () {
while (this.lineChunkIndex > 0) {
this.chunks.shift()
this.lineChunkIndex--
this.chunkIndex--
}
if (this.chunkIndex === this.chunks.length) {
this.chunks.length = 0
this.chunkIndex = 0
this.pos = 0
this.lineChunkIndex = 0
this.linePos = 0
}
}
readLine () {
if (this.lineChunkIndex === this.chunkIndex) {
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
}
const chunks = []
let length = 0
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
const chunk = this.chunks[i]
const start = i === this.lineChunkIndex ? this.linePos : 0
const end = i === this.chunkIndex ? this.pos : chunk.length
const slice = chunk.subarray(start, end)
length += slice.length
chunks.push(slice)
}
return Buffer.concat(chunks, length)
}
peekBufferedByte (offset) {
let chunkIndex = this.lineChunkIndex
let pos = this.linePos
while (chunkIndex < this.chunks.length) {
const chunk = this.chunks[chunkIndex]
const remaining = chunk.length - pos
if (offset < remaining) {
return chunk[pos + offset]
}
offset -= remaining
chunkIndex++
pos = 0
}
}
discardLeadingBytes (count) {
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
const chunk = this.chunks[this.lineChunkIndex]
const remaining = chunk.length - this.linePos
if (count < remaining) {
this.linePos += count
count = 0
} else {
count -= remaining
this.lineChunkIndex++
this.linePos = 0
}
}
this.chunkIndex = this.lineChunkIndex
this.pos = this.linePos
this.dropConsumedChunks()
}
handleBOM () {
const first = this.peekBufferedByte(0)
const second = this.peekBufferedByte(1)
const third = this.peekBufferedByte(2)
if (second === undefined) {
if (first === BOM[0]) {
return true
}
this.checkBOM = false
return true
}
if (third === undefined) {
if (first === BOM[0] && second === BOM[1]) {
return true
}
this.checkBOM = false
return false
}
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
this.discardLeadingBytes(3)
}
this.checkBOM = false
return !this.hasCurrentByte()
}
} }
module.exports = { module.exports = {
@@ -28383,7 +28644,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// is specified, the server needs to include the same field and one of // is specified, the server needs to include the same field and one of
// the selected subprotocol values in its response for the connection to // the selected subprotocol values in its response for the connection to
// be established. // be established.
if (!requestProtocols.includes(secProtocol)) { if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
return return
} }
@@ -29144,7 +29405,12 @@ class PerMessageDeflate {
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
callback(new MessageSizeExceededError()) callback(new MessageSizeExceededError())
// The inflater may still hold buffered input that can emit a late
// zlib error. Remove the data listener, then deterministically stop
// the stream so a subsequent 'error' cannot fire without a listener
// (which would terminate the process as an unhandled error event).
this.#inflate.removeAllListeners() this.#inflate.removeAllListeners()
this.#inflate.destroy()
this.#inflate = null this.#inflate = null
return return
} }
@@ -30975,12 +31241,7 @@ function canResolveTemurinJmods(version) {
if (normalizedVersion === 'latest') { if (normalizedVersion === 'latest') {
return true; return true;
} }
let normalizedRange = normalizedVersion const normalizedRange = (0,_util_js__WEBPACK_IMPORTED_MODULE_1__/* .normalizeJavaVersionToSemver */ .zZ)(normalizedVersion.replace(/-ea$/, '').replace('-ea.', '+'));
.replace(/-ea$/, '')
.replace('-ea.', '+');
if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) {
normalizedRange = (0,_util_js__WEBPACK_IMPORTED_MODULE_1__/* .convertVersionToSemver */ .ZY)(normalizedRange);
}
if (!semver__WEBPACK_IMPORTED_MODULE_0___default().validRange(normalizedRange)) { if (!semver__WEBPACK_IMPORTED_MODULE_0___default().validRange(normalizedRange)) {
// JavaBase owns general version validation and its targeted error messages. // JavaBase owns general version validation and its targeted error messages.
return true; return true;
@@ -31022,7 +31283,14 @@ function createUnsupportedPackageError(distributionName, packageType, supportedP
const X64_ARM64 = ['x64', 'aarch64']; const X64_ARM64 = ['x64', 'aarch64'];
const STANDARD_LINUX = ['x64', 'x86', 'aarch64', 'ppc64le', 's390x']; const STANDARD_LINUX = [
'x64',
'x86',
'aarch64',
'ppc64le',
'riscv64',
's390x'
];
const JAVA_PLATFORM_CAPABILITIES = { const JAVA_PLATFORM_CAPABILITIES = {
[_package_types_js__WEBPACK_IMPORTED_MODULE_2__/* .JavaDistribution */ .zS.Temurin]: { [_package_types_js__WEBPACK_IMPORTED_MODULE_2__/* .JavaDistribution */ .zS.Temurin]: {
platforms: { platforms: {
@@ -31164,6 +31432,7 @@ const CANONICAL_ARCHITECTURES = [
'aarch64', 'aarch64',
'ppc64le', 'ppc64le',
'ppc64', 'ppc64',
'riscv64',
's390x' 's390x'
]; ];
const PLATFORM_ALIASES = { const PLATFORM_ALIASES = {
@@ -31303,7 +31572,8 @@ function validateToolchainIds(versions, versionFile, toolchainIds) {
/* harmony export */ rC: () => (/* binding */ getNextPageUrlFromLinkHeader), /* harmony export */ rC: () => (/* binding */ getNextPageUrlFromLinkHeader),
/* harmony export */ ri: () => (/* binding */ getLatestMajorVersion), /* harmony export */ ri: () => (/* binding */ getLatestMajorVersion),
/* harmony export */ y: () => (/* binding */ isVersionSatisfies), /* harmony export */ y: () => (/* binding */ isVersionSatisfies),
/* harmony export */ yH: () => (/* binding */ getToolcachePath) /* harmony export */ yH: () => (/* binding */ getToolcachePath),
/* harmony export */ zZ: () => (/* binding */ normalizeJavaVersionToSemver)
/* harmony export */ }); /* harmony export */ });
/* unused harmony exports getVersionFromToolcachePath, isJobStatusSuccess */ /* unused harmony exports getVersionFromToolcachePath, isJobStatusSuccess */
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(857); /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(857);
@@ -31704,6 +31974,20 @@ function convertVersionToSemver(version) {
} }
return mainVersion; return mainVersion;
} }
/**
* Java versions (JEP 322) can contain more numeric fields than SemVer allows,
* e.g. '11.0.9.1' or Temurin respins such as '26.0.2.1+1'. Move the extra
* fields into SemVer build metadata ('11.0.9+1', '26.0.2+1.1'). Any other
* input (ranges, regular SemVer versions) is returned unchanged.
*/
function normalizeJavaVersionToSemver(version) {
const match = /^(\d+(?:\.\d+){3,})(?:\+([0-9A-Za-z.-]+))?$/.exec(version);
if (!match) {
return version;
}
const converted = convertVersionToSemver(match[1]);
return match[2] ? `${converted}.${match[2]}` : converted;
}
/** /**
* Builds a validator for the bytes currently served by a URL from the response * Builds a validator for the bytes currently served by a URL from the response
* headers of a HEAD request. A vendor's `/latest/` URL never changes, so this * headers of a HEAD request. A vendor's `/latest/` URL never changes, so this
+1 -1
View File
@@ -649,7 +649,7 @@ absent from a vendor catalog.
| Distribution | Linux | macOS | Windows | Other / version restrictions | | Distribution | Linux | macOS | Windows | Other / version restrictions |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `temurin` | `x64`, `x86`, `armv7`, `aarch64`, `ppc64le`, `s390x` | `x64`, `aarch64` | `x64`, `x86`, `aarch64` | Linux `armv7` is available through Java 17. | | `temurin` | `x64`, `x86`, `armv7`, `aarch64`, `ppc64le`, `riscv64`, `s390x` | `x64`, `aarch64` | `x64`, `x86`, `aarch64` | Linux `armv7` is available through Java 17. |
| `zulu` | `x64`, `x86`, `armv7`, `aarch64` | `x64`, `aarch64` | `x64`, `x86`, `aarch64` | | | `zulu` | `x64`, `x86`, `armv7`, `aarch64` | `x64`, `aarch64` | `x64`, `x86`, `aarch64` | |
| `liberica` | `x64`, `x86`, `armv7`, `aarch64`, `ppc64le` | `x64`, `aarch64` | `x64`, `x86`, `aarch64` | Solaris: `x64`. | | `liberica` | `x64`, `x86`, `armv7`, `aarch64`, `ppc64le` | `x64`, `aarch64` | `x64`, `x86`, `aarch64` | Solaris: `x64`. |
| `liberica-nik` | `x64`, `aarch64` | `x64`, `aarch64` | `x64`, `aarch64` | | | `liberica-nik` | `x64`, `aarch64` | `x64`, `aarch64` | `x64`, `aarch64` | |
+987 -642
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -49,27 +49,27 @@
"@actions/http-client": "^4.0.1", "@actions/http-client": "^4.0.1",
"@actions/io": "^3.0.2", "@actions/io": "^3.0.2",
"@actions/tool-cache": "^4.0.0", "@actions/tool-cache": "^4.0.0",
"fast-xml-parser": "^5.11.0", "fast-xml-parser": "^5.11.1",
"semver": "^7.8.5" "semver": "^7.8.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1", "@jest/globals": "^30.5.2",
"@types/node": "^26.2.0", "@types/node": "^26.6.2",
"@types/semver": "^7.8.0", "@types/semver": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.65.0", "@typescript-eslint/parser": "^8.65.0",
"@vercel/ncc": "^0.45.0", "@vercel/ncc": "^0.45.0",
"eslint": "^10.8.1", "eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-jest": "^29.16.1", "eslint-plugin-jest": "^29.16.6",
"eslint-plugin-n": "^18.3.0", "eslint-plugin-n": "^18.3.0",
"globals": "^17.11.0", "globals": "^17.12.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^30.4.2", "jest": "^30.5.2",
"lint-staged": "^17.3.0", "lint-staged": "^17.5.1",
"prettier": "^3.9.5", "prettier": "^3.9.9",
"ts-jest": "^29.4.11", "ts-jest": "^29.4.13",
"typescript": "^6.0.3" "typescript": "^6.0.3"
}, },
"bugs": { "bugs": {
+5 -8
View File
@@ -5,9 +5,9 @@ import semver from 'semver';
import path from 'path'; import path from 'path';
import * as httpm from '@actions/http-client'; import * as httpm from '@actions/http-client';
import { import {
convertVersionToSemver,
getToolcachePath, getToolcachePath,
isVersionSatisfies isVersionSatisfies,
normalizeJavaVersionToSemver
} from '../util.js'; } from '../util.js';
import type { import type {
ChecksumAlgorithm, ChecksumAlgorithm,
@@ -675,12 +675,9 @@ export abstract class JavaBase {
// Java uses a versioning scheme (JEP 322) that can contain more numeric // Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such // fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are // exact versions to SemVer build notation ('18.0.1+1', or '26.0.2+1.1'
// accepted. Ranges and versions that already carry build metadata are // for '26.0.2.1+1') so they are accepted. Ranges are left untouched.
// left untouched. version = normalizeJavaVersionToSemver(version);
if (/^\d+(\.\d+){3,}$/.test(version)) {
version = convertVersionToSemver(version);
}
if (!semver.validRange(version)) { if (!semver.validRange(version)) {
throw new Error( throw new Error(
+2 -2
View File
@@ -166,8 +166,8 @@ export class LibericaDistributions extends JavaBase {
} }
private convertVersionToSemver(version: LibericaVersion): string { private convertVersionToSemver(version: LibericaVersion): string {
const {buildVersion, featureVersion, interimVersion, updateVersion} = const {featureVersion, interimVersion, updateVersion} = version;
version; const buildVersion = version.version.split('+')[1] || version.buildVersion;
const mainVersion = [featureVersion, interimVersion, updateVersion].join( const mainVersion = [featureVersion, interimVersion, updateVersion].join(
'.' '.'
); );
+4 -7
View File
@@ -1,5 +1,5 @@
import semver from 'semver'; import semver from 'semver';
import {convertVersionToSemver} from '../util.js'; import {normalizeJavaVersionToSemver} from '../util.js';
export enum JavaDistribution { export enum JavaDistribution {
Temurin = 'temurin', Temurin = 'temurin',
@@ -98,12 +98,9 @@ function canResolveTemurinJmods(version: string): boolean {
return true; return true;
} }
let normalizedRange = normalizedVersion const normalizedRange = normalizeJavaVersionToSemver(
.replace(/-ea$/, '') normalizedVersion.replace(/-ea$/, '').replace('-ea.', '+')
.replace('-ea.', '+'); );
if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) {
normalizedRange = convertVersionToSemver(normalizedRange);
}
if (!semver.validRange(normalizedRange)) { if (!semver.validRange(normalizedRange)) {
// JavaBase owns general version validation and its targeted error messages. // JavaBase owns general version validation and its targeted error messages.
return true; return true;
+17 -2
View File
@@ -5,7 +5,14 @@ import {JavaDistribution} from './package-types.js';
export type JavaPlatform = 'linux' | 'macos' | 'windows' | 'solaris'; export type JavaPlatform = 'linux' | 'macos' | 'windows' | 'solaris';
export type JavaArchitecture = export type JavaArchitecture =
'x86' | 'x64' | 'armv7' | 'aarch64' | 'ppc64le' | 'ppc64' | 's390x'; | 'x86'
| 'x64'
| 'armv7'
| 'aarch64'
| 'ppc64le'
| 'ppc64'
| 'riscv64'
| 's390x';
interface VersionedArchitecture { interface VersionedArchitecture {
architecture: JavaArchitecture; architecture: JavaArchitecture;
@@ -27,7 +34,14 @@ export type JavaPlatformCapability =
RestrictedPlatformCapability | UnrestrictedPlatformCapability; RestrictedPlatformCapability | UnrestrictedPlatformCapability;
const X64_ARM64 = ['x64', 'aarch64'] as const; const X64_ARM64 = ['x64', 'aarch64'] as const;
const STANDARD_LINUX = ['x64', 'x86', 'aarch64', 'ppc64le', 's390x'] as const; const STANDARD_LINUX = [
'x64',
'x86',
'aarch64',
'ppc64le',
'riscv64',
's390x'
] as const;
export const JAVA_PLATFORM_CAPABILITIES: Record< export const JAVA_PLATFORM_CAPABILITIES: Record<
JavaDistribution, JavaDistribution,
@@ -174,6 +188,7 @@ const CANONICAL_ARCHITECTURES: readonly JavaArchitecture[] = [
'aarch64', 'aarch64',
'ppc64le', 'ppc64le',
'ppc64', 'ppc64',
'riscv64',
's390x' 's390x'
]; ];
+39 -4
View File
@@ -70,7 +70,7 @@ export class TemurinDistribution extends JavaBase {
const formattedVersion = this.stable const formattedVersion = this.stable
? item.version_data.semver ? item.version_data.semver
: item.version_data.semver.replace('-beta+', '+'); : item.version_data.semver.replace('-beta+', '+');
return { const release: JavaDownloadRelease = {
version: formattedVersion, version: formattedVersion,
url: item.binaries[0].package.link, url: item.binaries[0].package.link,
signatureUrl: item.binaries[0].package.signature_link, signatureUrl: item.binaries[0].package.signature_link,
@@ -79,11 +79,29 @@ export class TemurinDistribution extends JavaBase {
value: item.binaries[0].package.checksum, value: item.binaries[0].package.checksum,
source: item.binaries[0].package.checksum_link source: item.binaries[0].package.checksum_link
} }
} as JavaDownloadRelease; };
return {
release,
openjdkVersion: getOpenJdkSemverVersion(item.version_data)
};
}); });
// The Adoptium API `semver` folds the JEP 322 patch field into the build
// number ('26.0.2.1+1' -> '26.0.2+101') and appends extra metadata for LTS
// releases ('25.0.4+7' -> '25.0.4+7.0.LTS'). Exact versions requested by
// users follow the OpenJDK notation instead, so also match them against a
// key derived from the OpenJDK version fields ('26.0.2+1.1', '25.0.4+7').
const isExactBuildRequest = (semver.parse(version)?.build.length ?? 0) > 0;
const satisfiedVersions = availableVersionsWithBinaries const satisfiedVersions = availableVersionsWithBinaries
.filter(item => isVersionSatisfies(version, item.version)) .filter(
({release, openjdkVersion}) =>
isVersionSatisfies(version, release.version) ||
(isExactBuildRequest &&
openjdkVersion !== null &&
semver.compareBuild(version, openjdkVersion) === 0)
)
.map(({release}) => release)
.sort((a, b) => { .sort((a, b) => {
return -semver.compareBuild(a.version, b.version); return -semver.compareBuild(a.version, b.version);
}); });
@@ -92,7 +110,7 @@ export class TemurinDistribution extends JavaBase {
satisfiedVersions.length > 0 ? satisfiedVersions[0] : null; satisfiedVersions.length > 0 ? satisfiedVersions[0] : null;
if (!resolvedFullVersion) { if (!resolvedFullVersion) {
const availableVersionStrings = availableVersionsWithBinaries.map( const availableVersionStrings = availableVersionsWithBinaries.map(
item => item.version ({release}) => release.version
); );
throw this.createVersionNotFoundError(version, availableVersionStrings); throw this.createVersionNotFoundError(version, availableVersionStrings);
} }
@@ -309,3 +327,20 @@ export class TemurinDistribution extends JavaBase {
return architecture === 'armv7' ? 'arm' : architecture; return architecture === 'armv7' ? 'arm' : architecture;
} }
} }
/**
* Builds a SemVer version from the OpenJDK version fields reported by the
* Adoptium API, e.g. '26.0.2.1+1' -> '26.0.2+1.1' and '25.0.4+7-LTS' ->
* '25.0.4+7'. Returns null if the fields cannot form a valid SemVer version.
*/
function getOpenJdkSemverVersion(
versionData: ITemurinAvailableVersions['version_data']
): string | null {
const {major, minor, security, patch, build} = versionData;
if (build === undefined || build === null) {
return null;
}
const buildMetadata = patch ? `${patch}.${build}` : `${build}`;
const version = `${major}.${minor}.${security}+${buildMetadata}`;
return semver.valid(version) ? version : null;
}
+1
View File
@@ -34,6 +34,7 @@ export interface ITemurinAvailableVersions {
minor: number; minor: number;
openjdk_version: string; openjdk_version: string;
security: string; security: string;
patch?: number;
semver: string; semver: string;
}; };
} }
+21 -6
View File
@@ -2,7 +2,6 @@ import * as core from '@actions/core';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import semver from 'semver';
import {JavaBase} from '../base-installer.js'; import {JavaBase} from '../base-installer.js';
import {IZuluPackageDetails, IZuluVersions} from './models.js'; import {IZuluPackageDetails, IZuluVersions} from './models.js';
@@ -30,6 +29,17 @@ interface ZuluResolvedRelease {
packageUuid: string; packageUuid: string;
} }
function compareNumberArrays(a: number[], b: number[]): number {
const length = Math.max(a.length, b.length);
for (let i = 0; i < length; i++) {
const diff = (a[i] ?? 0) - (b[i] ?? 0);
if (diff !== 0) {
return diff;
}
}
return 0;
}
export class ZuluDistribution extends JavaBase { export class ZuluDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {
super('Zulu', installerOptions); super('Zulu', installerOptions);
@@ -50,7 +60,9 @@ export class ZuluDistribution extends JavaBase {
return { return {
version: convertVersionToSemver(javaVersion), version: convertVersionToSemver(javaVersion),
url: item.download_url, url: item.download_url,
zuluVersion: convertVersionToSemver(item.distro_version), javaVersion: item.java_version,
buildNumber: item.openjdk_build_number ?? 0,
distroVersion: item.distro_version,
packageUuid: item.package_uuid packageUuid: item.package_uuid
}; };
}); });
@@ -58,11 +70,14 @@ export class ZuluDistribution extends JavaBase {
const satisfiedVersions = availableVersions const satisfiedVersions = availableVersions
.filter(item => isVersionSatisfies(version, item.version)) .filter(item => isVersionSatisfies(version, item.version))
.sort((a, b) => { .sort((a, b) => {
// Azul provides two versions: java_version and distro_version // Compare numerically rather than via semver build metadata: Azul
// we should sort by both fields by descending // hotfix releases carry a 4th java_version segment (e.g. 25.0.4.1+1,
// rendered as '25.0.4+1.1') that must rank above 25.0.4+7, whereas
// semver.compareBuild would order the build identifiers '7' > '1'.
return ( return (
-semver.compareBuild(a.version, b.version) || -compareNumberArrays(a.javaVersion, b.javaVersion) ||
-semver.compareBuild(a.zuluVersion, b.zuluVersion) b.buildNumber - a.buildNumber ||
-compareNumberArrays(a.distroVersion, b.distroVersion)
); );
}) })
.map((item): ZuluResolvedRelease => ({ .map((item): ZuluResolvedRelease => ({
+7 -3
View File
@@ -89,17 +89,20 @@ export async function removeGpgHome(gpgHome: string): Promise<void> {
return; return;
} }
await stopGpgAgent(resolvedGpgHome);
await io.rmRF(resolvedGpgHome);
}
async function stopGpgAgent(gpgHome: string): Promise<void> {
try { try {
await exec.exec( await exec.exec(
'gpgconf', 'gpgconf',
['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], ['--homedir', toGpgPath(gpgHome), '--kill', 'gpg-agent'],
{silent: true, ignoreReturnCode: true} {silent: true, ignoreReturnCode: true}
); );
} catch { } catch {
// gpgconf may be unavailable, but directory removal must still be attempted. // gpgconf may be unavailable, but directory removal must still be attempted.
} }
await io.rmRF(resolvedGpgHome);
} }
export async function verifyPackageSignature( export async function verifyPackageSignature(
@@ -160,6 +163,7 @@ export async function verifyPackageSignature(
options options
); );
} finally { } finally {
await stopGpgAgent(gpgHome);
await io.rmRF(signaturePath); await io.rmRF(signaturePath);
await io.rmRF(gpgHome); await io.rmRF(gpgHome);
} }
+15
View File
@@ -510,6 +510,21 @@ export function convertVersionToSemver(version: number[] | string) {
return mainVersion; return mainVersion;
} }
/**
* Java versions (JEP 322) can contain more numeric fields than SemVer allows,
* e.g. '11.0.9.1' or Temurin respins such as '26.0.2.1+1'. Move the extra
* fields into SemVer build metadata ('11.0.9+1', '26.0.2+1.1'). Any other
* input (ranges, regular SemVer versions) is returned unchanged.
*/
export function normalizeJavaVersionToSemver(version: string): string {
const match = /^(\d+(?:\.\d+){3,})(?:\+([0-9A-Za-z.-]+))?$/.exec(version);
if (!match) {
return version;
}
const converted = convertVersionToSemver(match[1]);
return match[2] ? `${converted}.${match[2]}` : converted;
}
/** /**
* Builds a validator for the bytes currently served by a URL from the response * Builds a validator for the bytes currently served by a URL from the response
* headers of a HEAD request. A vendor's `/latest/` URL never changes, so this * headers of a HEAD request. A vendor's `/latest/` URL never changes, so this