'use strict'; const fs = require('node:fs'); const path = require('node:path'); const semver = require('semver'); const RELEASE_IDENTITY_PATH = 'ql3-release.json'; const RELEASE_IDENTITY_SCHEMA = 'qinglong/release-identity@v2'; const ARCHITECTURE_SUPPORT = Object.freeze({ tier1: Object.freeze(['amd64', 'arm64']), candidates: Object.freeze(['ppc64le', 's390x']), experimentalBlocked: Object.freeze(['arm/v7']), legacyOnly: Object.freeze(['arm/v6', '386']), legacyLine: '2.x', }); const MAX_RELEASE_IDENTITY_BYTES = 4096; const VERSION_PATTERN = /^3\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$/u; class QingLong3ReleaseIdentityError extends Error { constructor(message) { super(`QingLong 3 release identity failed: ${message}`); this.name = 'QingLong3ReleaseIdentityError'; } } function fail(message) { throw new QingLong3ReleaseIdentityError(message); } function exactKeys(value, expected) { return ( value !== null && typeof value === 'object' && !Array.isArray(value) && JSON.stringify(Object.keys(value)) === JSON.stringify(expected) ); } function normalizeReleaseIdentity(value) { if ( !exactKeys(value, [ 'schemaVersion', 'schema', 'product', 'version', 'node', 'architectureSupport', 'workspacePackageCount', 'legacyRootPackageExcluded', ]) || value.schemaVersion !== 2 || value.schema !== RELEASE_IDENTITY_SCHEMA || value.product !== 'qinglong3' || typeof value.version !== 'string' || !VERSION_PATTERN.test(value.version) || semver.valid(value.version) !== value.version || !exactKeys(value.node, ['version', 'engine']) || value.node.version !== '24.18.0' || value.node.engine !== '>=24.18.0 <25' || !exactKeys(value.architectureSupport, [ 'tier1', 'candidates', 'experimentalBlocked', 'legacyOnly', 'legacyLine', ]) || JSON.stringify(value.architectureSupport) !== JSON.stringify(ARCHITECTURE_SUPPORT) || value.workspacePackageCount !== 18 || value.legacyRootPackageExcluded !== true ) { fail('identity shape or value is incompatible'); } return Object.freeze({ ...value, node: Object.freeze({ ...value.node }), architectureSupport: Object.freeze({ ...value.architectureSupport, tier1: Object.freeze([...value.architectureSupport.tier1]), candidates: Object.freeze([...value.architectureSupport.candidates]), experimentalBlocked: Object.freeze([ ...value.architectureSupport.experimentalBlocked, ]), legacyOnly: Object.freeze([...value.architectureSupport.legacyOnly]), }), }); } function readReleaseIdentity(root) { const resolvedRoot = fs.realpathSync(path.resolve(root)); const filePath = path.join(resolvedRoot, RELEASE_IDENTITY_PATH); const stat = fs.lstatSync(filePath); if ( !stat.isFile() || stat.isSymbolicLink() || stat.size < 2 || stat.size > MAX_RELEASE_IDENTITY_BYTES || fs.realpathSync(filePath) !== filePath ) { fail('identity file must be one bounded canonical regular file'); } const contents = fs.readFileSync(filePath, 'utf8'); let parsed; try { parsed = JSON.parse(contents); } catch { fail('identity file must contain valid JSON'); } const identity = normalizeReleaseIdentity(parsed); if (`${JSON.stringify(identity, null, 2)}\n` !== contents) { fail('identity file must use exact canonical JSON encoding'); } return identity; } module.exports = Object.freeze({ ARCHITECTURE_SUPPORT, MAX_RELEASE_IDENTITY_BYTES, RELEASE_IDENTITY_PATH, RELEASE_IDENTITY_SCHEMA, VERSION_PATTERN, QingLong3ReleaseIdentityError, normalizeReleaseIdentity, readReleaseIdentity, });