diff --git a/__tests__/distributors/adopt-installer.test.ts b/__tests__/distributors/adopt-installer.test.ts index ff477be0..5442d3f6 100644 --- a/__tests__/distributors/adopt-installer.test.ts +++ b/__tests__/distributors/adopt-installer.test.ts @@ -14,6 +14,7 @@ import * as core from '@actions/core'; describe('getAvailableVersions', () => { let spyHttpClient: jest.SpyInstance; let spyCoreError: jest.SpyInstance; + let spyCoreWarning: jest.SpyInstance; beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -26,6 +27,8 @@ describe('getAvailableVersions', () => { // Mock core.error to suppress error logs spyCoreError = jest.spyOn(core, 'error'); spyCoreError.mockImplementation(() => {}); + spyCoreWarning = jest.spyOn(core, 'warning'); + spyCoreWarning.mockImplementation(() => {}); }); afterEach(() => { @@ -136,22 +139,19 @@ describe('getAvailableVersions', () => { ); it('load available versions', async () => { + const nextPageUrl = + 'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient .mockReturnValueOnce({ statusCode: 200, - headers: {}, + headers: {link: `<${nextPageUrl}>; rel="next"`}, result: manifestData as any }) .mockReturnValueOnce({ statusCode: 200, headers: {}, result: manifestData as any - }) - .mockReturnValueOnce({ - statusCode: 200, - headers: {}, - result: [] }); const distribution = new AdoptDistribution( @@ -166,6 +166,33 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); + }); + + it('stops pagination after 1000 pages as a safeguard', async () => { + const nextPageUrl = 'https://example.com/releases?page=2'; + spyHttpClient.mockReturnValue({ + statusCode: 200, + headers: {link: `<${nextPageUrl}>; rel="next"`}, + result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any + }); + + const distribution = new AdoptDistribution( + { + version: '11', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }, + AdoptImplementation.Hotspot + ); + + await distribution['getAvailableVersions'](); + + expect(spyHttpClient).toHaveBeenCalledTimes(1000); + expect(spyCoreWarning).toHaveBeenCalledWith( + expect.stringContaining('Reached pagination safeguard limit (1000 pages)') + ); }); it.each([ diff --git a/__tests__/distributors/semeru-installer.test.ts b/__tests__/distributors/semeru-installer.test.ts index 1c26c79a..ffef9530 100644 --- a/__tests__/distributors/semeru-installer.test.ts +++ b/__tests__/distributors/semeru-installer.test.ts @@ -9,6 +9,7 @@ import * as core from '@actions/core'; describe('getAvailableVersions', () => { let spyHttpClient: jest.SpyInstance; let spyCoreError: jest.SpyInstance; + let spyCoreWarning: jest.SpyInstance; beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -20,6 +21,8 @@ describe('getAvailableVersions', () => { // Mock core.error to suppress error logs spyCoreError = jest.spyOn(core, 'error'); spyCoreError.mockImplementation(() => {}); + spyCoreWarning = jest.spyOn(core, 'warning'); + spyCoreWarning.mockImplementation(() => {}); }); afterEach(() => { @@ -82,22 +85,19 @@ describe('getAvailableVersions', () => { ); it('load available versions', async () => { + const nextPageUrl = + 'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient .mockReturnValueOnce({ statusCode: 200, - headers: {}, + headers: {link: `<${nextPageUrl}>; rel="next"`}, result: manifestData as any }) .mockReturnValueOnce({ statusCode: 200, headers: {}, result: manifestData as any - }) - .mockReturnValueOnce({ - statusCode: 200, - headers: {}, - result: [] }); const distribution = new SemeruDistribution({ @@ -109,6 +109,30 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); + }); + + it('stops pagination after 1000 pages as a safeguard', async () => { + const nextPageUrl = 'https://example.com/releases?page=2'; + spyHttpClient.mockReturnValue({ + statusCode: 200, + headers: {link: `<${nextPageUrl}>; rel="next"`}, + result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any + }); + + const distribution = new SemeruDistribution({ + version: '8', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + await distribution['getAvailableVersions'](); + + expect(spyHttpClient).toHaveBeenCalledTimes(1000); + expect(spyCoreWarning).toHaveBeenCalledWith( + expect.stringContaining('Reached pagination safeguard limit (1000 pages)') + ); }); it.each([ diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 0c6ef3f5..49a52a7d 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -12,6 +12,7 @@ import * as core from '@actions/core'; describe('getAvailableVersions', () => { let spyHttpClient: jest.SpyInstance; let spyCoreError: jest.SpyInstance; + let spyCoreWarning: jest.SpyInstance; beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -23,6 +24,8 @@ describe('getAvailableVersions', () => { // Mock core.error to suppress error logs spyCoreError = jest.spyOn(core, 'error'); spyCoreError.mockImplementation(() => {}); + spyCoreWarning = jest.spyOn(core, 'warning'); + spyCoreWarning.mockImplementation(() => {}); }); afterEach(() => { @@ -93,22 +96,19 @@ describe('getAvailableVersions', () => { ); it('load available versions', async () => { + const nextPageUrl = + 'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient .mockReturnValueOnce({ statusCode: 200, - headers: {}, + headers: {link: `<${nextPageUrl}>; rel="next"`}, result: manifestData as any }) .mockReturnValueOnce({ statusCode: 200, headers: {}, result: manifestData as any - }) - .mockReturnValueOnce({ - statusCode: 200, - headers: {}, - result: [] }); const distribution = new TemurinDistribution( @@ -123,6 +123,33 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); + }); + + it('stops pagination after 1000 pages as a safeguard', async () => { + const nextPageUrl = 'https://example.com/releases?page=2'; + spyHttpClient.mockReturnValue({ + statusCode: 200, + headers: {link: `<${nextPageUrl}>; rel="next"`}, + result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any + }); + + const distribution = new TemurinDistribution( + { + version: '8', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }, + TemurinImplementation.Hotspot + ); + + await distribution['getAvailableVersions'](); + + expect(spyHttpClient).toHaveBeenCalledTimes(1000); + expect(spyCoreWarning).toHaveBeenCalledWith( + expect.stringContaining('Reached pagination safeguard limit (1000 pages)') + ); }); it.each([ diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts index 85b76069..a0c11de3 100644 --- a/__tests__/util.test.ts +++ b/__tests__/util.test.ts @@ -4,6 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { convertVersionToSemver, + getNextPageUrlFromLinkHeader, getVersionFromFileContent, isVersionSatisfies, isCacheFeatureAvailable, @@ -85,6 +86,27 @@ describe('convertVersionToSemver', () => { }); }); +describe('getNextPageUrlFromLinkHeader', () => { + it.each([ + [ + { + link: '; rel="next"' + }, + 'https://api.adoptium.net/v3/info/release_versions?page=1&page_size=10' + ], + [ + { + Link: '; rel="last", ; rel="next"' + }, + 'https://example.com/next?page=2' + ], + [{link: '; rel="last"'}, null], + [undefined, null] + ])('returns %s -> %s', (headers, expected) => { + expect(getNextPageUrlFromLinkHeader(headers)).toBe(expected); + }); +}); + describe('getVersionFromFileContent', () => { describe('.sdkmanrc', () => { it.each([ diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index df5d6c0e..6804a7be 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -51888,7 +51888,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.renameWinArchive = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; +exports.renameWinArchive = exports.getNextPageUrlFromLinkHeader = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; const os_1 = __importDefault(__nccwpck_require__(22037)); const path_1 = __importDefault(__nccwpck_require__(71017)); const fs = __importStar(__nccwpck_require__(57147)); @@ -52055,6 +52055,22 @@ function getGitHubHttpHeaders() { return headers; } exports.getGitHubHttpHeaders = getGitHubHttpHeaders; +function getNextPageUrlFromLinkHeader(headers) { + var _a, _b; + if (!headers) { + return null; + } + const linkHeader = (_a = headers.link) !== null && _a !== void 0 ? _a : headers.Link; + if (!linkHeader) { + return null; + } + const normalizedLinkHeader = Array.isArray(linkHeader) + ? linkHeader.join(',') + : linkHeader; + const nextLinkMatch = normalizedLinkHeader.match(/<([^>]+)>\s*;\s*rel="?next"?/i); + return (_b = nextLinkMatch === null || nextLinkMatch === void 0 ? void 0 : nextLinkMatch[1]) !== null && _b !== void 0 ? _b : null; +} +exports.getNextPageUrlFromLinkHeader = getNextPageUrlFromLinkHeader; // Rename archive to add extension because after downloading // archive does not contain extension type and it leads to some issues // on Windows runners without PowerShell Core. diff --git a/dist/setup/index.js b/dist/setup/index.js index fb687e03..5dc4a958 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -77569,6 +77569,7 @@ const path_1 = __importDefault(__nccwpck_require__(71017)); const semver_1 = __importDefault(__nccwpck_require__(11383)); const base_installer_1 = __nccwpck_require__(59741); const util_1 = __nccwpck_require__(92629); +const MAX_PAGINATION_PAGES = 1000; var AdoptImplementation; (function (AdoptImplementation) { AdoptImplementation["Hotspot"] = "Hotspot"; @@ -77650,24 +77651,26 @@ class AdoptDistribution extends base_installer_1.JavaBase { `release_type=${releaseType}`, `jvm_impl=${this.jvmImpl.toLowerCase()}` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adopt API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug(`Gathering available versions from '${availableVersionsUrl}'`); - } - const paginationPage = (yield this.http.getJson(availableVersionsUrl)).result; + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + while (availableVersionsUrl) { + pageCount++; + const response = yield this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + availableVersionsUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning(`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Adopt releases.`); + break; + } } if (core.isDebug()) { core.startGroup('Print information about available versions'); @@ -79730,6 +79733,7 @@ const core = __importStar(__nccwpck_require__(42186)); const tc = __importStar(__nccwpck_require__(27784)); const fs_1 = __importDefault(__nccwpck_require__(57147)); const path_1 = __importDefault(__nccwpck_require__(71017)); +const MAX_PAGINATION_PAGES = 1000; const supportedArchitectures = [ 'x64', 'x86', @@ -79825,24 +79829,26 @@ class SemeruDistribution extends base_installer_1.JavaBase { `release_type=${releaseType}`, `jvm_impl=openj9` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug(`Gathering available versions from '${availableVersionsUrl}'`); - } - const paginationPage = (yield this.http.getJson(availableVersionsUrl)).result; + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + while (availableVersionsUrl) { + pageCount++; + const response = yield this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + availableVersionsUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning(`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Semeru releases.`); + break; + } } if (core.isDebug()) { core.startGroup('Print information about available versions'); @@ -79920,6 +79926,7 @@ const path_1 = __importDefault(__nccwpck_require__(71017)); const semver_1 = __importDefault(__nccwpck_require__(11383)); const base_installer_1 = __nccwpck_require__(59741); const util_1 = __nccwpck_require__(92629); +const MAX_PAGINATION_PAGES = 1000; var TemurinImplementation; (function (TemurinImplementation) { TemurinImplementation["Hotspot"] = "Hotspot"; @@ -79999,24 +80006,26 @@ class TemurinDistribution extends base_installer_1.JavaBase { `release_type=${releaseType}`, `jvm_impl=${this.jvmImpl.toLowerCase()}` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug(`Gathering available versions from '${availableVersionsUrl}'`); - } - const paginationPage = (yield this.http.getJson(availableVersionsUrl)).result; + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + while (availableVersionsUrl) { + pageCount++; + const response = yield this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + availableVersionsUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning(`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Temurin releases.`); + break; + } } if (core.isDebug()) { core.startGroup('Print information about available versions'); @@ -80642,7 +80651,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.renameWinArchive = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; +exports.renameWinArchive = exports.getNextPageUrlFromLinkHeader = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; const os_1 = __importDefault(__nccwpck_require__(22037)); const path_1 = __importDefault(__nccwpck_require__(71017)); const fs = __importStar(__nccwpck_require__(57147)); @@ -80809,6 +80818,22 @@ function getGitHubHttpHeaders() { return headers; } exports.getGitHubHttpHeaders = getGitHubHttpHeaders; +function getNextPageUrlFromLinkHeader(headers) { + var _a, _b; + if (!headers) { + return null; + } + const linkHeader = (_a = headers.link) !== null && _a !== void 0 ? _a : headers.Link; + if (!linkHeader) { + return null; + } + const normalizedLinkHeader = Array.isArray(linkHeader) + ? linkHeader.join(',') + : linkHeader; + const nextLinkMatch = normalizedLinkHeader.match(/<([^>]+)>\s*;\s*rel="?next"?/i); + return (_b = nextLinkMatch === null || nextLinkMatch === void 0 ? void 0 : nextLinkMatch[1]) !== null && _b !== void 0 ? _b : null; +} +exports.getNextPageUrlFromLinkHeader = getNextPageUrlFromLinkHeader; // Rename archive to add extension because after downloading // archive does not contain extension type and it leads to some issues // on Windows runners without PowerShell Core. diff --git a/package-lock.json b/package-lock.json index 24f6a21f..5b39f749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1465,6 +1465,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3032,9 +3044,9 @@ "dev": true }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -3043,13 +3055,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.10.tgz", - "integrity": "sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", "funding": [ { "type": "github", @@ -3058,9 +3071,11 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.1", - "strnum": "^2.2.2" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -4562,9 +4577,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.4.0.tgz", - "integrity": "sha512-s4DQMxIdhj3jLFWd9LxHOplj4p9yQ4ffMGowFf3cpEgrrJjEhN0V5nxw4Ye1EViAGDoL4/1AeO6qHpqYPOzE4Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -5102,9 +5117,9 @@ } }, "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -5438,6 +5453,21 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xmlbuilder2": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", diff --git a/src/distributions/adopt/installer.ts b/src/distributions/adopt/installer.ts index 34c1716c..e1200564 100644 --- a/src/distributions/adopt/installer.ts +++ b/src/distributions/adopt/installer.ts @@ -14,11 +14,14 @@ import { } from '../base-models'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, renameWinArchive } from '../../util'; +const MAX_PAGINATION_PAGES = 1000; + export enum AdoptImplementation { Hotspot = 'Hotspot', OpenJ9 = 'OpenJ9' @@ -125,30 +128,35 @@ export class AdoptDistribution extends JavaBase { `jvm_impl=${this.jvmImpl.toLowerCase()}` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adopt API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl: string | null = + `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions: IAdoptAvailableVersions[] = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug( - `Gathering available versions from '${availableVersionsUrl}'` - ); - } + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( - await this.http.getJson(availableVersionsUrl) - ).result; + while (availableVersionsUrl) { + pageCount++; + const response = + await this.http.getJson( + availableVersionsUrl + ); + const paginationPage = response.result; + availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning( + `Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Adopt releases.` + ); + break; + } } if (core.isDebug()) { diff --git a/src/distributions/semeru/installer.ts b/src/distributions/semeru/installer.ts index edb29480..14adc89f 100644 --- a/src/distributions/semeru/installer.ts +++ b/src/distributions/semeru/installer.ts @@ -7,6 +7,7 @@ import { import semver from 'semver'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, renameWinArchive @@ -17,6 +18,8 @@ import fs from 'fs'; import path from 'path'; import {ISemeruAvailableVersions} from './models'; +const MAX_PAGINATION_PAGES = 1000; + const supportedArchitectures = [ 'x64', 'x86', @@ -155,32 +158,35 @@ export class SemeruDistribution extends JavaBase { `jvm_impl=openj9` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl: string | null = + `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions: ISemeruAvailableVersions[] = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug( - `Gathering available versions from '${availableVersionsUrl}'` - ); - } + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( + while (availableVersionsUrl) { + pageCount++; + const response = await this.http.getJson( availableVersionsUrl - ) - ).result; + ); + const paginationPage = response.result; + availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning( + `Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Semeru releases.` + ); + break; + } } if (core.isDebug()) { diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 8d03de74..225ced96 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -14,11 +14,14 @@ import { } from '../base-models'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, renameWinArchive } from '../../util'; +const MAX_PAGINATION_PAGES = 1000; + export enum TemurinImplementation { Hotspot = 'Hotspot' } @@ -123,32 +126,36 @@ export class TemurinDistribution extends JavaBase { `jvm_impl=${this.jvmImpl.toLowerCase()}` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl: string | null = + `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions: ITemurinAvailableVersions[] = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug( - `Gathering available versions from '${availableVersionsUrl}'` - ); - } + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( + while (availableVersionsUrl) { + pageCount++; + const response = await this.http.getJson( availableVersionsUrl - ) - ).result; + ); + const paginationPage = response.result; + availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers); + if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning( + `Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Temurin releases.` + ); + break; + } } if (core.isDebug()) { diff --git a/src/util.ts b/src/util.ts index 0325f7f4..43c004ec 100644 --- a/src/util.ts +++ b/src/util.ts @@ -201,6 +201,28 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders { return headers; } +export function getNextPageUrlFromLinkHeader( + headers?: Record +): string | null { + if (!headers) { + return null; + } + + const linkHeader = headers.link ?? headers.Link; + if (!linkHeader) { + return null; + } + + const normalizedLinkHeader = Array.isArray(linkHeader) + ? linkHeader.join(',') + : linkHeader; + const nextLinkMatch = normalizedLinkHeader.match( + /<([^>]+)>\s*;\s*rel="?next"?/i + ); + + return nextLinkMatch?.[1] ?? null; +} + // Rename archive to add extension because after downloading // archive does not contain extension type and it leads to some issues // on Windows runners without PowerShell Core.