mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
* docs: design Linux frame-copy packaging * docs: plan Linux frame-copy packaging * feat(packaging): define Linux frame-copy profiles * fix(packaging): reject inherited profile names * feat(embedded-mpv): validate staged Linux runtime * fix(embedded-mpv): require Linux source packages * fix(embedded-mpv): harden Linux runtime staging * feat(embedded-mpv): build LGPL Linux runtime * fix(embedded-mpv): pin Linux runtime inputs * feat(embedded-mpv): build relocatable Linux helper * fix(embedded-mpv): require bundled Linux runtime * fix(embedded-mpv): make Linux runtime portable * feat(packaging): ship Linux frame-copy artifacts * fix(embedded-mpv): verify Linux helper linkage * fix(packaging): enforce Linux frame-copy isolation * fix(embedded-mpv): pin Linux display data * docs(embedded-mpv): document Linux frame-copy packaging * feat(embedded-mpv): probe Linux frame-copy runtime * test(embedded-mpv): smoke packaged Linux frame-copy * docs(embedded-mpv): clarify Linux system runtime baseline * fix(embedded-mpv): harden Linux runtime capability gate * ci: verify Linux frame-copy packages * test(embedded-mpv): harden packaged Linux smoke * test(embedded-mpv): preserve packaged GL mode * test(packaging): harden Linux package probes * fix(embedded-mpv): enable private Snap shared memory * fix(embedded-mpv): sanitize Linux helper environment * fix(packaging): enforce private Snap memory semantics * fix(packaging): reject ambiguous Snap memory metadata * fix(embedded-mpv): prioritize trusted Snap GL * fix(packaging): reject advanced Snap YAML semantics * fix(packaging): reject arbitrary Snap YAML aliases * feat(packaging): ship Linux runtime license notices * docs(embedded-mpv): document Linux runtime distribution * fix(packaging): parse Snap trailing comments safely * fix(release): gate Snap publish on public source release * fix(packaging): strip VCS metadata from source bundle * docs(packaging): clarify Linux source release gate * test(embedded-mpv): smoke missing bundled libmpv * style(embedded-mpv): format final validation inputs * fix(e2e): satisfy fixture index signature typing * fix(ci): declare fontconfig gperf generator * fix(embedded-mpv): hash runtime cache identities * fix(packaging): harden Linux frame-copy delivery * fix(packaging): tighten runtime delivery gates * fix(ci): decouple Linux runtime matrix * fix(packaging): harden Linux frame-copy delivery * fix(packaging): validate Linux frame-copy runtimes * fix(packaging): scope Snap Electron library checks * feat(packaging): ship Linux frame-copy runtimes * fix(packaging): improve Linux runtime smoke diagnostics * fix(packaging): expose bounded helper probe details * test(packaging): trace Snap EGL probe failures * fix(packaging): prefer core22 ABI in Snap helper * fix(packaging): bound helper probe capture * fix(packaging): harden Linux frame-copy releases * fix(packaging): canonicalize libplacebo submodule identity * fix(packaging): make source archive inspection portable * fix(packaging): harden Snap release verification
341 lines
10 KiB
JavaScript
341 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
|
|
const LIBMPV_NEEDED_PATTERN = /^libmpv\.so(?:\..*)?$/;
|
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
|
const LINUX_FRAME_COPY_ARTIFACTS = Object.freeze([
|
|
Object.freeze({
|
|
fileName: 'embedded_mpv.node',
|
|
label: 'embedded MPV addon',
|
|
mayLinkLibmpv: false,
|
|
}),
|
|
Object.freeze({
|
|
fileName: 'embedded_mpv_frame_reader.node',
|
|
label: 'embedded MPV frame reader',
|
|
mayLinkLibmpv: false,
|
|
}),
|
|
Object.freeze({
|
|
fileName: 'iptvnator_mpv_helper',
|
|
label: 'embedded MPV frame-copy helper',
|
|
mayLinkLibmpv: true,
|
|
}),
|
|
]);
|
|
|
|
function parseReadelfDynamic(output) {
|
|
if (typeof output !== 'string') {
|
|
throw new TypeError('readelf dynamic output must be a string.');
|
|
}
|
|
|
|
const dynamic = {
|
|
needed: [],
|
|
rpath: [],
|
|
runpath: [],
|
|
soname: [],
|
|
};
|
|
const dynamicEntryPattern =
|
|
/\((NEEDED|RPATH|RUNPATH|SONAME)\)[^[]*\[([^\]]*)\]/g;
|
|
for (const [, tag, value] of output.matchAll(dynamicEntryPattern)) {
|
|
if (tag === 'NEEDED') {
|
|
dynamic.needed.push(value);
|
|
continue;
|
|
}
|
|
if (tag === 'SONAME') {
|
|
dynamic.soname.push(value);
|
|
continue;
|
|
}
|
|
dynamic[tag.toLowerCase()].push(
|
|
...value.split(':').filter((entry) => entry.length > 0)
|
|
);
|
|
}
|
|
|
|
return dynamic;
|
|
}
|
|
|
|
function exactlyOneRuntimeFile(runtimeFiles, name) {
|
|
if (!Array.isArray(runtimeFiles)) {
|
|
throw new Error('Linux runtimeFiles metadata must be an array.');
|
|
}
|
|
const matchingRecords = runtimeFiles.filter(
|
|
(runtimeFile) => runtimeFile?.name === name
|
|
);
|
|
if (matchingRecords.length !== 1) {
|
|
throw new Error(
|
|
`Linux runtime must contain exactly one exact runtimeFiles record for ${name}.`
|
|
);
|
|
}
|
|
|
|
const [runtimeFile] = matchingRecords;
|
|
if (
|
|
!Number.isInteger(runtimeFile.size) ||
|
|
runtimeFile.size <= 0 ||
|
|
typeof runtimeFile.sha256 !== 'string' ||
|
|
!SHA256_PATTERN.test(runtimeFile.sha256)
|
|
) {
|
|
throw new Error(
|
|
`Linux runtimeFiles record for ${name} has invalid size or SHA-256 metadata.`
|
|
);
|
|
}
|
|
return runtimeFile;
|
|
}
|
|
|
|
function readVerifiedRuntimeFile(filePath, runtimeFile) {
|
|
let stat;
|
|
try {
|
|
stat = fs.lstatSync(filePath);
|
|
} catch {
|
|
throw new Error(`Missing copied Linux runtime file: ${filePath}`);
|
|
}
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Copied Linux runtime file must be a regular non-symbolic-link file: ${filePath}`
|
|
);
|
|
}
|
|
|
|
let descriptor;
|
|
try {
|
|
descriptor = fs.openSync(
|
|
filePath,
|
|
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)
|
|
);
|
|
const descriptorStat = fs.fstatSync(descriptor);
|
|
if (!descriptorStat.isFile()) {
|
|
throw new Error(
|
|
`Copied Linux runtime path is not a regular file: ${filePath}`
|
|
);
|
|
}
|
|
const contents = fs.readFileSync(descriptor);
|
|
if (contents.byteLength !== runtimeFile.size) {
|
|
throw new Error(
|
|
`Size mismatch for copied Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.size}, received ${contents.byteLength}.`
|
|
);
|
|
}
|
|
const actualSha256 = crypto
|
|
.createHash('sha256')
|
|
.update(contents)
|
|
.digest('hex');
|
|
if (actualSha256 !== runtimeFile.sha256) {
|
|
throw new Error(
|
|
`SHA-256 mismatch for copied Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.sha256}, received ${actualSha256}.`
|
|
);
|
|
}
|
|
} finally {
|
|
if (descriptor !== undefined) {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
}
|
|
|
|
function closureLibMpvSoname(runtimeDependencyClosure) {
|
|
if (!Array.isArray(runtimeDependencyClosure?.entries)) {
|
|
throw new Error(
|
|
'Validated Linux runtime dependency closure entries are required.'
|
|
);
|
|
}
|
|
|
|
const libMpvEntries = runtimeDependencyClosure.entries.filter(
|
|
(entry) =>
|
|
entry?.name === 'libmpv.so' ||
|
|
VERSIONED_LIBMPV_PATTERN.test(entry?.name)
|
|
);
|
|
const declaredSonames = libMpvEntries.map((entry) => entry.soname);
|
|
const uniqueSonames = new Set(declaredSonames);
|
|
if (
|
|
declaredSonames.length === 0 ||
|
|
declaredSonames.some(
|
|
(soname) =>
|
|
typeof soname !== 'string' ||
|
|
!VERSIONED_LIBMPV_PATTERN.test(soname)
|
|
) ||
|
|
uniqueSonames.size !== 1
|
|
) {
|
|
throw new Error(
|
|
'Validated Linux runtime closure must declare exactly one versioned libmpv SONAME.'
|
|
);
|
|
}
|
|
|
|
return declaredSonames[0];
|
|
}
|
|
|
|
function resolveVerifiedLinuxLibMpvSoname({
|
|
outputLibDir,
|
|
runtimeFiles,
|
|
runtimeDependencyClosure,
|
|
readDynamicSection,
|
|
}) {
|
|
if (typeof readDynamicSection !== 'function') {
|
|
throw new TypeError('Linux readelf dynamic reader is required.');
|
|
}
|
|
|
|
const expectedSoname = closureLibMpvSoname(runtimeDependencyClosure);
|
|
const linkerInputRecord = exactlyOneRuntimeFile(runtimeFiles, 'libmpv.so');
|
|
const exactSonameRecord = exactlyOneRuntimeFile(
|
|
runtimeFiles,
|
|
expectedSoname
|
|
);
|
|
const linkerInputPath = path.join(outputLibDir, 'libmpv.so');
|
|
const exactSonamePath = path.join(outputLibDir, expectedSoname);
|
|
|
|
readVerifiedRuntimeFile(linkerInputPath, linkerInputRecord);
|
|
readVerifiedRuntimeFile(exactSonamePath, exactSonameRecord);
|
|
|
|
const dynamic = parseReadelfDynamic(readDynamicSection(linkerInputPath));
|
|
if (
|
|
dynamic.soname.length !== 1 ||
|
|
!VERSIONED_LIBMPV_PATTERN.test(dynamic.soname[0])
|
|
) {
|
|
throw new Error(
|
|
'Copied Linux libmpv.so must contain exactly one DT_SONAME with a versioned libmpv basename.'
|
|
);
|
|
}
|
|
if (dynamic.soname[0] !== expectedSoname) {
|
|
throw new Error(
|
|
`Copied Linux libmpv.so DT_SONAME ${dynamic.soname[0]} does not match validated closure SONAME ${expectedSoname}.`
|
|
);
|
|
}
|
|
|
|
return expectedSoname;
|
|
}
|
|
|
|
function resolveLinuxFrameCopyLinkageInputs({
|
|
buildInputMode,
|
|
outputLibDir,
|
|
packagedLibmpvSoname,
|
|
readDynamicSection,
|
|
runtimeLibDir,
|
|
}) {
|
|
const systemDevelopment = buildInputMode === 'system-dev';
|
|
const linkerLibraryDir = systemDevelopment ? runtimeLibDir : outputLibDir;
|
|
if (
|
|
typeof linkerLibraryDir !== 'string' ||
|
|
linkerLibraryDir.trim().length === 0
|
|
) {
|
|
throw new Error(
|
|
'Linux frame-copy linkage requires a non-empty linker library directory.'
|
|
);
|
|
}
|
|
|
|
if (!systemDevelopment) {
|
|
return {
|
|
expectedLibmpvSoname: packagedLibmpvSoname,
|
|
linkerLibraryDir,
|
|
};
|
|
}
|
|
if (typeof readDynamicSection !== 'function') {
|
|
throw new TypeError('Linux readelf dynamic reader is required.');
|
|
}
|
|
|
|
const linkerInputPath = path.join(linkerLibraryDir, 'libmpv.so');
|
|
const dynamic = parseReadelfDynamic(readDynamicSection(linkerInputPath));
|
|
if (
|
|
dynamic.soname.length !== 1 ||
|
|
!VERSIONED_LIBMPV_PATTERN.test(dynamic.soname[0])
|
|
) {
|
|
throw new Error(
|
|
'The system-development libmpv linker input must contain exactly one versioned libmpv SONAME.'
|
|
);
|
|
}
|
|
|
|
return {
|
|
expectedLibmpvSoname: dynamic.soname[0],
|
|
linkerLibraryDir,
|
|
};
|
|
}
|
|
|
|
function assertRegularArtifact(filePath, label) {
|
|
let stat;
|
|
try {
|
|
stat = fs.lstatSync(filePath);
|
|
} catch {
|
|
throw new Error(`Missing ${label}: ${filePath}`);
|
|
}
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
throw new Error(
|
|
`${label} must be a regular non-symbolic-link file: ${filePath}`
|
|
);
|
|
}
|
|
}
|
|
|
|
function validateLinuxFrameCopyLinkage({
|
|
expectedLibmpvSoname,
|
|
outputDir,
|
|
readDynamicSection,
|
|
}) {
|
|
if (
|
|
typeof expectedLibmpvSoname !== 'string' ||
|
|
!VERSIONED_LIBMPV_PATTERN.test(expectedLibmpvSoname)
|
|
) {
|
|
throw new Error(
|
|
'Linux frame-copy linkage validation requires an exact versioned libmpv SONAME.'
|
|
);
|
|
}
|
|
if (typeof readDynamicSection !== 'function') {
|
|
throw new TypeError('Linux readelf dynamic reader is required.');
|
|
}
|
|
|
|
for (const artifact of LINUX_FRAME_COPY_ARTIFACTS) {
|
|
const artifactPath = path.join(outputDir, artifact.fileName);
|
|
assertRegularArtifact(artifactPath, artifact.label);
|
|
const dynamic = parseReadelfDynamic(readDynamicSection(artifactPath));
|
|
const libMpvDependencies = dynamic.needed.filter((dependency) =>
|
|
LIBMPV_NEEDED_PATTERN.test(dependency)
|
|
);
|
|
|
|
if (!artifact.mayLinkLibmpv) {
|
|
if (libMpvDependencies.length > 0) {
|
|
throw new Error(
|
|
`${artifact.label} must not have a direct libmpv DT_NEEDED entry; found ${libMpvDependencies.join(', ')}.`
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
libMpvDependencies.length !== 1 ||
|
|
libMpvDependencies[0] !== expectedLibmpvSoname
|
|
) {
|
|
throw new Error(
|
|
`${artifact.label} DT_NEEDED must contain exactly ${expectedLibmpvSoname}; found ${
|
|
libMpvDependencies.join(', ') || '<none>'
|
|
}.`
|
|
);
|
|
}
|
|
if (dynamic.rpath.length !== 0) {
|
|
throw new Error(
|
|
`${artifact.label} must not contain RPATH; found ${dynamic.rpath.join(':')}.`
|
|
);
|
|
}
|
|
if (
|
|
dynamic.runpath.length !== 1 ||
|
|
dynamic.runpath[0] !== '$ORIGIN/lib'
|
|
) {
|
|
throw new Error(
|
|
`${artifact.label} RUNPATH must be exactly $ORIGIN/lib; found ${
|
|
dynamic.runpath.join(':') || '<none>'
|
|
}.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function runWithCleanup(operation, cleanup) {
|
|
try {
|
|
return operation();
|
|
} catch (error) {
|
|
cleanup();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
parseReadelfDynamic,
|
|
resolveLinuxFrameCopyLinkageInputs,
|
|
resolveVerifiedLinuxLibMpvSoname,
|
|
runWithCleanup,
|
|
validateLinuxFrameCopyLinkage,
|
|
};
|