接口开始5%
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import path from 'node:path'
|
||||
|
||||
const assertOnlyFields = (value, fields, label) => {
|
||||
for (const field of Object.keys(value)) {
|
||||
if (!fields.has(field)) throw new Error(`${label} contains unknown field: ${field}`)
|
||||
}
|
||||
}
|
||||
|
||||
const requireObject = (value, label) => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error(`${label} must be an object`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const requireString = (value, label) => {
|
||||
if (typeof value !== 'string' || value.trim() === '') {
|
||||
throw new Error(`${label} must be a non-empty string`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const requirePositiveInteger = (value, label) => {
|
||||
if (!Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`)
|
||||
return value
|
||||
}
|
||||
|
||||
const requireIntegerInRange = (value, minimum, maximum, label) => {
|
||||
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
||||
throw new Error(`${label} must be an integer from ${minimum} to ${maximum}`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const requireBoolean = (value, label) => {
|
||||
if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`)
|
||||
return value
|
||||
}
|
||||
|
||||
const requireExact = (value, expected, label) => {
|
||||
if (value !== expected) throw new Error(`${label} must be ${expected}`)
|
||||
return value
|
||||
}
|
||||
|
||||
const resolveInsideWorkspace = (workspace, relativePath, label) => {
|
||||
requireString(relativePath, label)
|
||||
const absolutePath = path.resolve(workspace, relativePath)
|
||||
const relative = path.relative(workspace, absolutePath)
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
throw new Error(`${label} escapes workspace: ${relativePath}`)
|
||||
}
|
||||
return absolutePath
|
||||
}
|
||||
|
||||
const validatePixels = (value, label) => {
|
||||
const pixels = requireObject(value, label)
|
||||
assertOnlyFields(pixels, new Set(['width', 'height']), label)
|
||||
requirePositiveInteger(pixels.width, `${label}.width`)
|
||||
requirePositiveInteger(pixels.height, `${label}.height`)
|
||||
}
|
||||
|
||||
const validateAlphaAndEdge = (asset, id) => {
|
||||
const alpha = requireObject(asset.alpha, `${id}.alpha`)
|
||||
assertOnlyFields(alpha, new Set(['required', 'transparentOuterPadding', 'cornerMaxAlpha']), `${id}.alpha`)
|
||||
requireExact(alpha.required, true, `${id}.alpha.required`)
|
||||
requireIntegerInRange(alpha.transparentOuterPadding, 0, 4096, `${id}.alpha.transparentOuterPadding`)
|
||||
requireIntegerInRange(alpha.cornerMaxAlpha, 0, 255, `${id}.alpha.cornerMaxAlpha`)
|
||||
|
||||
const edge = requireObject(asset.edge, `${id}.edge`)
|
||||
assertOnlyFields(
|
||||
edge,
|
||||
new Set(['forbidChromaResidue', 'forbidLightFringe', 'premultipliedAlphaCheck']),
|
||||
`${id}.edge`,
|
||||
)
|
||||
requireBoolean(edge.forbidChromaResidue, `${id}.edge.forbidChromaResidue`)
|
||||
requireBoolean(edge.forbidLightFringe, `${id}.edge.forbidLightFringe`)
|
||||
requireBoolean(edge.premultipliedAlphaCheck, `${id}.edge.premultipliedAlphaCheck`)
|
||||
}
|
||||
|
||||
const validateProcessing = (processing, id) => {
|
||||
requireObject(processing, `${id}.processing`)
|
||||
const mode = requireString(processing.mode, `${id}.processing.mode`)
|
||||
|
||||
if (mode === 'chroma-stretch') {
|
||||
assertOnlyFields(
|
||||
processing,
|
||||
new Set(['mode', 'capWidth', 'keyColor', 'keyTolerance', 'paletteColors', 'indexedPng']),
|
||||
`${id}.processing`,
|
||||
)
|
||||
requirePositiveInteger(processing.capWidth, `${id}.processing.capWidth`)
|
||||
if (!/^#[A-Fa-f0-9]{6}$/.test(processing.keyColor)) {
|
||||
throw new Error(`${id}.processing.keyColor must be a six-digit RGB color`)
|
||||
}
|
||||
requireIntegerInRange(processing.keyTolerance, 0, 441, `${id}.processing.keyTolerance`)
|
||||
if (processing.paletteColors !== undefined) {
|
||||
requireIntegerInRange(processing.paletteColors, 2, 256, `${id}.processing.paletteColors`)
|
||||
}
|
||||
if (processing.indexedPng !== undefined) {
|
||||
requireBoolean(processing.indexedPng, `${id}.processing.indexedPng`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (mode === 'opaque-resize') {
|
||||
assertOnlyFields(processing, new Set(['mode', 'resample', 'outputMode']), `${id}.processing`)
|
||||
requireExact(processing.resample, 'lanczos', `${id}.processing.resample`)
|
||||
requireExact(processing.outputMode, 'RGB', `${id}.processing.outputMode`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (mode === 'opaque-cover-crop') {
|
||||
assertOnlyFields(processing, new Set(['mode', 'resample', 'anchor', 'outputMode']), `${id}.processing`)
|
||||
requireExact(processing.resample, 'lanczos', `${id}.processing.resample`)
|
||||
requireExact(processing.anchor, 'center', `${id}.processing.anchor`)
|
||||
requireExact(processing.outputMode, 'RGB', `${id}.processing.outputMode`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (mode === 'warm-gold-frame-extract') {
|
||||
assertOnlyFields(
|
||||
processing,
|
||||
new Set([
|
||||
'mode',
|
||||
'borderBand',
|
||||
'redGreenMin',
|
||||
'greenBlueMin',
|
||||
'redBlueMin',
|
||||
'redMaxExclusive',
|
||||
'blueMaxExclusive',
|
||||
'alphaOffset',
|
||||
'alphaScale',
|
||||
'outputMode',
|
||||
]),
|
||||
`${id}.processing`,
|
||||
)
|
||||
requirePositiveInteger(processing.borderBand, `${id}.processing.borderBand`)
|
||||
for (const field of ['redGreenMin', 'greenBlueMin', 'redBlueMin', 'alphaOffset']) {
|
||||
requireIntegerInRange(processing[field], 0, 255, `${id}.processing.${field}`)
|
||||
}
|
||||
for (const field of ['redMaxExclusive', 'blueMaxExclusive']) {
|
||||
requireIntegerInRange(processing[field], 1, 256, `${id}.processing.${field}`)
|
||||
}
|
||||
requirePositiveInteger(processing.alphaScale, `${id}.processing.alphaScale`)
|
||||
requireExact(processing.outputMode, 'RGBA', `${id}.processing.outputMode`)
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error(`${id} has unsupported processing.mode: ${mode}`)
|
||||
}
|
||||
|
||||
export const validateAssetBuildManifest = (manifest, workspace) => {
|
||||
requireObject(manifest, 'manifest')
|
||||
assertOnlyFields(manifest, new Set(['schemaVersion', 'kind', 'family', 'assets']), 'manifest')
|
||||
if (manifest.schemaVersion !== 3) throw new Error('schemaVersion must be 3')
|
||||
if (manifest.kind !== 'asset-build-manifest') throw new Error('kind must be asset-build-manifest')
|
||||
requireString(manifest.family, 'family')
|
||||
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
||||
throw new Error('assets must be a non-empty array')
|
||||
}
|
||||
|
||||
const ids = new Set()
|
||||
const outputs = new Set()
|
||||
for (const asset of manifest.assets) {
|
||||
requireObject(asset, 'asset')
|
||||
const processing = requireObject(asset.processing, 'asset.processing')
|
||||
const mode = requireString(processing.mode, 'asset.processing.mode')
|
||||
const transparent = ['chroma-stretch', 'warm-gold-frame-extract'].includes(mode)
|
||||
const fields = new Set(['id', 'source', 'output', 'sourcePixels', 'outputPixels', 'quality', 'processing'])
|
||||
if (transparent) {
|
||||
fields.add('alpha')
|
||||
fields.add('edge')
|
||||
}
|
||||
assertOnlyFields(asset, fields, 'asset')
|
||||
|
||||
const id = requireString(asset.id, 'asset.id')
|
||||
if (ids.has(id)) throw new Error(`duplicate asset id: ${id}`)
|
||||
ids.add(id)
|
||||
|
||||
resolveInsideWorkspace(workspace, asset.source, `${id}.source`)
|
||||
resolveInsideWorkspace(workspace, asset.output, `${id}.output`)
|
||||
if (outputs.has(asset.output)) throw new Error(`duplicate output: ${asset.output}`)
|
||||
outputs.add(asset.output)
|
||||
|
||||
validatePixels(asset.sourcePixels, `${id}.sourcePixels`)
|
||||
validatePixels(asset.outputPixels, `${id}.outputPixels`)
|
||||
|
||||
const processingNeedsAlpha = validateProcessing(processing, id)
|
||||
if (processingNeedsAlpha) validateAlphaAndEdge(asset, id)
|
||||
|
||||
const quality = requireObject(asset.quality, `${id}.quality`)
|
||||
assertOnlyFields(quality, new Set(['maxBytes', 'colorSpace']), `${id}.quality`)
|
||||
requirePositiveInteger(quality.maxBytes, `${id}.quality.maxBytes`)
|
||||
requireExact(quality.colorSpace, 'sRGB', `${id}.quality.colorSpace`)
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
Reference in New Issue
Block a user