mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
283 lines
8.5 KiB
JavaScript
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,
|
|
};
|