Files
dsh/upstream/container/platform/lib/supported-target.mjs
T

93 lines
3.9 KiB
JavaScript

import { exactKeys, parseJsonDocument, TrustError } from './validation.mjs'
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
function parseSemver(value, label) {
if (typeof value !== 'string') throw new TrustError(`${label} must be a semantic version`)
const match = SEMVER.exec(value)
if (match === null) throw new TrustError(`${label} must be a semantic version`)
const prerelease = match[4] === undefined ? [] : match[4].split('.')
for (const identifier of prerelease) {
if (/^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith('0')) {
throw new TrustError(`${label} has an invalid numeric prerelease identifier`)
}
}
return {
core: match.slice(1, 4),
prerelease,
}
}
function compareNumeric(left, right) {
if (left.length !== right.length) return left.length < right.length ? -1 : 1
if (left === right) return 0
return left < right ? -1 : 1
}
function compareIdentifier(left, right) {
const leftNumeric = /^\d+$/.test(left)
const rightNumeric = /^\d+$/.test(right)
if (leftNumeric && rightNumeric) return compareNumeric(left, right)
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
if (left === right) return 0
return left < right ? -1 : 1
}
export function compareSemanticVersions(leftValue, rightValue, leftLabel = 'left version', rightLabel = 'right version') {
const left = parseSemver(leftValue, leftLabel)
const right = parseSemver(rightValue, rightLabel)
for (let index = 0; index < left.core.length; index += 1) {
const compared = compareNumeric(left.core[index], right.core[index])
if (compared !== 0) return compared
}
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length === 0 ? 1 : -1
}
const length = Math.max(left.prerelease.length, right.prerelease.length)
for (let index = 0; index < length; index += 1) {
if (left.prerelease[index] === undefined) return -1
if (right.prerelease[index] === undefined) return 1
const compared = compareIdentifier(left.prerelease[index], right.prerelease[index])
if (compared !== 0) return Math.sign(compared)
}
return 0
}
export function compareDshVersions(leftValue, rightValue) {
return compareSemanticVersions(leftValue, rightValue, 'left DSH version', 'right DSH version')
}
export function parseSupportedTarget(bytes) {
const object = parseJsonDocument(bytes, 'supported target')
exactKeys(object, ['environment', 'latestSupportedDsh', 'schema'], 'supported target')
if (object.schema !== 1) throw new TrustError('supported target schema must be 1')
parseSemver(object.latestSupportedDsh, 'latestSupportedDsh')
if (typeof object.environment !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(object.environment)) {
throw new TrustError('supported target environment is invalid')
}
return Object.freeze({
schema: 1,
latestSupportedDsh: object.latestSupportedDsh,
environment: object.environment,
})
}
export function validateSupportedTarget(targetBytes, environmentDefinitionBytes) {
const target = parseSupportedTarget(targetBytes)
const definition = parseJsonDocument(environmentDefinitionBytes, 'environment definition')
if (definition.version !== target.environment) {
throw new TrustError(`supported Environment ${JSON.stringify(target.environment)} does not match definition ${JSON.stringify(definition.version)}`)
}
return target
}
export function advanceSupportedDsh(target, upstreamVersion) {
const comparison = compareDshVersions(upstreamVersion, target.latestSupportedDsh)
if (comparison < 0) throw new TrustError('upstream DSH version would roll back Latest Supported')
if (comparison === 0) return Object.freeze({ changed: false, target })
return Object.freeze({
changed: true,
target: Object.freeze({ ...target, latestSupportedDsh: upstreamVersion }),
})
}