93 lines
3.9 KiB
JavaScript
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 }),
|
|
})
|
|
}
|