Files
iptvnator/tools/embedded-mpv/linux-runtime-manifest.cjs
T

283 lines
8.5 KiB
JavaScript

'use strict';
const path = require('node:path');
const LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION = 1;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
const SAFE_BASENAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
function isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function isSafeBasename(value) {
return (
isNonEmptyString(value) &&
path.basename(value) === value &&
!value.includes('/') &&
!value.includes('\\') &&
value !== '.' &&
value !== '..' &&
SAFE_BASENAME_PATTERN.test(value)
);
}
function validateSourceMetadata(errors, value, label) {
if (!isNonEmptyString(value.version)) {
errors.push(
`Linux runtime manifest ${label}.version must be a non-empty string.`
);
}
if (!isNonEmptyString(value.sourceUrl)) {
errors.push(
`Linux runtime manifest ${label}.sourceUrl must be a non-empty string.`
);
}
const hasSourceSha256 = value.sourceSha256 !== undefined;
const hasSourceGitCommit = value.sourceGitCommit !== undefined;
if (!hasSourceSha256 && !hasSourceGitCommit) {
errors.push(
`Linux runtime manifest ${label} must include sourceSha256 or sourceGitCommit.`
);
return;
}
if (
hasSourceSha256 &&
(typeof value.sourceSha256 !== 'string' ||
!SHA256_PATTERN.test(value.sourceSha256))
) {
errors.push(
`Linux runtime manifest ${label}.sourceSha256 must be a lowercase 64-character hexadecimal digest.`
);
}
if (
hasSourceGitCommit &&
(typeof value.sourceGitCommit !== 'string' ||
!GIT_COMMIT_PATTERN.test(value.sourceGitCommit))
) {
errors.push(
`Linux runtime manifest ${label}.sourceGitCommit must be a lowercase hexadecimal commit digest.`
);
}
}
function validatePackages(errors, packages) {
if (!isObject(packages) || Object.keys(packages).length === 0) {
errors.push(
'Linux runtime manifest packages must contain source package metadata.'
);
return;
}
for (const packageName of Object.keys(packages).sort()) {
const packageMetadata = packages[packageName];
const label = `packages.${packageName}`;
if (!isObject(packageMetadata)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
validateSourceMetadata(errors, packageMetadata, label);
if (!isNonEmptyString(packageMetadata.license)) {
errors.push(
`Linux runtime manifest ${label}.license must be a non-empty string.`
);
}
}
}
function validateFlags(errors, value, label, options) {
if (
!Array.isArray(value) ||
value.some((flag) => typeof flag !== 'string')
) {
errors.push(
`Linux runtime manifest ${label} must be an array of strings.`
);
return;
}
const addForbiddenErrors = () => {
for (const forbiddenFlag of options.forbidden) {
if (value.includes(forbiddenFlag)) {
errors.push(
`Linux runtime manifest ${label} must not include "${forbiddenFlag}".`
);
}
}
};
const addRequiredErrors = () => {
for (const requiredFlag of options.required) {
if (!value.includes(requiredFlag)) {
errors.push(
`Linux runtime manifest ${label} must include "${requiredFlag}".`
);
}
}
};
if (options.requiredFirst) {
addRequiredErrors();
addForbiddenErrors();
} else {
addForbiddenErrors();
addRequiredErrors();
}
}
function validateFfmpeg(errors, ffmpeg) {
if (!isObject(ffmpeg)) {
errors.push('Linux runtime manifest ffmpeg must be an object.');
return;
}
validateFlags(errors, ffmpeg.configureFlags, 'ffmpeg.configureFlags', {
forbidden: ['--enable-gpl', '--enable-nonfree'],
required: ['--disable-gpl', '--disable-nonfree'],
});
}
function validateMpv(errors, mpv) {
if (!isObject(mpv)) {
errors.push('Linux runtime manifest mpv must be an object.');
return;
}
validateFlags(errors, mpv.mesonFlags, 'mpv.mesonFlags', {
forbidden: ['-Dgpl=true'],
required: ['-Dgpl=false', '-Dlibmpv=true'],
requiredFirst: true,
});
if (Array.isArray(mpv.mesonFlags)) {
for (const flag of mpv.mesonFlags) {
if (
typeof flag === 'string' &&
flag.startsWith('-Dgpl=') &&
flag !== '-Dgpl=false' &&
flag !== '-Dgpl=true'
) {
errors.push(
`Linux runtime manifest mpv.mesonFlags must not include "${flag}".`
);
}
}
}
}
function validateRuntimeFiles(errors, runtimeFiles) {
if (!Array.isArray(runtimeFiles) || runtimeFiles.length === 0) {
errors.push(
'Linux runtime manifest runtimeFiles must be a non-empty array.'
);
return;
}
const names = new Set();
let hasVersionedLibMpv = false;
for (const [index, runtimeFile] of runtimeFiles.entries()) {
const label = `runtimeFiles[${index}]`;
if (!isObject(runtimeFile)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
const { name, sha256, size } = runtimeFile;
const safeName = isSafeBasename(name);
if (!safeName) {
errors.push(
`Linux runtime manifest ${label}.name must be a safe shared-library basename.`
);
}
if (
typeof name === 'string' &&
safeName &&
!SHARED_LIBRARY_PATTERN.test(name)
) {
errors.push(
`Linux runtime manifest ${label}.name must end in ".so" or a numeric ".so.N" suffix.`
);
}
if (!Number.isInteger(size) || size <= 0) {
errors.push(
`Linux runtime manifest ${label}.size must be a positive integer.`
);
}
if (typeof sha256 !== 'string' || !SHA256_PATTERN.test(sha256)) {
errors.push(
`Linux runtime manifest ${label}.sha256 must be a lowercase 64-character hexadecimal digest.`
);
}
if (typeof name === 'string') {
if (names.has(name)) {
errors.push(
`Linux runtime manifest runtimeFiles contains duplicate name "${name}".`
);
} else {
names.add(name);
}
if (VERSIONED_LIBMPV_PATTERN.test(name)) {
hasVersionedLibMpv = true;
}
}
}
if (!hasVersionedLibMpv) {
errors.push(
'Linux runtime manifest runtimeFiles must include a versioned libmpv.so.N entry.'
);
}
}
function validateLinuxRuntimeManifest(manifest) {
if (!isObject(manifest)) {
return ['Linux runtime manifest must be an object.'];
}
const errors = [];
if (manifest.schemaVersion !== LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION) {
errors.push(
`Linux runtime manifest schemaVersion must be ${LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION}.`
);
}
if (manifest.origin !== 'vendored-lgpl-source-build') {
errors.push(
'Linux runtime manifest origin must be "vendored-lgpl-source-build".'
);
}
if (manifest.platform !== 'linux') {
errors.push('Linux runtime manifest platform must be "linux".');
}
if (manifest.arch !== 'x64') {
errors.push('Linux runtime manifest arch must be "x64".');
}
validatePackages(errors, manifest.packages);
validateFfmpeg(errors, manifest.ffmpeg);
validateMpv(errors, manifest.mpv);
if (!isNonEmptyString(manifest.sourceDistribution)) {
errors.push(
'Linux runtime manifest sourceDistribution must be a non-empty string.'
);
}
validateRuntimeFiles(errors, manifest.runtimeFiles);
return errors;
}
module.exports = {
LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION,
validateLinuxRuntimeManifest,
};