From f1fadef4b33601303a90cde57d740a6d0b0906d9 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 11 Aug 2026 11:17:42 -0400 Subject: [PATCH] unity-setup@v2.6.1 - bump unity-cli@v3.0.2 - bump deps --- dist/index.js | 77652 ++++++++++++++++++++++---------------------- dist/index.js.map | 2 +- dist/licenses.txt | 183 +- package-lock.json | 658 +- package.json | 12 +- 5 files changed, 39488 insertions(+), 39019 deletions(-) diff --git a/dist/index.js b/dist/index.js index ce740c4..a064944 100644 --- a/dist/index.js +++ b/dist/index.js @@ -49,7 +49,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.FinalizeCacheError = exports.ReserveCacheError = exports.ValidationError = void 0; +exports.FinalizeCacheError = exports.CacheReadDeniedError = exports.CACHE_READ_DENIED_PREFIX = exports.CacheWriteDeniedError = exports.CACHE_WRITE_DENIED_PREFIX = exports.ReserveCacheError = exports.ValidationError = void 0; exports.isFeatureAvailable = isFeatureAvailable; exports.restoreCache = restoreCache; exports.saveCache = saveCache; @@ -61,6 +61,7 @@ const cacheTwirpClient = __importStar(__nccwpck_require__(82502)); const config_1 = __nccwpck_require__(35147); const tar_1 = __nccwpck_require__(56490); const http_client_1 = __nccwpck_require__(96255); +const constants_1 = __nccwpck_require__(88840); class ValidationError extends Error { constructor(message) { super(message); @@ -77,6 +78,40 @@ class ReserveCacheError extends Error { } } exports.ReserveCacheError = ReserveCacheError; +/** + * Stable prefix used by the cache receiver to signal that the token has + * no writable scopes (read-only cache policy). Consumers can match on + * this prefix to distinguish policy denials from ordinary contention. + */ +exports.CACHE_WRITE_DENIED_PREFIX = 'cache write denied:'; +/** + * Extends ReserveCacheError for source-compatibility: existing + * `instanceof ReserveCacheError` checks and `typedError.name === + * ReserveCacheError.name` paths keep working, while consumers that want to + * distinguish a policy denial can check for CacheWriteDeniedError.name. + */ +class CacheWriteDeniedError extends ReserveCacheError { + constructor(message) { + super(message); + this.name = 'CacheWriteDeniedError'; + Object.setPrototypeOf(this, CacheWriteDeniedError.prototype); + } +} +exports.CacheWriteDeniedError = CacheWriteDeniedError; +// Re-exported from constants so consumers keep referencing it here; the shared +// value also drives detection in cacheHttpClient without duplicating the string. +exports.CACHE_READ_DENIED_PREFIX = constants_1.CacheReadDeniedMessagePrefix; +// Raised when the cache backend denies a download URL because the run's token +// has no readable cache scopes. Caching is best-effort, so restoreCache logs a +// warning and reports a cache miss rather than rethrowing this. +class CacheReadDeniedError extends Error { + constructor(message) { + super(message); + this.name = 'CacheReadDeniedError'; + Object.setPrototypeOf(this, CacheReadDeniedError.prototype); + } +} +exports.CacheReadDeniedError = CacheReadDeniedError; class FinalizeCacheError extends Error { constructor(message) { super(message); @@ -132,6 +167,12 @@ function restoreCache(paths_1, primaryKey_1, restoreKeys_1, options_1) { const cacheServiceVersion = (0, config_1.getCacheServiceVersion)(); core.debug(`Cache service version: ${cacheServiceVersion}`); checkPaths(paths); + const cacheMode = (0, config_1.getCacheMode)(); + if (!(0, config_1.isCacheReadable)(cacheMode)) { + core.info(`Cache restore skipped: the effective cache-mode '${cacheMode}' does not permit reads.`); + core.debug(`Skipped restore for paths [${paths.join(', ')}] with primary key '${primaryKey}'.`); + return undefined; + } switch (cacheServiceVersion) { case 'v2': return yield restoreCacheV2(paths, primaryKey, restoreKeys, options, enableCrossOsArchive); @@ -153,6 +194,7 @@ function restoreCache(paths_1, primaryKey_1, restoreKeys_1, options_1) { */ function restoreCacheV1(paths_1, primaryKey_1, restoreKeys_1, options_1) { return __awaiter(this, arguments, void 0, function* (paths, primaryKey, restoreKeys, options, enableCrossOsArchive = false) { + var _a; restoreKeys = restoreKeys || []; const keys = [primaryKey, ...restoreKeys]; core.debug('Resolved Keys:'); @@ -167,10 +209,26 @@ function restoreCacheV1(paths_1, primaryKey_1, restoreKeys_1, options_1) { let archivePath = ''; try { // path are needed to compute version - const cacheEntry = yield cacheHttpClient.getCacheEntry(keys, paths, { - compressionMethod, - enableCrossOsArchive - }); + let cacheEntry; + try { + cacheEntry = yield cacheHttpClient.getCacheEntry(keys, paths, { + compressionMethod, + enableCrossOsArchive + }); + } + catch (error) { + // The v1 artifact cache service returns HTTP 403 with a + // `cache read denied:` body when the run's token has no readable cache + // scopes. getCacheEntry lives in a dependency-free internal module and + // cannot import CacheReadDeniedError without a circular dependency, so it + // only surfaces the raw denial message; we classify it into the typed + // error here so the outer catch and consumers can dispatch on it. + const errorMessage = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : ''; + if (errorMessage.includes(exports.CACHE_READ_DENIED_PREFIX)) { + throw new CacheReadDeniedError(errorMessage); + } + throw error; + } if (!(cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.archiveLocation)) { // Cache not found return undefined; @@ -199,7 +257,9 @@ function restoreCacheV1(paths_1, primaryKey_1, restoreKeys_1, options_1) { } else { // warn on cache restore failure and continue build - // Log server errors (5xx) as errors, all other errors as warnings + // Log server errors (5xx) as errors, all other errors as warnings. + // A read denied by policy (CacheReadDeniedError) is not an HttpClientError + // so it falls here and is warned, treated as a cache miss. if (typedError instanceof http_client_1.HttpClientError && typeof typedError.statusCode === 'number' && typedError.statusCode >= 500) { @@ -234,6 +294,7 @@ function restoreCacheV1(paths_1, primaryKey_1, restoreKeys_1, options_1) { */ function restoreCacheV2(paths_1, primaryKey_1, restoreKeys_1, options_1) { return __awaiter(this, arguments, void 0, function* (paths, primaryKey, restoreKeys, options, enableCrossOsArchive = false) { + var _a; // Override UploadOptions to force the use of Azure options = Object.assign(Object.assign({}, options), { useAzureSdk: true }); restoreKeys = restoreKeys || []; @@ -255,7 +316,20 @@ function restoreCacheV2(paths_1, primaryKey_1, restoreKeys_1, options_1) { restoreKeys, version: utils.getCacheVersion(paths, compressionMethod, enableCrossOsArchive) }; - const response = yield twirpClient.GetCacheEntryDownloadURL(request); + let response; + try { + response = yield twirpClient.GetCacheEntryDownloadURL(request); + } + catch (error) { + // The receiver returns twirp PermissionDenied (403) when the run's token + // has no readable cache scopes. The client wraps that 403, so the stable + // prefix is embedded in the message rather than leading it. + const errorMessage = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : ''; + if (errorMessage.includes(exports.CACHE_READ_DENIED_PREFIX)) { + throw new CacheReadDeniedError(errorMessage); + } + throw error; + } if (!response.ok) { core.debug(`Cache not found for version ${request.version} of keys: ${keys.join(', ')}`); return undefined; @@ -291,7 +365,9 @@ function restoreCacheV2(paths_1, primaryKey_1, restoreKeys_1, options_1) { } else { // Supress all non-validation cache related errors because caching should be optional - // Log server errors (5xx) as errors, all other errors as warnings + // Log server errors (5xx) as errors, all other errors as warnings. + // A read denied by policy (CacheReadDeniedError) is not an HttpClientError + // so it falls here and is warned, treated as a cache miss. if (typedError instanceof http_client_1.HttpClientError && typeof typedError.statusCode === 'number' && typedError.statusCode >= 500) { @@ -330,6 +406,12 @@ function saveCache(paths_1, key_1, options_1) { core.debug(`Cache service version: ${cacheServiceVersion}`); checkPaths(paths); checkKey(key); + const cacheMode = (0, config_1.getCacheMode)(); + if (!(0, config_1.isCacheWritable)(cacheMode)) { + core.info(`Cache save skipped: the effective cache-mode '${cacheMode}' does not permit writes.`); + core.debug(`Skipped save for paths [${paths.join(', ')}] with key '${key}'.`); + return -1; + } switch (cacheServiceVersion) { case 'v2': return yield saveCacheV2(paths, key, options, enableCrossOsArchive); @@ -387,7 +469,11 @@ function saveCacheV1(paths_1, key_1, options_1) { throw new Error((_d = (_c = reserveCacheResponse === null || reserveCacheResponse === void 0 ? void 0 : reserveCacheResponse.error) === null || _c === void 0 ? void 0 : _c.message) !== null && _d !== void 0 ? _d : `Cache size of ~${Math.round(archiveFileSize / (1024 * 1024))} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.`); } else { - throw new ReserveCacheError(`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${(_e = reserveCacheResponse === null || reserveCacheResponse === void 0 ? void 0 : reserveCacheResponse.error) === null || _e === void 0 ? void 0 : _e.message}`); + const detailMessage = (_e = reserveCacheResponse === null || reserveCacheResponse === void 0 ? void 0 : reserveCacheResponse.error) === null || _e === void 0 ? void 0 : _e.message; + if (detailMessage === null || detailMessage === void 0 ? void 0 : detailMessage.startsWith(exports.CACHE_WRITE_DENIED_PREFIX)) { + throw new CacheWriteDeniedError(`Unable to reserve cache with key ${key}. More details: ${detailMessage}`); + } + throw new ReserveCacheError(`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${detailMessage}`); } core.debug(`Saving Cache (ID: ${cacheId})`); yield cacheHttpClient.saveCache(cacheId, archivePath, '', options); @@ -397,6 +483,9 @@ function saveCacheV1(paths_1, key_1, options_1) { if (typedError.name === ValidationError.name) { throw error; } + else if (typedError.name === CacheWriteDeniedError.name) { + core.warning(`Failed to save: ${typedError.message}`); + } else if (typedError.name === ReserveCacheError.name) { core.info(`Failed to save: ${typedError.message}`); } @@ -435,6 +524,7 @@ function saveCacheV1(paths_1, key_1, options_1) { */ function saveCacheV2(paths_1, key_1, options_1) { return __awaiter(this, arguments, void 0, function* (paths, key, options, enableCrossOsArchive = false) { + var _a; // Override UploadOptions to force the use of Azure // ...options goes first because we want to override the default values // set in UploadOptions with these specific figures @@ -470,7 +560,11 @@ function saveCacheV2(paths_1, key_1, options_1) { try { const response = yield twirpClient.CreateCacheEntry(request); if (!response.ok) { - if (response.message) { + // Skip the redundant inner warning when the receiver signalled a + // policy denial: the outer catch arm below will log a single + // customer-facing warning. + if (response.message && + !response.message.startsWith(exports.CACHE_WRITE_DENIED_PREFIX)) { core.warning(`Cache reservation failed: ${response.message}`); } throw new Error(response.message || 'Response was not ok'); @@ -479,6 +573,10 @@ function saveCacheV2(paths_1, key_1, options_1) { } catch (error) { core.debug(`Failed to reserve cache: ${error}`); + const errorMessage = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : ''; + if (errorMessage.startsWith(exports.CACHE_WRITE_DENIED_PREFIX)) { + throw new CacheWriteDeniedError(`Unable to reserve cache with key ${key}. More details: ${errorMessage}`); + } throw new ReserveCacheError(`Unable to reserve cache with key ${key}, another job may be creating this cache.`); } core.debug(`Attempting to upload cache located at: ${archivePath}`); @@ -503,6 +601,9 @@ function saveCacheV2(paths_1, key_1, options_1) { if (typedError.name === ValidationError.name) { throw error; } + else if (typedError.name === CacheWriteDeniedError.name) { + core.warning(`Failed to save: ${typedError.message}`); + } else if (typedError.name === ReserveCacheError.name) { core.info(`Failed to save: ${typedError.message}`); } @@ -1225,6 +1326,7 @@ const downloadUtils_1 = __nccwpck_require__(55500); const options_1 = __nccwpck_require__(76215); const requestUtils_1 = __nccwpck_require__(13981); const config_1 = __nccwpck_require__(35147); +const constants_1 = __nccwpck_require__(88840); const user_agent_1 = __nccwpck_require__(580); function getCacheApiUrl(resource) { const baseUrl = (0, config_1.getCacheServiceURL)(); @@ -1253,6 +1355,7 @@ function createHttpClient() { } function getCacheEntry(keys, paths, options) { return __awaiter(this, void 0, void 0, function* () { + var _a; const httpClient = createHttpClient(); const version = utils.getCacheVersion(paths, options === null || options === void 0 ? void 0 : options.compressionMethod, options === null || options === void 0 ? void 0 : options.enableCrossOsArchive); const resource = `cache?keys=${encodeURIComponent(keys.join(','))}&version=${version}`; @@ -1266,6 +1369,12 @@ function getCacheEntry(keys, paths, options) { return null; } if (!(0, requestUtils_1.isSuccessStatusCode)(response.statusCode)) { + // Only surface the receiver's body for a `cache read denied:` policy denial + // so callers can dispatch on it; keep the generic message otherwise. + const errorMessage = (_a = response.error) === null || _a === void 0 ? void 0 : _a.message; + if (errorMessage === null || errorMessage === void 0 ? void 0 : errorMessage.includes(constants_1.CacheReadDeniedMessagePrefix)) { + throw new Error(errorMessage); + } throw new Error(`Cache service responded with ${response.statusCode}`); } const cacheResult = response.result; @@ -1674,6 +1783,9 @@ function getRuntimeToken() { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.isGhes = isGhes; exports.getCacheServiceVersion = getCacheServiceVersion; +exports.getCacheMode = getCacheMode; +exports.isCacheReadable = isCacheReadable; +exports.isCacheWritable = isCacheWritable; exports.getCacheServiceURL = getCacheServiceURL; function isGhes() { const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); @@ -1690,6 +1802,24 @@ function getCacheServiceVersion() { return 'v1'; return process.env['ACTIONS_CACHE_SERVICE_V2'] ? 'v2' : 'v1'; } +// The cache-mode lattice: readable = {read, write}, writable = {write, +// write-only}, none = neither. +const KNOWN_CACHE_MODES = ['none', 'read', 'write', 'write-only']; +// The effective cache-mode exported by the runner, or '' when not set. +function getCacheMode() { + return (process.env['ACTIONS_CACHE_MODE'] || '').trim().toLowerCase(); +} +// Unset or unrecognized modes are permissive so behavior matches today. +function isCacheReadable(mode) { + if (!KNOWN_CACHE_MODES.includes(mode)) + return true; + return mode === 'read' || mode === 'write'; +} +function isCacheWritable(mode) { + if (!KNOWN_CACHE_MODES.includes(mode)) + return true; + return mode === 'write' || mode === 'write-only'; +} function getCacheServiceURL() { const version = getCacheServiceVersion(); // Based on the version of the cache service, we will determine which @@ -1715,7 +1845,7 @@ function getCacheServiceURL() { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.CacheFileSizeLimit = exports.ManifestFilename = exports.TarFilename = exports.SystemTarPathOnWindows = exports.GnuTarPathOnWindows = exports.SocketTimeout = exports.DefaultRetryDelay = exports.DefaultRetryAttempts = exports.ArchiveToolType = exports.CompressionMethod = exports.CacheFilename = void 0; +exports.CacheReadDeniedMessagePrefix = exports.CacheFileSizeLimit = exports.ManifestFilename = exports.TarFilename = exports.SystemTarPathOnWindows = exports.GnuTarPathOnWindows = exports.SocketTimeout = exports.DefaultRetryDelay = exports.DefaultRetryAttempts = exports.ArchiveToolType = exports.CompressionMethod = exports.CacheFilename = void 0; var CacheFilename; (function (CacheFilename) { CacheFilename["Gzip"] = "cache.tgz"; @@ -1749,6 +1879,10 @@ exports.SystemTarPathOnWindows = `${process.env['SYSTEMDRIVE']}\\Windows\\System exports.TarFilename = 'cache.tar'; exports.ManifestFilename = 'manifest.txt'; exports.CacheFileSizeLimit = 10 * Math.pow(1024, 3); // 10GiB per repository +// Prefix the cache backend embeds in a read-denial message (v2 twirp +// GetCacheEntryDownloadURL error or the GHES v1 `_apis/artifactcache` 403 body). +// Shared so cache.ts and cacheHttpClient.ts match the same contract value. +exports.CacheReadDeniedMessagePrefix = 'cache read denied:'; //# sourceMappingURL=constants.js.map /***/ }), @@ -20313,6 +20447,7 @@ exports.buildUnitTestJobSummaryMarkdown = buildUnitTestJobSummaryMarkdown; exports.truncateStringToUtf8ByteLength = truncateStringToUtf8ByteLength; exports.stripSummaryNoiseFromLogMessage = stripSummaryNoiseFromLogMessage; const utp_1 = __nccwpck_require__(16282); +const utp_benign_1 = __nccwpck_require__(26239); const github_actions_ci_1 = __nccwpck_require__(89644); const logger_provider_1 = __nccwpck_require__(32416); /** Severity order for display: Error first, then Warning, then Info. Undefined treats as Warning. */ @@ -20695,18 +20830,31 @@ function formatDurationMsForSummary(ms) { return `${(ms / 1000).toFixed(1)}s`; } /** Unity/CI noise shown in logs; omit from workflow summary foldouts and counts. */ -const SUMMARY_NOISE_ACCESS_TOKEN = 'Access token is unavailable; failed to update'; +function buildSummaryNoisePatterns() { + return utp_benign_1.UTP_BENIGN_SEVERITY_REMAPS.map(({ fragment }) => { + const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Multicast lines often include "(err: 10013)." — strip the whole clause. + if (fragment.includes('multicast group')) { + return new RegExp(`${escaped}(?:\\s*\\(err:\\s*\\d+\\))?\\.?`, 'gi'); + } + return new RegExp(escaped, 'gi'); + }); +} +const SUMMARY_NOISE_PATTERNS = buildSummaryNoisePatterns(); /** * Removes known noise phrases from a log message for summary display. - * Exported for unit tests. + * Exported for unit tests. Fragments come from {@link UTP_BENIGN_SEVERITY_REMAPS}. */ function stripSummaryNoiseFromLogMessage(message) { const flat = toSingleLineText(message); if (!flat) return ''; - const pattern = SUMMARY_NOISE_ACCESS_TOKEN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const out = flat.replace(new RegExp(pattern, 'gi'), ' ').replace(/\s+/g, ' ').trim(); - return out; + let out = flat; + for (const pattern of SUMMARY_NOISE_PATTERNS) { + pattern.lastIndex = 0; + out = out.replace(pattern, ' '); + } + return out.replace(/\s+/g, ' ').trim(); } function filterNoiseFromSummaryLogEntries(entries) { const out = []; @@ -21238,6 +21386,7 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.UnityEditor = void 0; const fs = __importStar(__nccwpck_require__(57147)); +const os = __importStar(__nccwpck_require__(22037)); const path = __importStar(__nccwpck_require__(71017)); const logging_1 = __nccwpck_require__(44486); const unity_version_1 = __nccwpck_require__(97474); @@ -21583,33 +21732,66 @@ class UnityEditor { const timestamp = new Date().toISOString().replace(/[-:]/g, ``).replace(/\..+/, ``); return path.join(logsDir, `${prefix ? prefix + '-' : ''}Unity-${timestamp}.log`); } + /** + * Resolves a writable Pulse/XDG runtime directory. CI runners often lack systemd-logind's `/run/user/$UID` + * (Pulse then fails with "Failed to create secure directory (.../pulse)"). + */ + async resolveLinuxXdgRuntimeDir() { + const fromEnv = process.env.XDG_RUNTIME_DIR?.trim(); + if (fromEnv && fromEnv.length > 0) { + try { + await fs.promises.mkdir(fromEnv, { recursive: true, mode: 0o700 }); + } + catch (error) { + this.logger.debug(`Could not mkdir XDG_RUNTIME_DIR (${fromEnv}): ${error}`); + } + try { + await fs.promises.access(fromEnv, fs.constants.W_OK); + return fromEnv; + } + catch { + this.logger.debug(`XDG_RUNTIME_DIR from environment is not usable (${fromEnv}); falling back like unset.`); + } + } + const uid = typeof process.getuid === 'function' ? process.getuid() : 1000; + const systemdUser = `/run/user/${uid}`; + try { + await fs.promises.access(systemdUser, fs.constants.W_OK); + return systemdUser; + } + catch { + this.logger.debug(`Using tmp XDG_RUNTIME_DIR (not using ${systemdUser}: missing or not writable).`); + } + const fallback = path.join(os.tmpdir(), `unity-cli-xdg-runtime-${uid}`); + await fs.promises.mkdir(fallback, { recursive: true, mode: 0o700 }); + return fallback; + } async prepareLinuxAudioEnvironment() { if (process.platform !== 'linux') { return {}; } + const runtimeDir = await this.resolveLinuxXdgRuntimeDir(); const envOverrides = { SDL_AUDIODRIVER: process.env.SDL_AUDIODRIVER || 'dummy', AUDIODRIVER: process.env.AUDIODRIVER || 'dummy', - AUDIODEV: process.env.AUDIODEV || 'null', - ALSA_CARD: process.env.ALSA_CARD || 'Loopback', - PULSE_SINK: process.env.PULSE_SINK || 'unity_dummy' + AUDIODEV: process.env.AUDIODEV?.trim() || 'null', + PULSE_SINK: process.env.PULSE_SINK || 'unity_dummy', + XDG_RUNTIME_DIR: runtimeDir, }; - const defaultRuntimeDir = `/run/user/${typeof process.getuid === 'function' ? process.getuid() : 1000}`; - const runtimeDir = process.env.XDG_RUNTIME_DIR || defaultRuntimeDir; - envOverrides.XDG_RUNTIME_DIR = runtimeDir; - try { - await fs.promises.mkdir(runtimeDir, { recursive: true, mode: 0o700 }); - } - catch (error) { - this.logger.debug(`Failed to ensure XDG_RUNTIME_DIR (${runtimeDir}): ${error}`); - } - await this.tryExec('bash', ['-c', 'pulseaudio --check 2>/dev/null || pulseaudio --start --exit-idle-time=-1 || true']); - await this.tryExec('bash', ['-c', 'command -v pactl >/dev/null 2>&1 && { pactl list short sinks 2>/dev/null | grep -q unity_dummy || pactl load-module module-null-sink sink_name=unity_dummy sink_properties=device.description=UnityCI >/tmp/unity-null-sink.id; } || true']); + const alsaCard = process.env.ALSA_CARD?.trim(); + if (alsaCard && alsaCard.length > 0) { + envOverrides.ALSA_CARD = alsaCard; + } + await this.tryExec('bash', ['-c', 'pulseaudio --check 2>/dev/null || pulseaudio --start --exit-idle-time=-1 || true'], envOverrides); + await this.tryExec('bash', [ + '-c', + 'command -v pactl >/dev/null 2>&1 && { pactl list short sinks 2>/dev/null | grep -q unity_dummy || pactl load-module module-null-sink sink_name=unity_dummy sink_properties=device.description=UnityCI >/tmp/unity-null-sink.id; } || true', + ], envOverrides); return envOverrides; } - async tryExec(command, args) { + async tryExec(command, args, env) { try { - await (0, utilities_1.Exec)(command, args, { silent: true, showCommand: false }); + await (0, utilities_1.Exec)(command, args, { silent: true, showCommand: false, env }); } catch (error) { this.logger.debug(`Skipped helper command "${command} ${args.join(' ')}": ${error}`); @@ -21727,12 +21909,13 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.UnityHub = void 0; +exports.UnityHub = exports.LINUX_HUB_EXECUTABLE_LEGACY = exports.LINUX_HUB_EXECUTABLE_MODERN = void 0; +exports.resolveLinuxHubExecutable = resolveLinuxHubExecutable; const fs = __importStar(__nccwpck_require__(57147)); const os = __importStar(__nccwpck_require__(22037)); const path = __importStar(__nccwpck_require__(71017)); const yaml = __importStar(__nccwpck_require__(44083)); -const asar = __importStar(__nccwpck_require__(48767)); +const asar = __importStar(__nccwpck_require__(61066)); const child_process_1 = __nccwpck_require__(32081); const logging_1 = __nccwpck_require__(44486); const unity_editor_1 = __nccwpck_require__(68944); @@ -21742,6 +21925,113 @@ const utilities_1 = __nccwpck_require__(39746); const unity_releases_api_1 = __nccwpck_require__(47278); /** First Unity Hub line with native Windows ARM64 installers on the public CDN. */ const MIN_NATIVE_WINDOWS_ARM64_HUB_VERSION = (0, semver_1.coerce)('3.17.0'); +/** Allowed characters in a Debian package version (no shell metacharacters). */ +const LINUX_HUB_DEB_VERSION_RE = /^[0-9A-Za-z.+~:-]+$/; +/** Hub 3.20+ Electron Forge deb layout. */ +exports.LINUX_HUB_EXECUTABLE_MODERN = '/usr/lib/unityhub/unityhub'; +/** Hub ≤3.19 fpm / electron-builder layout. */ +exports.LINUX_HUB_EXECUTABLE_LEGACY = '/opt/unityhub/unityhub'; +/** + * Resolves the Unity Hub binary on Linux. + * Prefers UNITY_HUB_PATH, then the Hub 3.20+ path, then the legacy /opt path. + * When neither is present (pre-install), defaults to the modern path. + */ +function resolveLinuxHubExecutable(envPath = process.env.UNITY_HUB_PATH, existsSync = fs.existsSync) { + if (envPath !== undefined && envPath.length > 0) { + return envPath; + } + if (existsSync(exports.LINUX_HUB_EXECUTABLE_MODERN)) { + return exports.LINUX_HUB_EXECUTABLE_MODERN; + } + if (existsSync(exports.LINUX_HUB_EXECUTABLE_LEGACY)) { + return exports.LINUX_HUB_EXECUTABLE_LEGACY; + } + return exports.LINUX_HUB_EXECUTABLE_MODERN; +} +/** + * Fixed bootstrap for Linux Hub apt repo + update index. No user-controlled interpolation (CodeQL). + * Uses DEB822 .sources (Hub 3.20+) and removes legacy .list to avoid duplicate-source warnings. + */ +const LINUX_HUB_LINUX_UPDATE_REPO_BOOTSTRAP = `#!/bin/sh +set -e +wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null +sudo rm -f /etc/apt/sources.list.d/unityhub.list +sudo tee /etc/apt/sources.list.d/unityhub.sources >/dev/null <<'EOF' +Types: deb +URIs: https://hub.unity3d.com/linux/repos/deb +Suites: stable +Components: main +Signed-By: /usr/share/keyrings/Unity_Technologies_ApS.gpg +EOF +sudo apt-get update --allow-releaseinfo-change +`; +/** + * First phase of fresh Linux Hub install: machine-id, repo keys, jammy mirror, apt-get update. + * No user-controlled interpolation. Uses DEB822 .sources (Hub 3.20+). + */ +const LINUX_HUB_LINUX_INSTALL_BOOTSTRAP = `#!/bin/sh +set -e +dbus-uuidgen >/etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id +wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null +rm -f /etc/apt/sources.list.d/unityhub.list +tee /etc/apt/sources.list.d/unityhub.sources >/dev/null <<'EOF' +Types: deb +URIs: https://hub.unity3d.com/linux/repos/deb +Suites: stable +Components: main +Signed-By: /usr/share/keyrings/Unity_Technologies_ApS.gpg +EOF +echo "deb https://archive.ubuntu.com/ubuntu jammy main universe" | tee /etc/apt/sources.list.d/jammy.list +apt-get update +`; +/** + * Post-install cleanup and xvfb / unity-hub wrapper setup. Runs as root; no user interpolation. + * Wrapper resolves Hub 3.20+ (/usr/lib/unityhub) vs legacy (/opt/unityhub) at runtime so apt + * upgrades that move the binary do not leave a stale path (exit 127). + */ +const LINUX_HUB_LINUX_INSTALL_POST = `#!/bin/sh +set -e +apt-get clean +sed -i 's/^\\(.*DISPLAY=:.*XAUTHORITY=.*\\)\\( "\\$@" \\)2>&1$/\\1\\2/' /usr/bin/xvfb-run +command -v unityhub >/dev/null || { echo "Unity Hub installation failed"; exit 1; } +hubPath=$(readlink -f "$(command -v unityhub)" 2>/dev/null || true) +if [ -z "$hubPath" ] || [ ! -x "$hubPath" ]; then + if [ -x /usr/lib/unityhub/unityhub ]; then + hubPath=/usr/lib/unityhub/unityhub + elif [ -x /opt/unityhub/unityhub ]; then + hubPath=/opt/unityhub/unityhub + else + echo "Failed to install Unity Hub" + exit 1 + fi +fi +tee /usr/bin/unity-hub >/dev/null <<'WRAPPER' +#!/bin/bash +if [ -x /usr/lib/unityhub/unityhub ]; then + hubBin=/usr/lib/unityhub/unityhub +elif [ -x /opt/unityhub/unityhub ]; then + hubBin=/opt/unityhub/unityhub +else + hubBin=$(readlink -f "$(command -v unityhub)" 2>/dev/null || true) +fi +if [ -z "$hubBin" ] || [ ! -x "$hubBin" ]; then + echo "Unity Hub binary not found" >&2 + exit 127 +fi +exec xvfb-run --auto-servernum "$hubBin" "$@" 2>/dev/null +WRAPPER +chmod 777 /usr/bin/unity-hub +chmod -R 777 "$(dirname "$hubPath")" +`; +const LINUX_HUB_LINUX_APT_EXTRAS = [ + 'xvfb', + 'ffmpeg', + 'libgtk2.0-0', + 'libglu1-mesa', + 'libgconf-2-4', + 'libncurses5', + 'pulseaudio', +]; class UnityHub { /** The path to the Unity Hub executable. */ executable; @@ -21778,21 +22068,98 @@ class UnityHub { this.editorFileExtension = '/Unity.app/Contents/MacOS/Unity'; break; case 'linux': - this.executable = process.env.UNITY_HUB_PATH || '/opt/unityhub/unityhub'; - this.rootDirectory = path.join(this.executable, '../'); + this.refreshLinuxHubPaths(); this.editorFileExtension = '/Editor/Unity'; break; default: throw new Error(`Unsupported platform: ${process.platform}`); } } + /** Re-resolve Linux Hub executable + root after install/upgrade (Hub 3.20 moved under /usr/lib). */ + refreshLinuxHubPaths() { + this.executable = resolveLinuxHubExecutable(); + this.rootDirectory = path.join(this.executable, '../'); + } + /** + * Some Hub builds (notably Windows headless) occasionally exit non-zero after streaming usable + * `editors --releases` / `editors -i` data. Tolerate only when the captured output parses the same + * way {@link ListAvailableReleases} / {@link ListInstalledEditors} would (avoids regex false positives). + */ + hubListingExitTolerable(args, hubOutput) { + if (!this.isHubEditorListingArgs(args)) { + return false; + } + if (args.includes('--releases')) { + return this.parseAvailableReleasesFromHubText(hubOutput).length > 0; + } + if (args.includes('-i') || args.includes('--installed')) { + return hubOutput.includes('installed at'); + } + return false; + } + isHubEditorListingArgs(args) { + return args.length > 0 && args[0] === 'editors' && + (args.includes('--releases') || args.includes('-i') || args.includes('--installed')); + } + async delayMs(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); + } + /** Same parsing rules as {@link ListAvailableReleases}; must stay in sync. */ + parseAvailableReleasesFromHubText(output) { + return output.split('\n') + .map(line => line.trim()) + .map(line => { + const match = line.match(/^(\d{1,4}\.\d+\.\d+[abcfpx]?\d*)/); + return match ? match[1] : undefined; + }) + .filter((line) => !!line && /^\d{1,4}\.\d+\.\d+[abcfpx]?\d*/.test(line)) + .map(line => new unity_version_1.UnityVersion(line)) + .sort((a, b) => unity_version_1.UnityVersion.compare(b, a)); + } + /** Same parsing rules as {@link ListInstalledEditors}; must stay in sync. */ + parseInstalledEditorsFromHubText(output) { + const paths = output.split('\n') + .filter(line => /installed at/.test(line)) + .map(line => line.trim()); + const editors = []; + const pattern = /(?\d+\.\d+\.\d+[abcfpx]?\d*)\s*(?:\((?Apple silicon|Intel)\))?\s*,? installed at (?.*)/; + const matches = paths.map((line) => line.match(pattern)).filter(match => match && match.groups); + if (paths.length !== matches.length) { + throw new Error(`Failed to parse all installed Unity Editors!\n > paths: ${JSON.stringify(paths)}\n > matches: ${JSON.stringify(matches)}`); + } + for (const match of matches) { + if (match && match.groups && match.groups.version && match.groups.editorPath) { + const version = new unity_version_1.UnityVersion(match.groups.version, null, match.groups.arch === 'Apple silicon' ? 'ARM64' : match.groups.arch === 'Intel' ? 'X86_64' : undefined); + editors.push(new unity_editor_1.UnityEditor(path.normalize(match.groups.editorPath), version)); + } + } + editors.sort((a, b) => { + if (!a.version && !b.version) { + return 0; + } + if (!a.version) { + return 1; + } + if (!b.version) { + return -1; + } + return unity_version_1.UnityVersion.compare(b.version, a.version); + }); + return editors; + } /** * Executes the Unity Hub command with the specified arguments. * @param args Arguments to pass to the Unity Hub executable. - * @param silent If true, suppresses output logging. + * @param options Logging and spawn options for this invocation. * @returns The output from the command. */ async Exec(args, options = { silent: this.logger.logLevel > logging_1.LogLevel.CI, showCommand: this.logger.logLevel <= logging_1.LogLevel.CI }) { + return this.execImpl(args, options, 0); + } + /** + * @param listingRetryDepth 0 on first attempt; 1 after one listing-only retry (flaky Hub exits on Windows CI). + */ + async execImpl(args, options, listingRetryDepth) { let output = ''; let exitCode = 0; const filteredArgs = args.filter(arg => arg !== '--headless' && arg !== '--'); @@ -21833,7 +22200,8 @@ class UnityHub { 'Completed with errors.' ]; const child = (0, child_process_1.spawn)(executable, execArgs, { - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['ignore', 'pipe', 'pipe'], + ...(process.platform === 'win32' ? { windowsHide: true } : {}), }); const sigintHandler = () => child.kill('SIGINT'); const sigtermHandler = () => child.kill('SIGTERM'); @@ -21961,17 +22329,27 @@ class UnityHub { if (match || retryConditions.some(s => output.includes(s))) { this.logger.warn(`Install failed, retrying...`); - return await this.Exec(args); + return await this.execImpl(args, options, 0); } if (exitCode > 0) { - const error = output.match(/Error(?: given)?:\s*(.+)/); - const errorMessage = error && error[1] ? error[1] : 'Unknown Error'; - switch (errorMessage) { - case 'No modules found to install.': - break; - default: - this.logger.debug(output); - throw new Error(`Failed to execute Unity Hub (exit code: ${exitCode}) ${errorMessage}`); + if (this.hubListingExitTolerable(args, output)) { + this.logger.warn(`Unity Hub exited with code ${exitCode} but produced usable listing output; continuing.`); + } + else { + const error = output.match(/Error(?: given)?:\s*(.+)/); + const errorMessage = error && error[1] ? error[1] : 'Unknown Error'; + switch (errorMessage) { + case 'No modules found to install.': + break; + default: + if (this.isHubEditorListingArgs(args) && listingRetryDepth < 1) { + this.logger.warn(`Unity Hub listing command failed (exit code ${exitCode}); retrying once after 2s...`); + await this.delayMs(2000); + return await this.execImpl(args, options, listingRetryDepth + 1); + } + this.logger.debug(output); + throw new Error(`Failed to execute Unity Hub (exit code: ${exitCode}) ${errorMessage}`); + } } } output = output.split('\n') @@ -22056,12 +22434,13 @@ class UnityHub { await this.installHub(version); } else if (process.platform === 'linux') { - await (0, utilities_1.Exec)('sudo', ['sh', '-c', `#!/bin/bash -set -e -wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null -sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list' -sudo apt-get update --allow-releaseinfo-change -sudo apt-get install -y --no-install-recommends --only-upgrade unityhub${version ? '=' + version : ''}`]); + const hubPkg = this.unityHubAptPackageSpec(version); + const linuxExecOpts = { silent: true, showCommand: true }; + await (0, utilities_1.Exec)('sudo', ['sh', '-c', LINUX_HUB_LINUX_UPDATE_REPO_BOOTSTRAP], linuxExecOpts); + await (0, utilities_1.Exec)('sudo', ['apt-get', 'install', '-y', '--no-install-recommends', '--only-upgrade', hubPkg], linuxExecOpts); + // Refresh xvfb wrapper after upgrades that move /opt → /usr/lib (Hub 3.20+). + await (0, utilities_1.Exec)('sudo', ['sh', '-c', LINUX_HUB_LINUX_INSTALL_POST], linuxExecOpts); + this.refreshLinuxHubPaths(); this.logger.info(`Unity Hub updated successfully.`); } else { @@ -22072,9 +22451,40 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub${version this.logger.info(`Unity Hub is already installed and up to date.`); } } + if (process.platform === 'linux') { + this.refreshLinuxHubPaths(); + } await fs.promises.access(this.executable, fs.constants.X_OK); return this.executable; } + /** + * APT package spec for unityhub (e.g. `unityhub` or `unityhub=3.6.0`). Validated; passed as argv, not shell-embedded. + */ + unityHubAptPackageSpec(version) { + if (version === undefined || version === null) { + return 'unityhub'; + } + if (typeof version === 'object' && 'version' in version) { + const deb = version.version; + if (!LINUX_HUB_DEB_VERSION_RE.test(deb)) { + throw new Error(`Invalid Unity Hub apt version: ${deb}`); + } + return `unityhub=${deb}`; + } + const raw = String(version).trim(); + if (raw.length === 0) { + return 'unityhub'; + } + const pinned = (0, semver_1.coerce)(raw); + if (!pinned || !(0, semver_1.valid)(pinned)) { + throw new Error(`Invalid Unity Hub version for apt: ${raw}`); + } + const deb = pinned.version; + if (!LINUX_HUB_DEB_VERSION_RE.test(deb)) { + throw new Error(`Invalid Unity Hub apt version: ${deb}`); + } + return `unityhub=${deb}`; + } async installHub(version) { this.logger.ci(`Installing Unity Hub${version ? ' ' + version : ''}...`); if (!version) { @@ -22161,35 +22571,12 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub${version break; } case 'linux': { - await (0, utilities_1.Exec)('sudo', ['sh', '-c', `#!/bin/bash -set -e -dbus-uuidgen >/etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id -wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null -echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list -echo "deb https://archive.ubuntu.com/ubuntu jammy main universe" | tee /etc/apt/sources.list.d/jammy.list -apt-get update -apt-get install -y --no-install-recommends \\ - unityhub${version ? '=' + version : ''} \\ - xvfb \\ - ffmpeg \\ - libgtk2.0-0 \\ - libglu1-mesa \\ - libgconf-2-4 \\ - libncurses5 \\ - pulseaudio -apt-get clean -sed -i 's/^\\(.*DISPLAY=:.*XAUTHORITY=.*\\)\\( "\\$@" \\)2>&1$/\\1\\2/' /usr/bin/xvfb-run -printf '#!/bin/bash\nxvfb-run --auto-servernum /opt/unityhub/unityhub "$@" 2>/dev/null' | tee /usr/bin/unity-hub >/dev/null -chmod 777 /usr/bin/unity-hub -which unityhub || { echo "Unity Hub installation failed"; exit 1; } -hubPath=$(which unityhub) - -if [ -z "$hubPath" ]; then - echo "Failed to install Unity Hub" - exit 1 -fi - -chmod -R 777 "$hubPath"`]); + const hubPkg = this.unityHubAptPackageSpec(version); + const linuxExecOpts = { silent: true, showCommand: true }; + await (0, utilities_1.Exec)('sudo', ['sh', '-c', LINUX_HUB_LINUX_INSTALL_BOOTSTRAP], linuxExecOpts); + await (0, utilities_1.Exec)('sudo', ['apt-get', 'install', '-y', '--no-install-recommends', hubPkg, ...LINUX_HUB_LINUX_APT_EXTRAS], linuxExecOpts); + await (0, utilities_1.Exec)('sudo', ['sh', '-c', LINUX_HUB_LINUX_INSTALL_POST], linuxExecOpts); + this.refreshLinuxHubPaths(); break; } default: @@ -22296,27 +22683,43 @@ chmod -R 777 "$hubPath"`]); this.logger.ci(`Getting release info for Unity ${unityVersion.toString()}...`); let resolvedVersion = unityVersion; if (!resolvedVersion.isLegacy()) { - try { - if (!resolvedVersion.isFullyQualified()) { + // Hub list is a fast path only. Misses must fall through to the Releases API — + // do not fail-closed until both Hub match and API resolution have failed. + if (!resolvedVersion.isFullyQualified()) { + try { const releases = await this.ListAvailableReleases(); logging_1.Logger.instance.debug(`Found ${releases.length} available Unity releases, searching channels: ${channels.join(', ')}`); resolvedVersion = resolvedVersion.findMatch(releases, channels); } - if (!resolvedVersion?.changeset) { - const unityReleaseInfo = await this.GetEditorReleaseInfo(resolvedVersion); - resolvedVersion = new unity_version_1.UnityVersion(unityReleaseInfo.version, unityReleaseInfo.shortRevision, resolvedVersion.architecture); + catch (hubMatchError) { + this.logger.debug(`No Hub list match for ${resolvedVersion.toString()} (channels: ${channels.join(', ')}); trying Releases API...\n${hubMatchError}`); } } - catch (error) { - this.logger.warn(`Failed to get Unity release info for ${resolvedVersion.toString()}! falling back to legacy search...\n${error}`); + if (!resolvedVersion.changeset) { try { - resolvedVersion = await this.fallbackVersionLookup(resolvedVersion); + const unityReleaseInfo = await this.GetEditorReleaseInfo(resolvedVersion, channels); + resolvedVersion = new unity_version_1.UnityVersion(unityReleaseInfo.version, unityReleaseInfo.shortRevision, resolvedVersion.architecture); } - catch (fallbackError) { - this.logger.warn(`Failed to lookup changeset for Unity ${resolvedVersion.toString()}!\n${fallbackError}`); + catch (error) { + // Fail closed for partial versions: never Hub-install "6000.6" and hope it picks a beta. + if (!resolvedVersion.isFullyQualified()) { + const msg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to resolve Unity ${unityVersion.toString()} for channel(s) [${channels.join(', ')}]: ${msg}`); + } + this.logger.warn(`Failed to get Unity release info for ${resolvedVersion.toString()}! falling back to legacy search...\n${error}`); + try { + resolvedVersion = await this.fallbackVersionLookup(resolvedVersion); + } + catch (fallbackError) { + this.logger.warn(`Failed to lookup changeset for Unity ${resolvedVersion.toString()}!\n${fallbackError}`); + } } } } + if (!resolvedVersion.isLegacy() && !resolvedVersion.isFullyQualified()) { + throw new Error(`Refusing to install non-fully-qualified Unity version ${resolvedVersion.toString()} without a resolved release. ` + + `Use a fully-qualified version or --channel matching an available stream.`); + } const allowPartialMatches = !resolvedVersion.isFullyQualified(); let editorPath = await this.checkInstalledEditors(resolvedVersion, false, undefined, allowPartialMatches); unityVersion = resolvedVersion; @@ -22384,35 +22787,7 @@ chmod -R 777 "$hubPath"`]); */ async ListInstalledEditors() { const output = await this.Exec(['editors', '-i']); - const paths = output.split('\n') - .filter(line => /installed at/.test(line)) - .map(line => line.trim()); - const editors = []; - const pattern = /(?\d+\.\d+\.\d+[abcfpx]?\d*)\s*(?:\((?Apple silicon|Intel)\))?\s*,? installed at (?.*)/; - const matches = paths.map(path => path.match(pattern)).filter(match => match && match.groups); - if (paths.length !== matches.length) { - throw new Error(`Failed to parse all installed Unity Editors!\n > paths: ${JSON.stringify(paths)}\n > matches: ${JSON.stringify(matches)}`); - } - for (const match of matches) { - if (match && match.groups && match.groups.version && match.groups.editorPath) { - const version = new unity_version_1.UnityVersion(match.groups.version, null, match.groups.arch === 'Apple silicon' ? 'ARM64' : match.groups.arch === 'Intel' ? 'X86_64' : undefined); - editors.push(new unity_editor_1.UnityEditor(path.normalize(match.groups.editorPath), version)); - } - } - // Sort editors descending by UnityVersion so callers receive newest matches first - editors.sort((a, b) => { - if (!a.version && !b.version) { - return 0; - } - if (!a.version) { - return 1; - } - if (!b.version) { - return -1; - } - return unity_version_1.UnityVersion.compare(b.version, a.version); - }); - return editors; + return this.parseInstalledEditorsFromHubText(output); } /** * Lists the available Unity releases. @@ -22420,16 +22795,7 @@ chmod -R 777 "$hubPath"`]); */ async ListAvailableReleases() { const output = await this.Exec(['editors', '--releases']); - // filter out version lines only 2021.3.45f2 (may include installed path following version) - return output.split('\n') - .map(line => line.trim()) - .map(line => { - const match = line.match(/^(\d{1,4}\.\d+\.\d+[abcfpx]?\d*)/); - return match ? match[1] : undefined; - }) - .filter((line) => !!line && /^\d{1,4}\.\d+\.\d+[abcfpx]?\d*/.test(line)) - .map(line => new unity_version_1.UnityVersion(line)) - .sort((a, b) => unity_version_1.UnityVersion.compare(b, a)); // Sort descending by version + return this.parseAvailableReleasesFromHubText(output); } async checkInstalledEditors(unityVersion, failOnEmpty, installDir = undefined, allowPartialMatches = true) { let editorPath = undefined; @@ -22533,9 +22899,10 @@ done * Gets the specified Unity release info from the Unity Releases API. * Supports querying by exact version or by prefix (e.g., "2020", "2020.1", "2021.x", "2021.3.x"). * @param unityVersion The Unity version to get the release info for. + * @param channels Letter channels to accept (`f`, `p`, `b`, `a`, `x`). Default stable-only. * @returns The Unity release info. */ - async GetEditorReleaseInfo(unityVersion) { + async GetEditorReleaseInfo(unityVersion, channels = ['f']) { // Prefer querying the releases API with the exact fully-qualified Unity version (e.g., 2022.3.10f1). // If we don't have a fully-qualified version, use the most specific prefix available: // - "YYYY.M" when provided (e.g., 6000.1) @@ -22555,6 +22922,7 @@ done } } const releasesClient = new unity_releases_api_1.UnityReleasesClient(); + const channelSet = new Set(channels.map(c => c.toLowerCase())); function getPlatform() { switch (process.platform) { case 'darwin': @@ -22567,6 +22935,10 @@ done throw new Error(`Unsupported platform: ${process.platform}`); } } + function releaseChannelLetter(releaseVersion) { + const m = /^(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)$/.exec(releaseVersion); + return m?.[4]; + } const request = { url: '/unity/editor/release/v1/releases', query: { @@ -22586,22 +22958,29 @@ done if (!data || !data.results || data.results.length === 0) { throw new Error(`No Unity releases found for version: ${version}`); } - // Filter to stable 'f' releases only unless the user explicitly asked for a pre-release - const isExplicitPrerelease = /[abcpx]$/.test(unityVersion.version) || /[abcpx]/.test(unityVersion.version); const releases = (data.results || []) .filter((release) => { const v = release.version; if (v == null || v === '') { return false; } - return isExplicitPrerelease || v.includes('f'); + // Exact FQ request: accept that row regardless of channel filter. + if (fullUnityVersionPattern.test(unityVersion.version) && v === unityVersion.version) { + return true; + } + const letter = releaseChannelLetter(v); + return letter != null && channelSet.has(letter); }) .map(release => ({ unityRelease: release, unityVersion: new unity_version_1.UnityVersion(release.version, release.shortRevision, unityVersion.architecture) })); if (releases.length === 0) { - throw new Error(`No suitable Unity releases (stable) found for version: ${version}`); + const channelList = [...channelSet].join(','); + throw new Error(`No suitable Unity releases (channels: ${channelList}) found for version: ${version}` + + (channelSet.has('f') && channelSet.size === 1 + ? `. No stable (f) release for ${version}; use --channel b/a or a fully-qualified version.` + : '')); } releases.sort((a, b) => unity_version_1.UnityVersion.compare(b.unityVersion, a.unityVersion)); logging_1.Logger.instance.debug(`Found ${releases.length} matching Unity releases for version: ${version}`); @@ -22976,6 +23355,7 @@ const path = __importStar(__nccwpck_require__(71017)); const logging_1 = __nccwpck_require__(44486); const utilities_1 = __nccwpck_require__(39746); const utp_1 = __nccwpck_require__(16282); +const utp_benign_1 = __nccwpck_require__(26239); // Detects workflow command markers to avoid emitting duplicate annotations const annotationCommandPrefixRegex = /\n::[a-z]+::/i; // Matches ANSI escape sequences (CSI and single-character) @@ -23858,24 +24238,6 @@ async function writeUtpTelemetryLog(filePath, entries, logger) { logger.warn(`Failed to write UTP telemetry log (${filePath}): ${error}`); } } -/** - * Editor log messages whose severity has been changed. - * Useful for making certain error messages that are not critical less noisy. - * Key is a substring of the log message, value is the remapped LogLevel. - */ -const remappedEditorLogs = { - 'OpenCL device, baking cannot use GPU lightmapper.': logging_1.LogLevel.INFO, - 'Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.': logging_1.LogLevel.INFO, - '~StackAllocator(ALLOC_TEMP_MAIN) m_LastAlloc not NULL. Did you forget to call FreeAllStackAllocations()?': logging_1.LogLevel.INFO, -}; -function getRemappedEditorLogLevel(message) { - for (const [fragment, level] of Object.entries(remappedEditorLogs)) { - if (message.includes(fragment)) { - return level; - } - } - return undefined; -} /** * Tails a log file using fs.watch and ReadStream for efficient reading. * @param logPath The path to the log file to tail. @@ -23977,13 +24339,7 @@ function TailLogFile(logPath, projectPath) { } } } - if (utp.message && 'severity' in utp && - (utp.severity === utp_1.Severity.Error || utp.severity === utp_1.Severity.Exception || utp.severity === utp_1.Severity.Assert)) { - let messageLevel = logging_1.LogLevel.ERROR; - const remappedLevel = getRemappedEditorLogLevel(utp.message); - if (remappedLevel !== undefined) { - messageLevel = remappedLevel; - } + if (utp.message && 'severity' in utp && (0, utp_1.isElevatedUtpSeverity)(utp.severity)) { const normalizedPath = normalizeAnnotationPath(utp.file, projectPath); const stacktrace = sanitizeStackTrace(utp.stackTrace); const message = stacktrace == undefined ? utp.message : `${utp.message}\n${stacktrace}`; @@ -24001,21 +24357,18 @@ function TailLogFile(logPath, projectPath) { } } else { - switch (messageLevel) { - case logging_1.LogLevel.WARN: - logger.warn(message); - break; - case logging_1.LogLevel.ERROR: - logger.error(message); - break; - case logging_1.LogLevel.INFO: - default: - logger.info(message); - break; - } + logger.error(message); } } } + else if (utp.message && (0, utp_benign_1.utpMessageMatchesBenignRemap)(utp.message)) { + // Remapped at normalize time (e.g. multicast WSAEACCES); surface as info, not error. + const stacktrace = sanitizeStackTrace(utp.stackTrace); + const message = stacktrace == undefined ? utp.message : `${utp.message}\n${stacktrace}`; + if (!annotationCommandPrefixRegex.test(message)) { + logger.info(message); + } + } else if (logging_1.Logger.instance.logLevel === logging_1.LogLevel.UTP) { printUTP(utp); } @@ -24025,8 +24378,21 @@ function TailLogFile(logPath, projectPath) { } } else { + // Skip plain-log false positives (e.g. "Socket: bind failed, error: …" matching \berror\b). + if ((0, utp_benign_1.utpMessageMatchesBenignRemap)(line)) { + if (logging_1.Logger.instance.logLevel !== logging_1.LogLevel.UTP) { + process.stdout.write(`${line}\n`); + } + return; + } const scan = parsePlainLogIssue(line); if (scan) { + if ((0, utp_benign_1.utpMessageMatchesBenignRemap)(scan.message)) { + if (logging_1.Logger.instance.logLevel !== logging_1.LogLevel.UTP) { + process.stdout.write(`${line}\n`); + } + return; + } const key = buildIssueKey(scan.file, scan.line, scan.message); if (!seenIssueKeys.has(key)) { seenIssueKeys.add(key); @@ -24336,6 +24702,12 @@ class UnityVersion { semVer; logger = logging_1.Logger.instance; constructor(version, changeset = undefined, architecture = undefined) { + // Accept ProjectVersion / matrix style: "5.6.7f1 (e80cc3114ac1)" (no regex: avoid ReDoS). + const embedded = UnityVersion.tryParseEmbeddedChangeset(version); + if (embedded) { + version = embedded.version; + changeset = changeset ?? embedded.changeset; + } this.version = version; this.changeset = changeset; this.semVer = UnityVersion.createSemVer(version); @@ -24394,9 +24766,12 @@ class UnityVersion { this.logger.debug(`Found Unity ${latest.version}`); return new UnityVersion(latest.version, null, this.architecture); } + throw new Error(`No Unity release matching ${this.version} for channel(s) [${channels.join(', ')}]. ` + + (channels.length === 1 && channels[0] === 'f' + ? `No stable (f) release for ${this.version}; use --channel b/a or a fully-qualified version (e.g. 6000.6.0b7).` + : `Try a different --channel or a fully-qualified version.`)); } - this.logger.debug(`No matching Unity version found for ${this.version}`); - return this; + throw new Error(`No matching Unity version found for ${this.version}`); } satisfies(version) { return (0, semver_1.satisfies)(version.semVer, `^${this.semVer.version}`); @@ -24425,6 +24800,35 @@ class UnityVersion { } static UNITY_RELEASE_PATTERN = /^(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)$/; static VERSION_TOKEN_PATTERN = /^(\d{1,4})(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/; + /** + * Parses trailing " (hexchangeset)" without regex to avoid ReDoS on hostile input. + */ + static tryParseEmbeddedChangeset(raw) { + if (!raw.endsWith(')')) { + return null; + } + const open = raw.lastIndexOf('('); + if (open <= 0 || raw[open - 1] !== ' ') { + return null; + } + const hex = raw.slice(open + 1, -1); + if (hex.length === 0) { + return null; + } + for (let i = 0; i < hex.length; i++) { + const c = hex.charCodeAt(i); + const isHex = (c >= 48 && c <= 57) || // 0-9 + (c >= 97 && c <= 102) || // a-f + (c >= 65 && c <= 70); // A-F + if (!isHex) { + return null; + } + } + return { + version: raw.slice(0, open - 1).trimEnd(), + changeset: hex, + }; + } static UNITY_CHANNEL_ORDER = { a: 0, b: 1, @@ -24634,6 +25038,17 @@ class UpmCli { } return 'https://cdn.packages.unity.com/upm-cli'; } + /** + * HTTPS URL under the UPM CLI CDN for a release file. Caller must validate `tag` (e.g. {@link UpmCli.validateVersionFormat}); + * path segments are encoded to avoid tainted file-derived strings reaching the network unchecked (CodeQL js/file-access-to-http). + */ + static buildUpmReleaseAssetUrl(cdnBase, tag, fileName) { + const root = new URL(`${cdnBase.replace(/\/$/, '')}/`); + return new URL(`releases/${encodeURIComponent(tag)}/${encodeURIComponent(fileName)}`, root).href; + } + static buildUpmLatestTxtUrl(cdnBase) { + return new URL('latest.txt', new URL(`${cdnBase.replace(/\/$/, '')}/`)).href; + } static normalizeSemver(version) { const normalized = (0, semver_1.valid)(version); if (normalized) { @@ -24817,7 +25232,7 @@ class UpmCli { } async GetLatestReleaseTag() { const cdn = UpmCli.getCdnBaseUrl(); - const latestUrl = `${cdn}/latest.txt`; + const latestUrl = UpmCli.buildUpmLatestTxtUrl(cdn); const version = (await (0, utilities_1.HttpsGetText)(latestUrl)).trim(); this.validateVersionFormat(version); return version; @@ -24861,9 +25276,8 @@ class UpmCli { } const platform = this.getPlatformId(); const zipName = `upm-${platform}.zip`; - const baseReleaseUrl = `${cdn}/releases/${version}`; - const zipUrl = `${baseReleaseUrl}/${zipName}`; - const checksumUrl = `${baseReleaseUrl}/${zipName}.sha256`; + const zipUrl = UpmCli.buildUpmReleaseAssetUrl(cdn, version, zipName); + const checksumUrl = UpmCli.buildUpmReleaseAssetUrl(cdn, version, `${zipName}.sha256`); const tempRoot = path.join((0, utilities_1.GetTempDir)(), `unity-cli-upm-${Date.now()}`); const resolvedTempRoot = path.resolve(tempRoot); const zipPath = path.join(resolvedTempRoot, zipName); @@ -25106,7 +25520,7 @@ const fs = __importStar(__nccwpck_require__(57147)); const path = __importStar(__nccwpck_require__(71017)); const https = __importStar(__nccwpck_require__(95687)); const readline = __importStar(__nccwpck_require__(14521)); -const glob_1 = __nccwpck_require__(38211); +const glob_1 = __nccwpck_require__(95979); const child_process_1 = __nccwpck_require__(32081); const logging_1 = __nccwpck_require__(44486); const logger = logging_1.Logger.instance; @@ -25259,9 +25673,13 @@ async function Exec(command, args, options = { silent: false, showCommand: true } try { exitCode = await new Promise((resolve, reject) => { + const spawnEnv = options.env !== undefined && Object.keys(options.env).length > 0 + ? { ...process.env, ...options.env } + : undefined; const child = (0, child_process_1.spawn)(command, args, { - env: process.env, + shell: false, stdio: ['ignore', 'pipe', 'pipe'], + ...(spawnEnv !== undefined ? { env: spawnEnv } : {}), }); const sigintHandler = () => child.kill('SIGINT'); const sigtermHandler = () => child.kill('SIGTERM'); @@ -25358,8 +25776,7 @@ function assertResolvedPathUnderRoot(candidate, root, label) { } } /** - * Extracts a zip archive using only OS tools (`tar` or PowerShell on Windows, `unzip` on macOS/Linux). - * Does not use a Node unzip library. + * Extracts a zip archive using OS tools (PowerShell on Windows, `unzip` elsewhere). */ async function extractZipNative(zipPath, destDir, pathTrust, execOptions) { assertResolvedPathUnderRoot(zipPath, pathTrust.zipUnder, 'extractZipNative zipPath'); @@ -25368,40 +25785,27 @@ async function extractZipNative(zipPath, destDir, pathTrust, execOptions) { const silent = execOptions?.silent ?? true; const show = execOptions?.showCommand ?? false; if (process.platform === 'win32') { + const scriptBody = 'param([Parameter(Mandatory=$true)][string]$ZipPath,[Parameter(Mandatory=$true)][string]$DestPath)\n' + + '$ErrorActionPreference = "Stop"\n' + + 'Expand-Archive -LiteralPath $ZipPath -DestinationPath $DestPath -Force\n'; + const tmpDir = await fs.promises.mkdtemp(path.join(GetTempDir(), 'unity-cli-expand-zip-')); + const scriptPath = path.join(tmpDir, 'Expand-Archive.ps1'); try { - await Exec('tar', [ - '-xf', + await fs.promises.writeFile(scriptPath, scriptBody, 'utf8'); + await Exec('powershell.exe', [ + '-NoProfile', + '-NonInteractive', + '-File', + scriptPath, zipPath, - '-C', - destDir + destDir, ], { silent, - showCommand: show + showCommand: show, }); } - catch { - const scriptBody = 'param([Parameter(Mandatory=$true)][string]$ZipPath,[Parameter(Mandatory=$true)][string]$DestPath)\n' + - '$ErrorActionPreference = "Stop"\n' + - 'Expand-Archive -LiteralPath $ZipPath -DestinationPath $DestPath -Force\n'; - const tmpDir = await fs.promises.mkdtemp(path.join(GetTempDir(), 'unity-cli-expand-zip-')); - const scriptPath = path.join(tmpDir, 'Expand-Archive.ps1'); - try { - await fs.promises.writeFile(scriptPath, scriptBody, 'utf8'); - await Exec('powershell.exe', [ - '-NoProfile', - '-NonInteractive', - '-File', - scriptPath, - zipPath, - destDir, - ], { - silent, - showCommand: show, - }); - } - finally { - await fs.promises.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined); - } + finally { + await fs.promises.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined); } } else { @@ -25767,15 +26171,69 @@ function tryParseJson(content) { /***/ }), +/***/ 26239: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.UTP_BENIGN_SEVERITY_REMAPS = void 0; +exports.utpMessageMatchesBenignRemap = utpMessageMatchesBenignRemap; +/** + * Known Unity/editor messages that are non-actionable despite elevated UTP severity. + * Kept in a leaf module (no imports) so normalize, summaries, and CI share one list + * without circular deps between utp.ts and logging.ts. + * + * Severity strings must match {@link Severity} in utp.ts. + */ +exports.UTP_BENIGN_SEVERITY_REMAPS = [ + // Longer OpenCL form first so summary strip does not leave a "Failed to find a suitable" prefix. + { fragment: 'Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.', severity: 'Info' }, + { fragment: 'OpenCL device, baking cannot use GPU lightmapper.', severity: 'Info' }, + { + fragment: '~StackAllocator(ALLOC_TEMP_MAIN) m_LastAlloc not NULL. Did you forget to call FreeAllStackAllocations()?', + severity: 'Info', + }, + // Windows hosted CI: WSAEACCES (10013) — player-connection multicast / socket bind. Unity falls back. + { fragment: 'Unable to join player connection multicast group', severity: 'Info' }, + { fragment: 'Socket: bind failed', severity: 'Info' }, + { + fragment: 'An attempt was made to access a socket in a way forbidden by its access permissions', + severity: 'Info', + }, + { fragment: 'Access token is unavailable; failed to update', severity: 'Info' }, +]; +/** True if the message matches a known benign Unity/CI noise fragment. */ +function utpMessageMatchesBenignRemap(message) { + if (!message) { + return false; + } + for (const { fragment } of exports.UTP_BENIGN_SEVERITY_REMAPS) { + if (message.includes(fragment)) { + return true; + } + } + return false; +} +//# sourceMappingURL=utp-benign.js.map + +/***/ }), + /***/ 16282: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.UTP_SUPPORTED_TOP_LEVEL_PROPERTIES = exports.Severity = exports.Phase = exports.UTPPlayerBuildInfo = exports.UTPTestStatus = exports.UTPQualitySettings = exports.UTPPlayerSystemInfo = exports.UTPBuildSettings = exports.UTPPlayerSettings = exports.UTPScreenSettings = exports.UTPTestPlan = exports.UTPCompiler = exports.UTPLogEntry = exports.UTPMemoryLeaks = exports.UTPMemoryLeak = exports.UTPAction = exports.UTPBase = void 0; +exports.UTP_SUPPORTED_TOP_LEVEL_PROPERTIES = exports.Severity = exports.Phase = exports.UTPPlayerBuildInfo = exports.UTPTestStatus = exports.UTPQualitySettings = exports.UTPPlayerSystemInfo = exports.UTPBuildSettings = exports.UTPPlayerSettings = exports.UTPScreenSettings = exports.UTPTestPlan = exports.UTPCompiler = exports.UTPLogEntry = exports.UTPMemoryLeaks = exports.UTPMemoryLeak = exports.UTPAction = exports.UTPBase = exports.utpMessageMatchesBenignRemap = exports.UTP_BENIGN_SEVERITY_REMAPS = void 0; +exports.isElevatedUtpSeverity = isElevatedUtpSeverity; +exports.remapBenignUtpSeverity = remapBenignUtpSeverity; exports.normalizeTelemetryEntry = normalizeTelemetryEntry; const logging_1 = __nccwpck_require__(44486); +const utp_benign_1 = __nccwpck_require__(26239); +var utp_benign_2 = __nccwpck_require__(26239); +Object.defineProperty(exports, "UTP_BENIGN_SEVERITY_REMAPS", ({ enumerable: true, get: function () { return utp_benign_2.UTP_BENIGN_SEVERITY_REMAPS; } })); +Object.defineProperty(exports, "utpMessageMatchesBenignRemap", ({ enumerable: true, get: function () { return utp_benign_2.utpMessageMatchesBenignRemap; } })); class UTPBase { type; version; @@ -25861,6 +26319,28 @@ var Severity; Severity["Exception"] = "Exception"; Severity["Assert"] = "Assert"; })(Severity || (exports.Severity = Severity = {})); +/** Severities that normally fail builds / CI expected-success checks. */ +function isElevatedUtpSeverity(severity) { + return severity === Severity.Error + || severity === Severity.Exception + || severity === Severity.Assert; +} +/** + * Downgrades elevated severity on known benign messages. Mutates `utp`. + * @returns true when severity was changed. + */ +function remapBenignUtpSeverity(utp) { + if (!utp.message || !isElevatedUtpSeverity(utp.severity)) { + return false; + } + for (const { fragment, severity } of utp_benign_1.UTP_BENIGN_SEVERITY_REMAPS) { + if (utp.message.includes(fragment)) { + utp.severity = severity; + return true; + } + } + return false; +} /** * Root-level JSON keys on UTP objects that this CLI recognizes. Other keys are still parsed * but reported via {@link normalizeTelemetryEntry}'s `unknownTopLevelKeys` for logging. @@ -25898,8 +26378,9 @@ exports.UTP_SUPPORTED_TOP_LEVEL_PROPERTIES = new Set([ 'version', ]); /** - * Normalizes UTP telemetry entries to canonical shapes. Unknown top-level keys are listed - * for the caller to log (with the raw `##utp:` line when tailing logs). + * Normalizes UTP telemetry entries to canonical shapes and remaps known benign elevated + * severities. Unknown top-level keys are listed for the caller to log (with the raw + * `##utp:` line when tailing logs). */ function normalizeTelemetryEntry(entry) { if (!entry || typeof entry !== 'object') { @@ -25925,6 +26406,14 @@ function normalizeTelemetryEntry(entry) { if (utp.lineNumber === undefined && typeof utp.line === 'number') { utp.lineNumber = utp.line; } + // Canonicalize severity string casing from Unity payloads. + if (typeof utp.severity === 'string') { + const matched = Object.values(Severity).find(s => s.toLowerCase() === utp.severity.toLowerCase()); + if (matched) { + utp.severity = matched; + } + } + remapBenignUtpSeverity(utp); if (!utp.type) { logging_1.Logger.instance.warn('UTP entry missing type property; telemetry entry may be ignored.'); } @@ -28156,6 +28645,20 @@ var escClose = '\0CLOSE'+Math.random()+'\0'; var escComma = '\0COMMA'+Math.random()+'\0'; var escPeriod = '\0PERIOD'+Math.random()+'\0'; +var EXPANSION_MAX = 100000 + +// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An +// input like `'{a,b}'.repeat(1500)` stays under that count - its output is +// truncated to 100k results - while making every result ~1500 characters +// long. The result set, and the intermediate arrays built while combining +// brace sets, then grow large enough to exhaust memory and crash the process +// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of +// characters the accumulator may hold at any point, so memory stays flat no +// matter how many brace groups are chained. The limit sits well above any +// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M +// characters) so legitimate input is unaffected. +var EXPANSION_MAX_LENGTH = 4000000 + function numeric(str) { return parseInt(str, 10) == str ? parseInt(str, 10) @@ -28214,7 +28717,8 @@ function expandTop(str, options) { return []; options = options || {}; - var max = options.max == null ? Infinity : options.max; + var max = options.max == null ? EXPANSION_MAX : options.max; + var maxLength = options.maxLength == null ? EXPANSION_MAX_LENGTH : options.maxLength; // I don't know why Bash 4.3 does this, but it does. // Anything starting with {} will have the first two bytes preserved @@ -28226,7 +28730,7 @@ function expandTop(str, options) { str = '\\{\\}' + str.substr(2); } - return expand(escapeBraces(str), max, true).map(unescapeBraces); + return expand(escapeBraces(str), max, maxLength, true).map(unescapeBraces); } function identity(e) { @@ -28247,106 +28751,270 @@ function gte(i, y) { return i >= y; } -function expand(str, max, isTop) { - var expansions = []; - - var m = balanced('{', '}', str); - if (!m || /\$$/.test(m.pre)) return [str]; - - var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - var isSequence = isNumericSequence || isAlphaSequence; - var isOptions = m.body.indexOf(',') >= 0; - if (!isSequence && !isOptions) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; - return expand(str, max, true); +// Build `{ acc[a] + pre + values[v] }` for every combination, capping the +// number of results at `max` and the total number of characters at `maxLength`. +// This is the one place output grows, so bounding it here keeps the single +// accumulator - and therefore memory - flat regardless of how many brace groups +// are combined (CVE-2026-14257). +// +// `base[a]` is the length of the part of `acc[a]` that predates the current +// empty-drop baseline (see `expand`). The matching baselines for the results +// are appended to `outBase`, which the caller carries forward alongside them. +function combine( + acc, + base, + pre, + values, + max, + maxLength, + dropEmpties, + outBase +) { + var out = [] + var length = 0 + for (var a = 0; a < acc.length; a++) { + for (var v = 0; v < values.length; v++) { + if (out.length >= max) return out + var expansion = acc[a] + pre + values[v] + // Bash drops empty results at the top level. Skip them before they count + // against `max`, so `max` bounds the number of *kept* results. "Empty" + // means "adds nothing past the baseline", not "empty overall". + if (dropEmpties && expansion.length === base[a]) continue + if (length + expansion.length > maxLength) return out + out.push(expansion) + outBase.push(base[a]) + length += expansion.length } - return [str]; } + return out +} - var n; - if (isSequence) { - n = m.body.split(/\.\./); - } else { - n = parseCommaParts(m.body); - if (n.length === 1) { - // x{{a,b}}y ==> x{a}y x{b}y - n = expand(n[0], max, false).map(embrace); - if (n.length === 1) { - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - return post.map(function(p) { - return m.pre + n[0] + p; - }); +// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`) +// sequence body. +function expandSequence( + body, + isAlphaSequence, + max, + maxLength +) { + var n = body.split(/\.\./) + var N = [] + // A sequence body always splits into two or three parts, but the compiler + // can't know that. + /* c8 ignore start */ + if (n[0] === undefined || n[1] === undefined) { + return N + } + /* c8 ignore stop */ + var x = numeric(n[0]) + var y = numeric(n[1]) + var width = Math.max(n[0].length, n[1].length) + var incr = + n.length === 3 && n[2] !== undefined ? + Math.max(Math.abs(numeric(n[2])), 1) + : 1 + var test = lte + var reverse = y < x + if (reverse) { + incr *= -1 + test = gte + } + var pad = n.some(isPadded) + + var length = 0 + for (var i = x; test(i, y) && N.length < max; i += incr) { + var c + if (isAlphaSequence) { + c = String.fromCharCode(i) + if (c === '\\') { + c = '' + } + } else { + c = String(i) + if (pad) { + var need = width - c.length + if (need > 0) { + var z = new Array(need + 1).join('0') + if (i < 0) { + c = '-' + z + c.slice(1) + } else { + c = z + c + } + } } } + if (length + c.length > maxLength) break + N.push(c) + length += c.length } + return N +} - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. +function expand( + str, + max, + maxLength, + isTop +) { + // Consume the string's top-level brace groups left to right, threading a + // running set of combined prefixes (`acc`). Expanding the tail iteratively - + // rather than recursing on `m.post` once per group - keeps the native stack + // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no + // longer overflow the stack, and leaves a single accumulator whose size + // `maxLength` bounds directly (CVE-2026-14257). + var acc = [''] + + // Bash drops empty results, but only when the *first* group of the run is a + // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop + // is on the final strings, so it is applied to whichever `combine` produces + // them (the one with no brace set left in the tail). + // + // The old implementation recursed on `m.post`, so the drop tested only the + // expansion of the current call's substring. The `{a},b}` rewrite below turns + // `isTop` back on part-way through a string, starting a fresh such run, so + // the drop must ignore whatever `acc` already holds from earlier groups. + // `accBase[a]` records how much of `acc[a]` predates the current run; + // `combine` treats an expansion as empty when it adds nothing past that. + var accBase = [0] + var dropEmpties = false + var firstGroup = true + var nextBase + + for (;;) { + var m = balanced('{', '}', str); + + // No brace set left: the rest of the string is literal. + if (!m) { + return combine(acc, accBase, str, [''], max, maxLength, dropEmpties, []) + } - // no need to expand pre, since it is guaranteed to be free of brace-sets - var pre = m.pre; - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - - var N; - - if (isSequence) { - var x = numeric(n[0]); - var y = numeric(n[1]); - var width = Math.max(n[0].length, n[1].length) - var incr = n.length == 3 - ? Math.max(Math.abs(numeric(n[2])), 1) - : 1; - var test = lte; - var reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + + // For compatibility reasons, `${` is not eligible for brace expansion, and + // on the 1.x line it suppresses expansion of the rest of the string too: + // the whole remainder is literal. The 2.x and 5.x lines instead keep + // expanding the tail, which is what bash does, but changing that here would + // be a breaking change for 1.x consumers. Routed through `combine` so the + // result is still bounded by `max` and `maxLength`. + if (/\$$/.test(pre)) { + return combine(acc, accBase, str, [''], max, maxLength, dropEmpties, []) + } + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + // The rewritten string is expanded as if it were a fresh top-level one, + // so start a new empty-drop run: anchor the baseline at what `acc` + // holds now, and let the next expanding group decide whether to drop. + isTop = true + firstGroup = true + dropEmpties = false + accBase = [] + for (var b = 0; b < acc.length; b++) { + accBase.push(acc[b].length) + } + continue + } + // Nothing here expands, so the whole remaining string is literal. + return combine( + acc, + accBase, + pre + '{' + m.body + '}' + m.post, + [''], + max, + maxLength, + dropEmpties, + [] + ) } - var pad = n.some(isPadded); - N = []; + if (firstGroup) { + dropEmpties = isTop && !isSequence + firstGroup = false + } - for (var i = x; test(i, y); i += incr) { - var c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') - c = ''; - } else { - c = String(i); - if (pad) { - var need = width - c.length; - if (need > 0) { - var z = new Array(need + 1).join('0'); - if (i < 0) - c = '-' + z + c.slice(1); - else - c = z + c; + var values; + if (isSequence) { + values = expandSequence(m.body, isAlphaSequence, max, maxLength); + } else { + var n = parseCommaParts(m.body); + if (n.length === 1 && n[0] !== undefined) { + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], max, maxLength, false).map(embrace); + //XXX is this necessary? Can't seem to hit it in tests. + /* c8 ignore start */ + if (n.length === 1) { + nextBase = [] + acc = combine( + acc, + accBase, + pre + n[0], + [''], + max, + maxLength, + dropEmpties && !m.post.length, + nextBase + ) + accBase = nextBase + if (!m.post.length) break + str = m.post + continue + } + /* c8 ignore stop */ + } + + // Values that `combine` is going to drop as empty produce no result, so + // they must not count against `max` - otherwise `{a,,b}` with `max: 2` + // would stop at `['a', '']` and yield one result instead of two. Skipping + // them outright keeps `values` bounded while leaving `max` a bound on + // *kept* results. A value is dropped when it adds nothing past the + // baseline, which is what `combine` tests. + var dropsEmpties = dropEmpties && !m.post.length && !pre + for (var d = 0; dropsEmpties && d < acc.length; d++) { + if (acc[d].length !== accBase[d]) { + dropsEmpties = false + } + } + + values = [] + var valuesLength = 0 + outer: for (var j = 0; j < n.length; j++) { + var expanded = expand(n[j], max, maxLength, false) + for (var k = 0; k < expanded.length; k++) { + var v = expanded[k] + if (dropsEmpties && !v) continue + if (values.length >= max || valuesLength + v.length > maxLength) { + break outer } + values.push(v) + valuesLength += v.length } } - N.push(c); } - } else { - N = concatMap(n, function(el) { return expand(el, max, false) }); - } - for (var j = 0; j < N.length; j++) { - for (var k = 0; k < post.length && expansions.length < max; k++) { - var expansion = pre + N[j] + post[k]; - if (!isTop || isSequence || expansion) - expansions.push(expansion); - } + nextBase = [] + acc = combine( + acc, + accBase, + pre, + values, + max, + maxLength, + dropEmpties && !m.post.length, + nextBase + ) + accBase = nextBase + if (!m.post.length) break + str = m.post } - return expansions; + return acc } @@ -31123,6 +31791,9 @@ class Range { } parseRange (range) { + // strip build metadata so it can't bleed into the version + range = range.replace(BUILDSTRIPRE, '') + // memoize range parsing for performance. // this is a very hot path, and fully deterministic. const memoOpts = @@ -31248,6 +31919,7 @@ const debug = __nccwpck_require__(50427) const SemVer = __nccwpck_require__(48088) const { safeRe: re, + src, t, comparatorTrimReplace, tildeTrimReplace, @@ -31255,6 +31927,9 @@ const { } = __nccwpck_require__(9523) const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = __nccwpck_require__(42293) +// unbounded global build-metadata stripper used by parseRange +const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g') + const isNullSet = c => c.value === '<0.0.0-0' const isAny = c => c.value === '' @@ -31295,6 +31970,11 @@ const parseComparator = (comp, options) => { const isX = id => !id || id.toLowerCase() === 'x' || id === '*' +const invalidXRangeOrder = (M, m, p) => ( + (isX(M) && !isX(m)) || + (isX(m) && p && !isX(p)) +) + // ~, ~> --> * (any, kinda silly) // ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0 // ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0 @@ -31312,6 +31992,10 @@ const replaceTildes = (comp, options) => { const replaceTilde = (comp, options) => { const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] + // if we're including prereleases in the match, then the lower bound is + // -0, the lowest possible prerelease value, just like x-ranges and carets. + // this keeps `~1.2` equivalent to the `1.2.x` x-range it's documented as. + const z = options.includePrerelease ? '-0' : '' return comp.replace(r, (_, M, m, p, pr) => { debug('tilde', comp, _, M, m, p, pr) let ret @@ -31319,10 +32003,10 @@ const replaceTilde = (comp, options) => { if (isX(M)) { ret = '' } else if (isX(m)) { - ret = `>=${M}.0.0 <${+M + 1}.0.0-0` + ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0` } else if (isX(p)) { // ~1.2 == >=1.2.0 <1.3.0-0 - ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0` + ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0` } else if (pr) { debug('replaceTilde pr', pr) ret = `>=${M}.${m}.${p}-${pr @@ -31391,10 +32075,10 @@ const replaceCaret = (comp, options) => { if (M === '0') { if (m === '0') { ret = `>=${M}.${m}.${p - }${z} <${M}.${m}.${+p + 1}-0` + } <${M}.${m}.${+p + 1}-0` } else { ret = `>=${M}.${m}.${p - }${z} <${M}.${+m + 1}.0-0` + } <${M}.${+m + 1}.0-0` } } else { ret = `>=${M}.${m}.${p @@ -31420,6 +32104,10 @@ const replaceXRange = (comp, options) => { const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] return comp.replace(r, (ret, gtlt, M, m, p, pr) => { debug('xRange', comp, ret, gtlt, M, m, p, pr) + if (invalidXRangeOrder(M, m, p)) { + return comp + } + const xM = isX(M) const xm = xM || isX(m) const xp = xm || isX(p) @@ -31596,6 +32284,22 @@ const { safeRe: re, t } = __nccwpck_require__(9523) const parseOptions = __nccwpck_require__(40785) const { compareIdentifiers } = __nccwpck_require__(92463) + +const isPrereleaseIdentifier = (prerelease, identifier) => { + const identifiers = identifier.split('.') + if (identifiers.length > prerelease.length) { + return false + } + + for (let i = 0; i < identifiers.length; i++) { + if (compareIdentifiers(prerelease[i], identifiers[i]) !== 0) { + return false + } + } + + return true +} + class SemVer { constructor (version, options) { options = parseOptions(options) @@ -31899,8 +32603,9 @@ class SemVer { if (identifierBase === false) { prerelease = [identifier] } - if (compareIdentifiers(this.prerelease[0], identifier) === 0) { - if (isNaN(this.prerelease[1])) { + if (isPrereleaseIdentifier(this.prerelease, identifier)) { + const prereleaseBase = this.prerelease[identifier.split('.').length] + if (isNaN(prereleaseBase)) { this.prerelease = prerelease } } else { @@ -33525,7 +34230,7 @@ const simpleSubset = (sub, dom, options) => { if (higher === c && higher !== gt) { return false } - } else if (gt.operator === '>=' && !satisfies(gt.semver, String(c), options)) { + } else if (gt.operator === '>=' && !c.test(gt.semver)) { return false } } @@ -33543,7 +34248,7 @@ const simpleSubset = (sub, dom, options) => { if (lower === c && lower !== lt) { return false } - } else if (lt.operator === '<=' && !satisfies(lt.semver, String(c), options)) { + } else if (lt.operator === '<=' && !c.test(lt.semver)) { return false } } @@ -37442,7 +38147,13 @@ function processHeader (request, key, val) { } else if (typeof val[i] === 'object') { throw new InvalidArgumentError(`invalid ${key} header`) } else { - arr.push(`${val[i]}`) + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). + const str = `${val[i]}` + if (!isValidHeaderValue(str)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + arr.push(str) } } val = arr @@ -37453,7 +38164,12 @@ function processHeader (request, key, val) { } else if (val === null) { val = '' } else { + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). val = `${val}` + if (!isValidHeaderValue(val)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } } if (headerName === 'host') { @@ -38490,7 +39206,6 @@ function defaultFactory (origin, opts) { class Agent extends DispatcherBase { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -38831,6 +39546,7 @@ const { RequestContentLengthMismatchError, ResponseContentLengthMismatchError, RequestAbortedError, + InvalidArgumentError, HeadersTimeoutError, HeadersOverflowError, SocketError, @@ -38878,6 +39594,9 @@ const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const addListener = util.addListener const removeAllListeners = util.removeAllListeners +const kIdleSocketValidation = Symbol('kIdleSocketValidation') +const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') +const kSocketUsed = Symbol('kSocketUsed') let extractBody @@ -39100,29 +39819,71 @@ class Parser { const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr - if (ret === constants.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)) - } else if (ret === constants.ERROR.PAUSED) { - this.paused = true - socket.unshift(data.slice(offset)) - } else if (ret !== constants.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr) - let message = '' - /* istanbul ignore else: difficult to make a test case for */ - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) - message = - 'Response does not match the HTTP/1.1 protocol (' + - Buffer.from(llhttp.memory.buffer, ptr, len).toString() + - ')' - } - throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + if (ret !== constants.ERROR.OK) { + const body = data.subarray(offset) + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(body) + } else { + throw this.createError(ret, body) + } } } catch (err) { util.destroy(socket, err) } } + finish () { + assert(currentParser === null) + assert(this.ptr != null) + assert(!this.paused) + + const { llhttp } = this + + let ret + + try { + currentParser = this + ret = llhttp.llhttp_finish(this.ptr) + } finally { + currentParser = null + } + + if (ret === constants.ERROR.OK) { + return null + } + + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true + return null + } + + return this.createError(ret, EMPTY_BUF) + } + + createError (ret, data) { + const { llhttp, contentLength, bytesRead } = this + + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError() + } + + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + + return new HTTPParserError(message, constants.ERROR[ret], data) + } + destroy () { assert(this.ptr != null) assert(currentParser == null) @@ -39150,6 +39911,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] if (!request) { return -1 @@ -39253,6 +40019,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] /* istanbul ignore next: difficult to make a test case for */ @@ -39426,6 +40197,7 @@ class Parser { request.onComplete(headers) client[kQueue][client[kRunningIdx]++] = null + socket[kSocketUsed] = true if (socket[kWriting]) { assert(client[kRunning] === 0) @@ -39484,6 +40256,9 @@ async function connectH1 (client, socket) { socket[kWriting] = false socket[kReset] = false socket[kBlocking] = false + socket[kIdleSocketValidation] = 0 + socket[kIdleSocketValidationTimeout] = null + socket[kSocketUsed] = false socket[kParser] = new Parser(client, socket, llhttpInstance) addListener(socket, 'error', function (err) { @@ -39494,8 +40269,11 @@ async function connectH1 (client, socket) { // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // to the user. if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + this[kError] = parserErr + this[kClient][kOnError](parserErr) + } return } @@ -39514,8 +40292,10 @@ async function connectH1 (client, socket) { const parser = this[kParser] if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + util.destroy(this, parserErr) + } return } @@ -39525,10 +40305,11 @@ async function connectH1 (client, socket) { const client = this[kClient] const parser = this[kParser] + clearIdleSocketValidation(this) + if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + this[kError] = parser.finish() || this[kError] } this[kParser].destroy() @@ -39591,7 +40372,7 @@ async function connectH1 (client, socket) { return socket.destroyed }, busy (request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true } @@ -39629,6 +40410,31 @@ async function connectH1 (client, socket) { } } +function clearIdleSocketValidation (socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearTimeout(socket[kIdleSocketValidationTimeout]) + socket[kIdleSocketValidationTimeout] = null + } + + socket[kIdleSocketValidation] = 0 +} + +function scheduleIdleSocketValidation (client, socket) { + socket[kIdleSocketValidation] = 1 + socket[kIdleSocketValidationTimeout] = setTimeout(() => { + socket[kIdleSocketValidationTimeout] = null + socket[kIdleSocketValidation] = 2 + + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume]() + } + }, 0) + socket[kIdleSocketValidationTimeout].unref?.() +} + +/** + * @param {import('./client.js')} client + */ function resumeH1 (client) { const socket = client[kSocket] @@ -39643,6 +40449,32 @@ function resumeH1 (client) { socket[kNoRef] = false } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket) + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + } + + if (client[kRunning] === 0) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + } + if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) @@ -39698,8 +40530,16 @@ function writeH1 (client, request) { } body = bodyStream.stream contentLength = bodyStream.length - } else if (util.isBlobLike(body) && request.contentType == null && body.type) { - headers.push('content-type', body.type) + } else if (util.isBlobLike(body) && request.contentType == null) { + const contentType = body.type + if (contentType) { + const contentTypeValue = `${contentType}` + if (!util.isValidHeaderValue(contentTypeValue)) { + util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header')) + return false + } + headers.push('content-type', contentTypeValue) + } } if (body && typeof body.read === 'function') { @@ -39736,6 +40576,7 @@ function writeH1 (client, request) { } const socket = client[kSocket] + clearIdleSocketValidation(socket) const abort = (err) => { if (request.aborted || request.completed) { @@ -41608,6 +42449,7 @@ class DispatcherBase extends Dispatcher { get webSocketOptions () { return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 } } @@ -43184,6 +44026,28 @@ function calculateRetryAfterHeader (retryAfter) { return new Date(retryAfter).getTime() - current } +function validatePartialResponseContentLength (headers, range, statusCode, retryCount) { + const contentLength = headers['content-length'] + if (contentLength == null) { + return null + } + + if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) { + return null + } + + const length = Number(contentLength) + const expectedLength = range.end - range.start + 1 + if (!Number.isFinite(length) || length !== expectedLength) { + return new RequestRetryError('Content-Length mismatch', statusCode, { + headers, + data: { count: retryCount } + }) + } + + return null +} + class RetryHandler { constructor (opts, handlers) { const { retryOptions, ...dispatchOpts } = opts @@ -43398,6 +44262,12 @@ class RetryHandler { return false } + const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount) + if (contentLengthError != null) { + this.abort(contentLengthError) + return false + } + const { start, size, end = size - 1 } = contentRange assert(this.start === start, 'content-range mismatch') @@ -43421,6 +44291,12 @@ class RetryHandler { ) } + const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount) + if (contentLengthError != null) { + this.abort(contentLengthError) + return false + } + const { start, size, end = size - 1 } = range assert( start != null && Number.isFinite(start), @@ -47544,32 +48420,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // If the attribute-name case-insensitively matches the string // "SameSite", the user agent MUST process the cookie-av as follows: - // 1. Let enforcement be "Default". - let enforcement = 'Default' - const attributeValueLowercase = attributeValue.toLowerCase() - // 2. If cookie-av's attribute-value is a case-insensitive match for - // "None", set enforcement to "None". - if (attributeValueLowercase.includes('none')) { - enforcement = 'None' - } - - // 3. If cookie-av's attribute-value is a case-insensitive match for - // "Strict", set enforcement to "Strict". - if (attributeValueLowercase.includes('strict')) { - enforcement = 'Strict' - } - // 4. If cookie-av's attribute-value is a case-insensitive match for - // "Lax", set enforcement to "Lax". - if (attributeValueLowercase.includes('lax')) { - enforcement = 'Lax' + // 1. If cookie-av's attribute-value is a case-insensitive match for + // "None", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "None". + if (attributeValueLowercase === 'none') { + cookieAttributeList.sameSite = 'None' + } else if (attributeValueLowercase === 'strict') { + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", append an attribute to the cookie-attribute-list with + // an attribute-name of "SameSite" and an attribute-value of + // "Strict". + cookieAttributeList.sameSite = 'Strict' + } else if (attributeValueLowercase === 'lax') { + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "Lax". + cookieAttributeList.sameSite = 'Lax' } - - // 5. Append an attribute to the cookie-attribute-list with an - // attribute-name of "SameSite" and an attribute-value of - // enforcement. - cookieAttributeList.sameSite = enforcement } else { cookieAttributeList.unparsed ??= [] @@ -47699,7 +48568,7 @@ function validateCookiePath (path) { if ( code < 0x20 || // exclude CTLs (0-31) - code === 0x7F || // DEL + code > 0x7E || // exclude DEL and non-ascii code === 0x3B // ; ) { throw new Error('Invalid cookie path') @@ -47708,16 +48577,80 @@ function validateCookiePath (path) { } /** - * I have no idea why these values aren't allowed to be honest, - * but Deno tests these. - Khafra + * ::= | + * + * ::= any one of the 52 alphabetic characters A through Z in + * upper case and a through z in lower case + * + * ::= any one of the ten digits 0 through 9r + * + * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5 + * @param {number} code + */ +function isLetterOrDigit (code) { + return ( + (code >= 0x30 && code <= 0x39) || // 0-9 + (code >= 0x41 && code <= 0x5A) || // A-Z + (code >= 0x61 && code <= 0x7A) // a-z + ) +} + +/** + * Validates a cookie domain against the "preferred name syntax". + * + * ::= | " " + * ::=