Files
iptvnator/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/source-runtime-validator.ts
T
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* 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
2026-07-18 17:28:22 +02:00

248 lines
7.6 KiB
TypeScript

import path from 'path';
import { isDeepStrictEqual } from 'util';
import {
GIT_COMMIT_PATTERN,
PINNED_SOURCE_PACKAGE_IDENTITIES,
PORTABLE_ABI_BASELINE,
SUBMODULE_RECORD_PATTERN,
VERSION_PATTERN,
} from './contracts';
import type { RuntimeFile } from './types';
import {
hasExactFields,
isObject,
isSafeRuntimeName,
} from './validation-primitives';
function compareDottedVersions(left: string, right: string): number {
const leftParts = left.split('.').map(Number);
const rightParts = right.split('.').map(Number);
const length = Math.max(leftParts.length, rightParts.length);
for (let index = 0; index < length; index += 1) {
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
if (difference !== 0) {
return difference;
}
}
return 0;
}
function validatesPinnedSourceIdentity(
candidate: unknown,
expected: (typeof PINNED_SOURCE_PACKAGE_IDENTITIES)[keyof typeof PINNED_SOURCE_PACKAGE_IDENTITIES]
): boolean {
if (
!isObject(candidate) ||
candidate.version !== expected.version ||
candidate.sourceUrl !== expected.sourceUrl ||
candidate.license !== expected.license
) {
return false;
}
if (
('sourceTag' in expected
? candidate.sourceTag !== expected.sourceTag
: candidate.sourceTag !== undefined) ||
('buildInput' in expected
? !isDeepStrictEqual(candidate.buildInput, expected.buildInput)
: candidate.buildInput !== undefined)
) {
return false;
}
if ('sourceSha256' in expected) {
return (
candidate.sourceSha256 === expected.sourceSha256 &&
candidate.sourceGitCommit === undefined &&
candidate.sourceSubmodules === undefined
);
}
return (
'sourceSubmodules' in expected &&
candidate.sourceGitCommit === expected.sourceGitCommit &&
GIT_COMMIT_PATTERN.test(candidate.sourceGitCommit) &&
candidate.sourceSha256 === undefined &&
validatesGitSubmoduleRecords(candidate.sourceSubmodules) &&
isDeepStrictEqual(candidate.sourceSubmodules, expected.sourceSubmodules)
);
}
function validatesGitSubmoduleRecords(value: unknown): boolean {
if (
!Array.isArray(value) ||
value.length === 0 ||
new Set(value).size !== value.length
) {
return false;
}
return value.every((record) => {
if (typeof record !== 'string') {
return false;
}
const match = record.match(SUBMODULE_RECORD_PATTERN);
if (!match) {
return false;
}
const submodulePath = match[1];
return (
!path.posix.isAbsolute(submodulePath) &&
!submodulePath
.split('/')
.some((segment) => segment === '.' || segment === '..')
);
});
}
function validateStringFlags(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every(
(flag) =>
typeof flag === 'string' &&
flag.length > 0 &&
flag.trim() === flag
) &&
new Set(value).size === value.length
);
}
function validateRuntimeAbi(
value: unknown,
runtimeFiles: RuntimeFile[]
): boolean {
if (
!isObject(value) ||
!hasExactFields(value, ['baseline', 'files']) ||
!isDeepStrictEqual(value.baseline, PORTABLE_ABI_BASELINE) ||
!Array.isArray(value.files)
) {
return false;
}
const abiFileNames: string[] = [];
for (const record of value.files) {
if (
!isObject(record) ||
!hasExactFields(record, [
'name',
'requiredGlibc',
'requiredGlibcxx',
]) ||
!isSafeRuntimeName(record.name)
) {
return false;
}
for (const [field, maximum] of [
['requiredGlibc', PORTABLE_ABI_BASELINE.glibcMaximum],
['requiredGlibcxx', PORTABLE_ABI_BASELINE.glibcxxMaximum],
] as const) {
const version = record[field];
if (
version !== null &&
(typeof version !== 'string' ||
!VERSION_PATTERN.test(version) ||
compareDottedVersions(version, maximum) > 0)
) {
return false;
}
}
abiFileNames.push(record.name);
}
return isDeepStrictEqual(
abiFileNames,
runtimeFiles.map(({ name }) => name)
);
}
/**
* The source builder and package verifier own exhaustive build-host, recipe,
* tool-version, URL and external-configuration validation. Startup repeats
* only the immutable policy boundary needed before sandbox relaxation:
* pinned source identities/licenses, LGPL flags, portable ABI, display-data
* distribution, and the exact runtime closure mirrored by the package.
*/
export function validateSourceRuntimePolicy(
value: unknown,
runtimeFiles: RuntimeFile[],
runtimeDependencyClosure: unknown,
externalSystemLibraries: unknown
): boolean {
if (
!isObject(value) ||
value.schemaVersion !== 1 ||
value.origin !== 'vendored-lgpl-source-build' ||
value.platform !== 'linux' ||
value.arch !== 'x64' ||
!isObject(value.packages) ||
!isObject(value.ffmpeg) ||
!isObject(value.mpv) ||
!isDeepStrictEqual(
Object.keys(value.packages).sort(),
Object.keys(PINNED_SOURCE_PACKAGE_IDENTITIES).sort()
)
) {
return false;
}
for (const [packageName, expectedIdentity] of Object.entries(
PINNED_SOURCE_PACKAGE_IDENTITIES
)) {
if (
!validatesPinnedSourceIdentity(
value.packages[packageName],
expectedIdentity
)
) {
return false;
}
}
if (
!validatesPinnedSourceIdentity(
value.ffmpeg,
PINNED_SOURCE_PACKAGE_IDENTITIES.ffmpeg
) ||
!validateStringFlags(value.ffmpeg.configureFlags) ||
!value.ffmpeg.configureFlags.includes('--disable-gpl') ||
!value.ffmpeg.configureFlags.includes('--disable-nonfree') ||
value.ffmpeg.configureFlags.includes('--enable-gpl') ||
value.ffmpeg.configureFlags.includes('--enable-nonfree') ||
!validatesPinnedSourceIdentity(
value.mpv,
PINNED_SOURCE_PACKAGE_IDENTITIES.mpv
) ||
!validateStringFlags(value.mpv.mesonFlags) ||
!value.mpv.mesonFlags.includes('-Dgpl=false') ||
!value.mpv.mesonFlags.includes('-Dlibmpv=true') ||
value.mpv.mesonFlags.includes('-Dgpl=true')
) {
return false;
}
if (
typeof value.sourceDistribution !== 'string' ||
!/\bhwdata\b/i.test(value.sourceDistribution) ||
!/\bpnp\.ids\b/i.test(value.sourceDistribution) ||
!/\blibdisplay-info\b/i.test(value.sourceDistribution) ||
value.runtimeTotalBytes !==
runtimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
) ||
!isDeepStrictEqual(value.runtimeFiles, runtimeFiles) ||
!isDeepStrictEqual(
value.runtimeDependencyClosure,
runtimeDependencyClosure
) ||
!isDeepStrictEqual(
value.externalSystemLibraries,
externalSystemLibraries
) ||
!validateRuntimeAbi(value.runtimeAbi, runtimeFiles)
) {
return false;
}
return true;
}