fix(packaging): harden Linux frame-copy delivery

This commit is contained in:
4gray committed 2026-07-18 00:21:31 +02:00
1 parent f0c8f686c2
commit b6a7b87a61
38 files changed
+4016 -3026

No files matched your search

+2
View File
@@ -80,5 +80,7 @@ jobs:
while IFS= read -r SNAP_NAME; do
SNAP_FILE="${RUNNER_TEMP}/snap-release-downloads/${SNAP_NAME}"
echo "Publishing public release asset: ${SNAP_NAME}"
# Candidate/stable promotion is manual after installed-Snap frame-copy and missing-runtime fallback smoke.
# GitHub Actions never promotes automatically.
snapcraft upload --release=edge "${SNAP_FILE}"
done
+4 -2
View File
@@ -252,8 +252,10 @@ Key files:
`linux-frame-copy-runtime-sources.tar.xz` for the current checkout on every
run while retaining exact commit/submodule records. Automated Snap Store
publication is allowed only after a public `v*` GitHub release contains
both the Snap assets and exactly one matching source archive. Canonical
maintenance docs:
both the Snap assets and exactly one matching source archive. The workflow
uploads only to the Store's edge channel. Candidate/stable promotion is
manual after installed-Snap frame-copy and missing-runtime fallback smoke;
GitHub Actions never promotes automatically. Canonical maintenance docs:
`docs/architecture/embedded-mpv-native.md` and
`tools/embedded-mpv/README.md`.
+5 -3
View File
@@ -645,9 +645,11 @@ engine` (restart required) or
VCS-metadata-free `linux-frame-copy-runtime-sources.tar.xz` for the current
checkout while preserving exact commit/submodule records. Snap Store
publication runs only from a public `v*` GitHub release that already
contains the Snap assets and exactly one source archive. On Windows, package
validation requires the exact MPV DLL named by the helper's PE import table
beside the executable.
contains the Snap assets and exactly one source archive. The workflow uploads
only to the Store's edge channel. Candidate/stable promotion is manual after
installed-Snap frame-copy and missing-runtime fallback smoke; GitHub Actions
never promotes automatically. On Windows, package validation requires the
exact MPV DLL named by the helper's PE import table beside the executable.
Backend adapter:
`apps/electron-backend/src/app/services/embedded-mpv-frame-copy.adapter.ts`;
shared-controls adapter:
+19 -3
View File
@@ -36,7 +36,9 @@
"inputs": [
"production",
"^production",
"{workspaceRoot}/apps/electron-backend/native/build/Release/**"
"{workspaceRoot}/apps/electron-backend/native/build/Release/**",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts"
],
"options": {
"outputPath": "dist/apps/electron-backend",
@@ -98,7 +100,9 @@
"inputs": [
"production",
"^production",
"{workspaceRoot}/apps/electron-backend/native/build/Release/**"
"{workspaceRoot}/apps/electron-backend/native/build/Release/**",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts"
],
"options": {
"outputPath": "dist/apps/electron-backend",
@@ -193,11 +197,23 @@
}
},
"lint": {
"command": "eslint apps/electron-backend/**/*.ts"
"inputs": [
"default",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts"
],
"command": "eslint apps/electron-backend/**/*.ts \"apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts\" \"apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/**/*.ts\""
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"inputs": [
"default",
"^production",
"{workspaceRoot}/jest.preset.js",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts"
],
"options": {
"jestConfig": "apps/electron-backend/jest.config.ts"
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,307 @@
import runtimeProbeContract = require('../../../../../../tools/embedded-mpv/runtime-probe-contract.cjs');
import type { EmbeddedMpvFrameCopyRuntimeMode, RuntimeProfile } from './types';
interface RuntimeProfileContract {
origin: string;
runtimeMode: EmbeddedMpvFrameCopyRuntimeMode;
targets: ReadonlySet<string>;
}
export const RUNTIME_MANIFEST_NAME = 'embedded-mpv-runtime.json';
export const FRAME_COPY_ADDON_NAME = 'embedded_mpv.node';
export const FRAME_COPY_READER_NAME = 'embedded_mpv_frame_reader.node';
export const FRAME_COPY_HELPER_NAME = 'iptvnator_mpv_helper';
export const RUNTIME_PROBE_PROTOCOL = 1;
export const { RUNTIME_PROBE_TIMEOUT_MS } = runtimeProbeContract;
export const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
export const SAFE_RUNTIME_NAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
export const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
export const SHA256_PATTERN = /^[a-f0-9]{64}$/;
export const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
export const SUBMODULE_RECORD_PATTERN =
/^[a-f0-9]{40,64}\s+([A-Za-z0-9_+./-]+)(?:\s+\(.+\))?$/;
export const VERSION_PATTERN = /^\d+(?:\.\d+)+$/;
export const GLIBC_TOOLCHAIN_ALLOWLIST = [
'ld-linux-x86-64.so.2',
'libc.so.6',
'libdl.so.2',
'libgcc_s.so.1',
'libm.so.6',
'libpthread.so.0',
'librt.so.1',
'libstdc++.so.6',
] as const;
export const EXPECTED_EXTERNAL_SYSTEM_LIBRARIES = [
{
name: 'libEGL.so.1',
interface: 'EGL',
reason: 'System graphics-driver interface used by the frame-copy helper.',
},
{
name: 'libGL.so.1',
interface: 'OpenGL',
reason: 'System OpenGL compatibility interface supplied by the graphics stack.',
},
{
name: 'libGLX.so.0',
interface: 'OpenGL',
reason: 'GLVND OpenGL dispatch interface supplied by the graphics stack.',
},
{
name: 'libOpenGL.so.0',
interface: 'OpenGL',
reason: 'GLVND OpenGL interface supplied by the graphics stack.',
},
{
name: 'libasound.so.2',
interface: 'ALSA',
reason: 'Linux system audio interface intentionally used by libmpv.',
},
{
name: 'libdrm.so.2',
interface: 'DRM',
reason: 'Kernel graphics interface used by system GBM and VA-API drivers.',
},
{
name: 'libgbm.so.1',
interface: 'GBM',
reason: 'System graphics-buffer interface used by headless EGL rendering.',
},
{
name: 'libpulse.so.0',
interface: 'PulseAudio',
reason: 'Linux desktop audio interface intentionally used by libmpv.',
},
{
name: 'libva-drm.so.2',
interface: 'VA-API DRM',
reason: 'System VA-API DRM interface used for hardware decoding.',
},
{
name: 'libva.so.2',
interface: 'VA-API',
reason: 'System video-acceleration interface used for hardware decoding.',
},
] as const;
export const ALLOWED_EXTERNAL_LIBRARY_NAMES = new Set<string>([
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXPECTED_EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
]);
export const PORTABLE_ABI_BASELINE = {
distribution: 'Ubuntu 22.04',
glibcMaximum: '2.35',
glibcxxMaximum: '3.4.30',
} as const;
export const PINNED_SOURCE_PACKAGE_IDENTITIES = {
freetype: {
version: '2.13.3',
sourceUrl:
'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
sourceSha256:
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
license: 'FreeType License (FTL)',
},
fribidi: {
version: '1.0.16',
sourceUrl:
'https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz',
sourceSha256:
'1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c',
license: 'LGPL-2.1-or-later',
},
harfbuzz: {
version: '8.5.0',
sourceUrl:
'https://github.com/harfbuzz/harfbuzz/releases/download/8.5.0/harfbuzz-8.5.0.tar.xz',
sourceSha256:
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
license: 'MIT',
},
expat: {
version: '2.8.2',
sourceUrl:
'https://github.com/libexpat/libexpat/releases/download/R_2_8_2/expat-2.8.2.tar.xz',
sourceSha256:
'3ad89b8588e6644bd4e49981480d48b21289eebbcd4f0a1a4afb1c29f99b6ab4',
license: 'MIT',
},
fontconfig: {
version: '2.16.0',
sourceUrl:
'https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.16.0.tar.xz',
sourceSha256:
'6a33dc555cc9ba8b10caf7695878ef134eeb36d0af366041f639b1da9b6ed220',
license: 'MIT',
},
libass: {
version: '0.17.3',
sourceUrl:
'https://github.com/libass/libass/releases/download/0.17.3/libass-0.17.3.tar.xz',
sourceSha256:
'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
license: 'ISC',
},
openssl: {
version: '3.5.7',
sourceUrl:
'https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz',
sourceSha256:
'a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8',
license: 'Apache-2.0',
},
ffmpeg: {
version: '8.1',
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
sourceSha256:
'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
license: 'LGPL-2.1-or-later',
},
libplacebo: {
version: '7.360.1',
sourceUrl: 'https://github.com/haasn/libplacebo.git',
sourceTag: 'v7.360.1',
sourceGitCommit: 'cee9b076f2c63104ccfd497fa79c39a867293ec4',
license: 'LGPL-2.1-or-later',
},
hwdata: {
version: '0.409',
sourceUrl:
'https://github.com/vcrhonek/hwdata/archive/refs/tags/v0.409.tar.gz',
sourceSha256:
'23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd',
buildInput: {
consumer: 'libdisplay-info',
relativePath: 'pnp.ids',
purpose: 'PNP vendor lookup table compiled into libdisplay-info.',
},
license: 'GPL-2.0-or-later OR XFree86-1.0',
},
'libdisplay-info': {
version: '0.1.1',
sourceUrl:
'https://gitlab.freedesktop.org/emersion/libdisplay-info/-/releases/0.1.1/downloads/libdisplay-info-0.1.1.tar.xz',
sourceSha256:
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
license: 'MIT',
},
mpv: {
version: '0.41.0',
sourceUrl:
'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
sourceSha256:
'ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209',
license: 'LGPL-2.1-or-later with -Dgpl=false',
},
} as const;
export const EXPECTED_ARTIFACTS = {
addon: {
name: FRAME_COPY_ADDON_NAME,
regularFile: true,
readable: true,
},
frameReader: {
name: FRAME_COPY_READER_NAME,
regularFile: true,
readable: true,
},
helper: {
name: FRAME_COPY_HELPER_NAME,
regularFile: true,
readable: true,
executable: true,
},
};
export const EXPECTED_PROCESS_ISOLATION = {
addonLoadsLibmpv: false,
readerLoadsLibmpv: false,
electronLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
};
export const EXPECTED_DEVELOPMENT_ARTIFACTS = {
addon: FRAME_COPY_ADDON_NAME,
frameReader: FRAME_COPY_READER_NAME,
helper: FRAME_COPY_HELPER_NAME,
};
export const EXPECTED_DEVELOPMENT_PROCESS_ISOLATION = {
addonLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
};
export const DEVELOPMENT_MANIFEST_FIELDS = [
'allowedPackageRuntimeModes',
'arch',
'artifacts',
'buildInputMode',
'generatedAt',
'libmpvSoname',
'nativeViewFallback',
'origin',
'packageRuntimeAvailability',
'platform',
'processIsolation',
'runtimeFiles',
'runtimeTotalBytes',
'schemaVersion',
'sourceRuntime',
'sourceRuntimeValidated',
] as const;
export const SYSTEM_PACKAGE_DEPENDENCIES = {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
};
export const PROFILE_CONTRACTS = {
system: {
origin: 'system-libmpv-frame-copy',
runtimeMode: 'system',
targets: new Set(['deb', 'rpm', 'pacman']),
},
portable: {
origin: 'bundled-lgpl-frame-copy',
runtimeMode: 'bundled',
targets: new Set(['appimage', 'snap']),
},
flatpak: {
origin: 'bundled-lgpl-frame-copy',
runtimeMode: 'bundled',
targets: new Set(['flatpak']),
},
} as const satisfies Record<RuntimeProfile, RuntimeProfileContract>;
export const BASE_MANIFEST_FIELDS = [
'arch',
'artifacts',
'generatedAt',
'libmpvSoname',
'nativeViewFallback',
'origin',
'packageDependencies',
'platform',
'processIsolation',
'profile',
'runtimeFiles',
'runtimeMode',
'runtimeTotalBytes',
'schemaVersion',
'targets',
] as const;
export const BUNDLED_MANIFEST_FIELDS = [
...BASE_MANIFEST_FIELDS,
'externalSystemLibraries',
'runtimeDependencyClosure',
'sourceRuntime',
] as const;
@@ -0,0 +1,152 @@
import { mkdirSync } from 'fs';
import path from 'path';
import {
cloneManifest,
createDevelopmentFixture,
probeDevelopmentRuntime,
writeManifest,
} from './runtime-fixtures.test-helpers';
import {
createRuntimeTestContext,
type RuntimeTestContext,
} from './runtime-harness.test-helpers';
describe('embedded-mpv frame-copy development manifest', () => {
let context: RuntimeTestContext;
beforeEach(() => {
context = createRuntimeTestContext();
});
afterEach(() => {
context.dispose();
});
it.each(['system-dev', 'system-build-inputs', 'bundled-runtime'] as const)(
'accepts an exact unpackaged %s build manifest and still runs the helper probe',
(buildInputMode) => {
const fixture = createDevelopmentFixture(
context.rootDir,
buildInputMode
);
expect(
probeDevelopmentRuntime(
context.createProbe(),
fixture.helperPath
)
).toEqual(
expect.objectContaining({
usable: true,
runtimeMode:
buildInputMode === 'bundled-runtime'
? 'bundled'
: 'system',
})
);
expect(context.spawnRuntimeProbe).toHaveBeenCalledWith(
fixture.helperPath,
['--runtime-probe'],
expect.objectContaining({
env:
buildInputMode === 'bundled-runtime'
? {
PATH: '/usr/bin',
LD_LIBRARY_PATH: path.join(
fixture.nativeDir,
'lib'
),
}
: {
PATH: '/usr/bin',
},
})
);
}
);
it('keeps the packaged manifest contract strict when a development manifest is present', () => {
const fixture = createDevelopmentFixture(
context.rootDir,
'bundled-runtime'
);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it.each([
{
label: 'origin',
mutate(manifest: Record<string, unknown>) {
manifest.origin = 'system-libmpv-frame-copy';
},
},
{
label: 'architecture',
mutate(manifest: Record<string, unknown>) {
manifest.arch = 'arm64';
},
},
{
label: 'artifact set',
mutate(manifest: Record<string, unknown>) {
const artifacts = manifest.artifacts as Record<string, unknown>;
artifacts.helper = 'other-helper';
},
},
{
label: 'package availability',
mutate(manifest: Record<string, unknown>) {
manifest.packageRuntimeAvailability = {
system: true,
bundled: false,
};
},
},
{
label: 'unexpected field',
mutate(manifest: Record<string, unknown>) {
manifest.manifestContract = 'packaged';
},
},
])(
'rejects a development manifest with an invalid $label',
({ mutate }) => {
const fixture = createDevelopmentFixture(
context.rootDir,
'system-dev'
);
const manifest = cloneManifest(fixture.manifest);
mutate(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(
probeDevelopmentRuntime(
context.createProbe(),
fixture.helperPath
)
).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
}
);
it('rejects a system development manifest with a private runtime directory', () => {
const fixture = createDevelopmentFixture(context.rootDir, 'system-dev');
mkdirSync(path.join(fixture.nativeDir, 'lib'));
expect(
probeDevelopmentRuntime(context.createProbe(), fixture.helperPath)
).toEqual({
usable: false,
reason: 'runtime-library-directory-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,91 @@
import path from 'path';
import { createLinuxFrameCopyHelperEnvironment } from '../embedded-mpv-frame-copy-runtime';
describe('createLinuxFrameCopyHelperEnvironment', () => {
it('removes ambient loader overrides for system packages', () => {
expect(
createLinuxFrameCopyHelperEnvironment(
{
PATH: '/usr/bin',
HOME: '/home/user',
LD_LIBRARY_PATH: '/tmp/hostile-libs',
LD_PRELOAD: '/tmp/inject.so',
},
'/opt/iptvnator/native',
'system'
)
).toEqual({
PATH: '/usr/bin',
HOME: '/home/user',
});
});
it('keeps trusted Snap GL roots ahead of generic Snap libraries', () => {
const snapRoot = '/snap/iptvnator/42';
const nativeDir = path.join(
snapRoot,
'resources',
'app.asar.unpacked',
'electron-backend',
'native'
);
expect(
createLinuxFrameCopyHelperEnvironment(
{
PATH: '/snap/bin:/usr/bin',
SNAP: snapRoot,
SNAP_LIBRARY_PATH: [
'/var/lib/snapd/lib/gl',
'/tmp/hostile-gl',
'/var/lib/snapd/lib/gl/nvidia',
'/var/lib/snapd/lib/gl-evil',
].join(':'),
LD_LIBRARY_PATH: '/tmp/hostile-libs',
LD_PRELOAD: '/tmp/inject.so',
},
nativeDir,
'bundled'
)
).toEqual({
PATH: '/snap/bin:/usr/bin',
SNAP: snapRoot,
SNAP_LIBRARY_PATH: [
'/var/lib/snapd/lib/gl',
'/tmp/hostile-gl',
'/var/lib/snapd/lib/gl/nvidia',
'/var/lib/snapd/lib/gl-evil',
].join(':'),
LD_LIBRARY_PATH: [
path.join(nativeDir, 'lib'),
'/var/lib/snapd/lib/gl',
'/var/lib/snapd/lib/gl/nvidia',
path.join(snapRoot, 'lib'),
path.join(snapRoot, 'usr', 'lib'),
path.join(snapRoot, 'lib', 'x86_64-linux-gnu'),
path.join(snapRoot, 'usr', 'lib', 'x86_64-linux-gnu'),
].join(':'),
});
});
it('does not trust Snap loader paths when nativeDir is outside the declared mount', () => {
expect(
createLinuxFrameCopyHelperEnvironment(
{
PATH: '/usr/bin',
SNAP: '/snap/iptvnator/42',
SNAP_LIBRARY_PATH: '/var/lib/snapd/lib/gl:/tmp/hostile-gl',
LD_LIBRARY_PATH: '/tmp/hostile-libs',
LD_PRELOAD: '/tmp/inject.so',
},
'/opt/iptvnator/native',
'bundled'
)
).toEqual({
PATH: '/usr/bin',
SNAP: '/snap/iptvnator/42',
SNAP_LIBRARY_PATH: '/var/lib/snapd/lib/gl:/tmp/hostile-gl',
LD_LIBRARY_PATH: '/opt/iptvnator/native/lib',
});
});
});
@@ -0,0 +1,105 @@
import path from 'path';
import type { EmbeddedMpvFrameCopyRuntimeMode } from './types';
const TRUSTED_SNAP_MOUNT_ROOTS = ['/snap', '/var/lib/snapd/snap'] as const;
const TRUSTED_SNAP_GL_ROOT = '/var/lib/snapd/lib/gl';
function isPathInside(
parentPath: string,
candidatePath: string,
allowEqual: boolean
): boolean {
const relativePath = path.relative(parentPath, candidatePath);
if (relativePath === '') {
return allowEqual;
}
return (
relativePath !== '..' &&
!relativePath.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relativePath)
);
}
function resolveTrustedSnapRoot(
environment: NodeJS.ProcessEnv,
nativeDir: string
): string | null {
const declaredSnapRoot = environment.SNAP;
if (
!declaredSnapRoot ||
!path.isAbsolute(declaredSnapRoot) ||
!path.isAbsolute(nativeDir)
) {
return null;
}
const normalizedSnapRoot = path.resolve(declaredSnapRoot);
const resemblesReadOnlySnapMount = TRUSTED_SNAP_MOUNT_ROOTS.some(
(mountRoot) => {
const relativePath = path.relative(mountRoot, normalizedSnapRoot);
return (
isPathInside(mountRoot, normalizedSnapRoot, false) &&
relativePath.split(path.sep).filter(Boolean).length >= 2
);
}
);
if (
!resemblesReadOnlySnapMount ||
!isPathInside(normalizedSnapRoot, path.resolve(nativeDir), false)
) {
return null;
}
return normalizedSnapRoot;
}
function getTrustedSnapLibraryPaths(
environment: NodeJS.ProcessEnv,
snapRoot: string
): string[] {
const snapLibraryPaths = (environment.SNAP_LIBRARY_PATH ?? '')
.split(':')
.filter(Boolean)
.filter((libraryPath) => path.isAbsolute(libraryPath))
.map((libraryPath) => path.resolve(libraryPath))
.filter((libraryPath) =>
isPathInside(TRUSTED_SNAP_GL_ROOT, libraryPath, true)
);
return [
...snapLibraryPaths,
path.join(snapRoot, 'lib'),
path.join(snapRoot, 'usr', 'lib'),
path.join(snapRoot, 'lib', 'x86_64-linux-gnu'),
path.join(snapRoot, 'usr', 'lib', 'x86_64-linux-gnu'),
];
}
/**
* Builds the loader environment shared by the bounded startup probe and each
* real Linux helper session. The validated package profile is authoritative:
* system packages use the system loader, while bundled packages start at
* native/lib and may add only immutable-looking Snap runtime/GL roots.
*/
export function createLinuxFrameCopyHelperEnvironment(
environment: NodeJS.ProcessEnv,
nativeDir: string,
runtimeMode: EmbeddedMpvFrameCopyRuntimeMode
): NodeJS.ProcessEnv {
const helperEnvironment = { ...environment };
delete helperEnvironment.LD_LIBRARY_PATH;
delete helperEnvironment.LD_PRELOAD;
if (runtimeMode === 'system') {
return helperEnvironment;
}
const libraryPaths = [path.join(nativeDir, 'lib')];
const trustedSnapRoot = resolveTrustedSnapRoot(environment, nativeDir);
if (trustedSnapRoot) {
libraryPaths.push(
...getTrustedSnapLibraryPaths(environment, trustedSnapRoot)
);
}
helperEnvironment.LD_LIBRARY_PATH = [...new Set(libraryPaths)].join(':');
return helperEnvironment;
}
@@ -0,0 +1,126 @@
import { SUCCESS_OUTPUT } from './runtime.spec-data';
import { createFixture } from './runtime-fixtures.test-helpers';
import {
createRuntimeTestContext,
type RuntimeTestContext,
} from './runtime-harness.test-helpers';
describe('embedded-mpv frame-copy helper failures', () => {
let context: RuntimeTestContext;
beforeEach(() => {
context = createRuntimeTestContext();
});
afterEach(() => {
context.dispose();
});
it('converts a thrown spawn failure into a stable result', () => {
const fixture = createFixture(context.rootDir);
context.spawnRuntimeProbe.mockImplementation(() => {
throw new Error('spawn exploded');
});
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'helper-probe-spawn-error',
});
});
it.each([
{
label: 'timeout',
spawnResult: {
status: null,
signal: 'SIGTERM',
stdout: '',
stderr: '',
error: Object.assign(new Error('timed out'), {
code: 'ETIMEDOUT',
}),
},
reason: 'helper-probe-timeout',
},
{
label: 'spawn error',
spawnResult: {
status: null,
signal: null,
stdout: '',
stderr: '',
error: Object.assign(new Error('spawn failed'), {
code: 'EACCES',
}),
},
reason: 'helper-probe-spawn-error',
},
{
label: 'nonzero exit',
spawnResult: {
status: 1,
signal: null,
stdout: '{"protocol":1,"usable":false,"reason":"egl-unavailable"}\n',
stderr: '',
},
reason: 'helper-probe-failed',
},
{
label: 'signal',
spawnResult: {
status: null,
signal: 'SIGKILL',
stdout: '',
stderr: '',
},
reason: 'helper-probe-signaled',
},
{
label: 'invalid JSON',
spawnResult: {
status: 0,
signal: null,
stdout: 'not-json\n',
stderr: '',
},
reason: 'helper-probe-invalid-output',
},
{
label: 'multiple lines',
spawnResult: {
status: 0,
signal: null,
stdout: `${SUCCESS_OUTPUT}${SUCCESS_OUTPUT}`,
stderr: '',
},
reason: 'helper-probe-invalid-output',
},
{
label: 'wrong protocol',
spawnResult: {
status: 0,
signal: null,
stdout: '{"protocol":2,"usable":true,"libmpv":"2.3","renderApi":"egl"}\n',
stderr: '',
},
reason: 'helper-probe-protocol-mismatch',
},
{
label: 'unusable success',
spawnResult: {
status: 0,
signal: null,
stdout: '{"protocol":1,"usable":false,"reason":"egl-unavailable"}\n',
stderr: '',
},
reason: 'helper-probe-unusable',
},
])('fails closed on helper $label', ({ spawnResult, reason }) => {
const fixture = createFixture(context.rootDir);
context.spawnRuntimeProbe.mockReturnValue(spawnResult);
expect(context.createProbe()(fixture.helperPath)).toEqual(
expect.objectContaining({ usable: false, reason })
);
});
});
@@ -0,0 +1,148 @@
import { unlinkSync, writeFileSync } from 'fs';
import {
cloneManifest,
createFixture,
mirrorBundledManifestFields,
writeManifest,
} from './runtime-fixtures.test-helpers';
import {
createRuntimeTestContext,
type RuntimeTestContext,
} from './runtime-harness.test-helpers';
describe('embedded-mpv frame-copy packaged manifest policy', () => {
let context: RuntimeTestContext;
beforeEach(() => {
context = createRuntimeTestContext();
});
afterEach(() => {
context.dispose();
});
it('rejects a missing or malformed manifest without throwing', () => {
const missing = createFixture(context.rootDir);
unlinkSync(missing.manifestPath);
expect(context.createProbe()(missing.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-missing',
});
const malformed = createFixture(context.rootDir, 'portable');
writeFileSync(malformed.manifestPath, '{broken\n', { mode: 0o644 });
expect(context.createProbe()(malformed.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it.each([
['origin', 'system-libmpv-frame-copy'],
['arch', 'arm64'],
['runtimeMode', 'system'],
['targets', ['deb']],
['unexpectedField', true],
])('rejects a bundled profile mismatch in %s', (field, value) => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
manifest[field] = value;
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it.each([
['system', ['deb', 'pacman']],
['portable', ['appimage']],
] as const)(
'rejects an allowed subset of the exact %s profile targets',
(profile, targets) => {
const fixture = createFixture(context.rootDir, profile);
const manifest = cloneManifest(fixture.manifest);
manifest.targets = targets;
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
}
);
it('rejects a bundled closure dependency outside the deterministic system allowlist', () => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
const closure = manifest.runtimeDependencyClosure as {
entries: Array<{ needed: string[] }>;
externalDependencies: string[];
};
closure.entries[0].needed = ['libambient-only.so.1'];
closure.externalDependencies = ['libambient-only.so.1'];
mirrorBundledManifestFields(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('requires externalDependencies to exactly equal the sorted external closure', () => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
const closure = manifest.runtimeDependencyClosure as {
entries: Array<{ needed: string[] }>;
externalDependencies: string[];
};
closure.entries[0].needed = ['libEGL.so.1', 'libc.so.6'];
closure.externalDependencies = [];
mirrorBundledManifestFields(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('accepts only the exact deterministic external-system library declaration', () => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
(manifest.externalSystemLibraries as unknown[]).pop();
mirrorBundledManifestFields(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('accepts bundled dependencies on declared system interfaces and the glibc toolchain', () => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
const closure = manifest.runtimeDependencyClosure as {
entries: Array<{ needed: string[] }>;
externalDependencies: string[];
};
closure.entries[0].needed = ['libEGL.so.1', 'libc.so.6'];
closure.externalDependencies = ['libEGL.so.1', 'libc.so.6'];
mirrorBundledManifestFields(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual(
expect.objectContaining({ usable: true, runtimeMode: 'bundled' })
);
expect(context.spawnRuntimeProbe).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,260 @@
import { isDeepStrictEqual } from 'util';
import {
BASE_MANIFEST_FIELDS,
BUNDLED_MANIFEST_FIELDS,
DEVELOPMENT_MANIFEST_FIELDS,
EXPECTED_ARTIFACTS,
EXPECTED_DEVELOPMENT_ARTIFACTS,
EXPECTED_DEVELOPMENT_PROCESS_ISOLATION,
EXPECTED_PROCESS_ISOLATION,
PROFILE_CONTRACTS,
SYSTEM_PACKAGE_DEPENDENCIES,
VERSIONED_LIBMPV_PATTERN,
} from './contracts';
import { validateRuntimeClosure } from './runtime-closure-validator';
import { validateSourceRuntimePolicy } from './source-runtime-validator';
import type { RuntimeProfile, ValidManifest, ValidationResult } from './types';
import {
hasExactFields,
isObject,
validateRuntimeFiles,
validateTargets,
validationFailure,
} from './validation-primitives';
export function validatePackagedManifest(
manifest: Record<string, unknown>
): ValidationResult<ValidManifest> {
if (
manifest.schemaVersion !== 1 ||
manifest.platform !== 'linux' ||
manifest.arch !== 'x64' ||
typeof manifest.profile !== 'string' ||
!Object.prototype.hasOwnProperty.call(
PROFILE_CONTRACTS,
manifest.profile
)
) {
return validationFailure('runtime-manifest-invalid');
}
const profile = manifest.profile as RuntimeProfile;
const contract = PROFILE_CONTRACTS[profile];
if (
manifest.origin !== contract.origin ||
manifest.runtimeMode !== contract.runtimeMode ||
!hasExactFields(
manifest,
contract.runtimeMode === 'bundled'
? BUNDLED_MANIFEST_FIELDS
: BASE_MANIFEST_FIELDS
) ||
typeof manifest.generatedAt !== 'string' ||
manifest.generatedAt.trim() === '' ||
Number.isNaN(Date.parse(manifest.generatedAt)) ||
!validateTargets(manifest.targets, contract.targets) ||
!isDeepStrictEqual(manifest.artifacts, EXPECTED_ARTIFACTS) ||
!isDeepStrictEqual(
manifest.processIsolation,
EXPECTED_PROCESS_ISOLATION
) ||
manifest.nativeViewFallback !== 'process-isolated mpv --wid' ||
typeof manifest.libmpvSoname !== 'string' ||
!VERSIONED_LIBMPV_PATTERN.test(manifest.libmpvSoname)
) {
return validationFailure('runtime-manifest-invalid');
}
if (contract.runtimeMode === 'system') {
if (
!isDeepStrictEqual(
manifest.packageDependencies,
SYSTEM_PACKAGE_DEPENDENCIES
) ||
!isDeepStrictEqual(manifest.runtimeFiles, []) ||
manifest.runtimeTotalBytes !== 0
) {
return validationFailure('runtime-manifest-invalid');
}
return {
value: {
profile,
runtimeMode: 'system',
runtimeFiles: [],
},
};
}
const runtimeFiles = validateRuntimeFiles(manifest.runtimeFiles);
if (
!runtimeFiles ||
!isDeepStrictEqual(manifest.packageDependencies, {}) ||
!runtimeFiles.some(({ name }) => name === 'libmpv.so') ||
!runtimeFiles.some(({ name }) => name === manifest.libmpvSoname) ||
manifest.runtimeTotalBytes !==
runtimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
) ||
!validateRuntimeClosure(
manifest.runtimeDependencyClosure,
runtimeFiles,
manifest.libmpvSoname,
manifest.externalSystemLibraries
) ||
!validateSourceRuntimePolicy(
manifest.sourceRuntime,
runtimeFiles,
manifest.runtimeDependencyClosure,
manifest.externalSystemLibraries
)
) {
return validationFailure('runtime-manifest-invalid');
}
return {
value: {
profile,
runtimeMode: 'bundled',
runtimeFiles,
},
};
}
function validateSystemDevelopmentSource(
value: unknown,
buildInputMode: 'system-dev' | 'system-build-inputs'
): boolean {
if (buildInputMode === 'system-dev') {
return isDeepStrictEqual(value, {
linuxBackend: 'process-isolated mpv --wid',
warning: 'Development-only unmanaged system libmpv toolchain.',
});
}
if (
!isObject(value) ||
!hasExactFields(value, [
'buildInputs',
'linuxBackend',
'sourceDistribution',
]) ||
value.linuxBackend !== 'process-isolated mpv --wid' ||
typeof value.sourceDistribution !== 'string' ||
value.sourceDistribution.trim() === '' ||
!isObject(value.buildInputs) ||
!hasExactFields(value.buildInputs, ['libmpvDevPackage', 'mpvPackage'])
) {
return false;
}
return ['libmpvDevPackage', 'mpvPackage'].every((packageField) => {
const packageName = value.buildInputs[packageField];
return typeof packageName === 'string' && packageName.trim().length > 0;
});
}
export function validateDevelopmentManifest(
manifest: Record<string, unknown>
): ValidationResult<ValidManifest> {
const buildInputMode = manifest.buildInputMode;
if (
!hasExactFields(manifest, DEVELOPMENT_MANIFEST_FIELDS) ||
manifest.schemaVersion !== 1 ||
manifest.origin !== 'linux-frame-copy-build' ||
manifest.platform !== 'linux' ||
manifest.arch !== 'x64' ||
typeof manifest.generatedAt !== 'string' ||
manifest.generatedAt.trim() === '' ||
Number.isNaN(Date.parse(manifest.generatedAt)) ||
!['system-dev', 'system-build-inputs', 'bundled-runtime'].includes(
String(buildInputMode)
) ||
!isDeepStrictEqual(manifest.allowedPackageRuntimeModes, [
'system',
'bundled',
]) ||
!isDeepStrictEqual(
manifest.artifacts,
EXPECTED_DEVELOPMENT_ARTIFACTS
) ||
!isDeepStrictEqual(
manifest.processIsolation,
EXPECTED_DEVELOPMENT_PROCESS_ISOLATION
) ||
manifest.nativeViewFallback !== 'process-isolated mpv --wid'
) {
return validationFailure('runtime-manifest-invalid');
}
if (
buildInputMode === 'system-dev' ||
buildInputMode === 'system-build-inputs'
) {
if (
manifest.sourceRuntimeValidated !== false ||
!isDeepStrictEqual(manifest.packageRuntimeAvailability, {
system: false,
bundled: false,
}) ||
manifest.libmpvSoname !== null ||
!isDeepStrictEqual(manifest.runtimeFiles, []) ||
manifest.runtimeTotalBytes !== 0 ||
!validateSystemDevelopmentSource(
manifest.sourceRuntime,
buildInputMode
)
) {
return validationFailure('runtime-manifest-invalid');
}
return {
value: {
profile: 'development',
runtimeMode: 'system',
runtimeFiles: [],
},
};
}
const sourceRuntime = manifest.sourceRuntime;
const runtimeFiles = validateRuntimeFiles(manifest.runtimeFiles);
if (
buildInputMode !== 'bundled-runtime' ||
manifest.sourceRuntimeValidated !== true ||
!isDeepStrictEqual(manifest.packageRuntimeAvailability, {
system: true,
bundled: true,
}) ||
typeof manifest.libmpvSoname !== 'string' ||
!VERSIONED_LIBMPV_PATTERN.test(manifest.libmpvSoname) ||
!runtimeFiles ||
!runtimeFiles.some(({ name }) => name === 'libmpv.so') ||
!runtimeFiles.some(({ name }) => name === manifest.libmpvSoname) ||
manifest.runtimeTotalBytes !==
runtimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
) ||
!isObject(sourceRuntime) ||
!validateRuntimeClosure(
sourceRuntime.runtimeDependencyClosure,
runtimeFiles,
manifest.libmpvSoname,
sourceRuntime.externalSystemLibraries
) ||
!validateSourceRuntimePolicy(
sourceRuntime,
runtimeFiles,
sourceRuntime.runtimeDependencyClosure,
sourceRuntime.externalSystemLibraries
)
) {
return validationFailure('runtime-manifest-invalid');
}
return {
value: {
profile: 'development',
runtimeMode: 'bundled',
runtimeFiles,
},
};
}
@@ -0,0 +1,225 @@
import {
chmodSync,
constants as fsConstants,
mkdirSync,
readFileSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from 'fs';
import path from 'path';
import type { RuntimeFile, RuntimeFixture } from './runtime.spec-data';
import {
cloneManifest,
createFixture,
writeManifest,
} from './runtime-fixtures.test-helpers';
import {
createRuntimeTestContext,
type RuntimeTestContext,
} from './runtime-harness.test-helpers';
describe('embedded-mpv frame-copy package integrity', () => {
let context: RuntimeTestContext;
beforeEach(() => {
context = createRuntimeTestContext();
});
afterEach(() => {
context.dispose();
});
it('rejects system manifests with a private library directory', () => {
const fixture = createFixture(context.rootDir);
mkdirSync(path.join(fixture.nativeDir, 'lib'));
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-library-directory-invalid',
});
});
it.each([
{
label: 'missing addon',
mutate(fixture: RuntimeFixture) {
unlinkSync(path.join(fixture.nativeDir, 'embedded_mpv.node'));
},
reason: 'runtime-artifact-missing',
},
{
label: 'non-executable helper',
mutate(fixture: RuntimeFixture) {
chmodSync(fixture.helperPath, 0o644);
},
reason: 'runtime-artifact-invalid',
},
{
label: 'wrong reader mode',
mutate(fixture: RuntimeFixture) {
chmodSync(
path.join(
fixture.nativeDir,
'embedded_mpv_frame_reader.node'
),
0o600
);
},
reason: 'runtime-artifact-invalid',
},
{
label: 'symlinked helper',
mutate(fixture: RuntimeFixture) {
const target = `${fixture.helperPath}.real`;
writeFileSync(target, '#!/bin/sh\n', { mode: 0o755 });
unlinkSync(fixture.helperPath);
symlinkSync(target, fixture.helperPath);
},
reason: 'runtime-artifact-invalid',
},
{
label: 'reader directory',
mutate(fixture: RuntimeFixture) {
const readerPath = path.join(
fixture.nativeDir,
'embedded_mpv_frame_reader.node'
);
unlinkSync(readerPath);
mkdirSync(readerPath);
},
reason: 'runtime-artifact-invalid',
},
])('rejects a $label', ({ mutate, reason }) => {
const fixture = createFixture(context.rootDir);
mutate(fixture);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason,
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it.each([
{
label: 'missing declared library',
mutate(fixture: RuntimeFixture) {
unlinkSync(path.join(fixture.nativeDir, 'lib', 'libmpv.so.2'));
},
reason: 'runtime-library-missing',
},
{
label: 'undeclared library',
mutate(fixture: RuntimeFixture) {
writeFileSync(
path.join(fixture.nativeDir, 'lib', 'libextra.so'),
'extra'
);
},
reason: 'runtime-library-undeclared',
},
{
label: 'library size mismatch',
mutate(fixture: RuntimeFixture) {
writeFileSync(
path.join(fixture.nativeDir, 'lib', 'libmpv.so.2'),
'different-length'
);
},
reason: 'runtime-library-size-mismatch',
},
{
label: 'library hash mismatch',
mutate(fixture: RuntimeFixture) {
const runtimePath = path.join(
fixture.nativeDir,
'lib',
'libmpv.so.2'
);
const original = readFileSync(runtimePath);
writeFileSync(
runtimePath,
Buffer.from(original.map((value) => value ^ 0xff))
);
},
reason: 'runtime-library-hash-mismatch',
},
{
label: 'symlinked library',
mutate(fixture: RuntimeFixture) {
const runtimePath = path.join(
fixture.nativeDir,
'lib',
'libmpv.so.2'
);
const targetPath = path.join(
fixture.nativeDir,
'libmpv-real.so.2'
);
writeFileSync(
targetPath,
fixture.runtimeContents['libmpv.so.2']
);
unlinkSync(runtimePath);
symlinkSync(targetPath, runtimePath);
},
reason: 'runtime-library-invalid',
},
{
label: 'library directory',
mutate(fixture: RuntimeFixture) {
const runtimePath = path.join(
fixture.nativeDir,
'lib',
'libmpv.so.2'
);
unlinkSync(runtimePath);
mkdirSync(runtimePath);
},
reason: 'runtime-library-invalid',
},
])('rejects a $label', ({ mutate, reason }) => {
const fixture = createFixture(context.rootDir, 'portable');
mutate(fixture);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason,
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
it.each(['../libmpv.so.2', '/tmp/libmpv.so.2', 'sub/libmpv.so.2'])(
'rejects unsafe runtime path %s',
(unsafeName) => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
const runtimeFiles = manifest.runtimeFiles as RuntimeFile[];
runtimeFiles[0].name = unsafeName;
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
}
);
it('uses read and execute access checks for declared artifacts', () => {
const fixture = createFixture(context.rootDir);
const probeRuntime = context.createProbe();
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
const accessMock = context.fileSystem.accessSync as jest.Mock;
expect(accessMock).toHaveBeenCalledWith(
fixture.helperPath,
fsConstants.R_OK | fsConstants.X_OK
);
expect(accessMock).toHaveBeenCalledWith(
path.join(fixture.nativeDir, 'embedded_mpv_frame_reader.node'),
fsConstants.R_OK
);
});
});
@@ -0,0 +1,238 @@
import { createHash } from 'crypto';
import * as nodeFileSystem from 'fs';
import path from 'path';
import {
FRAME_COPY_ADDON_NAME,
FRAME_COPY_HELPER_NAME,
FRAME_COPY_READER_NAME,
} from './contracts';
import {
validateDevelopmentManifest,
validatePackagedManifest,
} from './manifest-validator';
import type {
EmbeddedMpvFrameCopyManifestContract,
EmbeddedMpvFrameCopyRuntimeFileSystem,
RuntimeFile,
ValidatedPackage,
ValidationResult,
} from './types';
import {
isMissingFileError,
isValidationFailure,
readManifest,
validationFailure,
} from './validation-primitives';
function validateRegularArtifact(
filePath: string,
accessMode: number,
expectedMode: number | null,
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem
): ValidationResult<nodeFileSystem.Stats> {
let stat: nodeFileSystem.Stats;
try {
stat = fileSystem.lstatSync(filePath);
} catch (error) {
return validationFailure(
isMissingFileError(error)
? 'runtime-artifact-missing'
: 'runtime-artifact-invalid'
);
}
if (
stat.isSymbolicLink() ||
!stat.isFile() ||
(expectedMode !== null && (stat.mode & 0o777) !== expectedMode)
) {
return validationFailure('runtime-artifact-invalid');
}
try {
fileSystem.accessSync(filePath, accessMode);
} catch {
return validationFailure('runtime-artifact-invalid');
}
return { value: stat };
}
function pathExistsByLstat(
filePath: string,
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem
): boolean {
try {
fileSystem.lstatSync(filePath);
return true;
} catch (error) {
if (isMissingFileError(error)) {
return false;
}
throw error;
}
}
function validateBundledRuntimeFiles(
nativeDir: string,
runtimeFiles: RuntimeFile[],
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem
): ValidationResult<true> {
const libDir = path.join(nativeDir, 'lib');
let libStat: nodeFileSystem.Stats;
try {
libStat = fileSystem.lstatSync(libDir);
} catch {
return validationFailure('runtime-library-directory-invalid');
}
if (libStat.isSymbolicLink() || !libStat.isDirectory()) {
return validationFailure('runtime-library-directory-invalid');
}
let packagedNames: string[];
try {
packagedNames = fileSystem.readdirSync(libDir) as string[];
} catch {
return validationFailure('runtime-library-directory-invalid');
}
const declaredNames = new Set(runtimeFiles.map(({ name }) => name));
if (packagedNames.some((name) => !declaredNames.has(name))) {
return validationFailure('runtime-library-undeclared');
}
for (const runtimeFile of runtimeFiles) {
const runtimePath = path.join(libDir, runtimeFile.name);
let stat: nodeFileSystem.Stats;
try {
stat = fileSystem.lstatSync(runtimePath);
} catch (error) {
return validationFailure(
isMissingFileError(error)
? 'runtime-library-missing'
: 'runtime-library-invalid'
);
}
if (stat.isSymbolicLink() || !stat.isFile()) {
return validationFailure('runtime-library-invalid');
}
try {
fileSystem.accessSync(runtimePath, nodeFileSystem.constants.R_OK);
} catch {
return validationFailure('runtime-library-invalid');
}
if (stat.size !== runtimeFile.size) {
return validationFailure('runtime-library-size-mismatch');
}
let contents: Buffer;
try {
contents = fileSystem.readFileSync(runtimePath);
} catch {
return validationFailure('runtime-library-invalid');
}
if (contents.length !== runtimeFile.size) {
return validationFailure('runtime-library-size-mismatch');
}
const actualSha256 = createHash('sha256')
.update(contents)
.digest('hex');
if (actualSha256 !== runtimeFile.sha256) {
return validationFailure('runtime-library-hash-mismatch');
}
}
return { value: true };
}
export function validatePackage(
helperPath: string,
manifestPath: string,
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem,
manifestContract: EmbeddedMpvFrameCopyManifestContract
): ValidationResult<ValidatedPackage> {
const nativeDir = path.dirname(helperPath);
if (
path.basename(helperPath) !== FRAME_COPY_HELPER_NAME ||
path.dirname(manifestPath) !== nativeDir
) {
return validationFailure('runtime-artifact-invalid');
}
const manifestArtifact = validateRegularArtifact(
manifestPath,
nodeFileSystem.constants.R_OK,
0o644,
fileSystem
);
if (isValidationFailure(manifestArtifact)) {
return validationFailure(
manifestArtifact.reason === 'runtime-artifact-missing'
? 'runtime-manifest-missing'
: 'runtime-manifest-invalid'
);
}
const manifestResult = readManifest(manifestPath, fileSystem);
if (isValidationFailure(manifestResult)) {
return manifestResult;
}
const validManifest =
manifestContract === 'packaged'
? validatePackagedManifest(manifestResult.value)
: validateDevelopmentManifest(manifestResult.value);
if (isValidationFailure(validManifest)) {
return validManifest;
}
for (const [artifactPath, accessMode, expectedMode] of [
[
path.join(nativeDir, FRAME_COPY_ADDON_NAME),
nodeFileSystem.constants.R_OK,
null,
],
[
path.join(nativeDir, FRAME_COPY_READER_NAME),
nodeFileSystem.constants.R_OK,
0o644,
],
[
helperPath,
nodeFileSystem.constants.R_OK | nodeFileSystem.constants.X_OK,
0o755,
],
] as const) {
const artifact = validateRegularArtifact(
artifactPath,
accessMode,
expectedMode,
fileSystem
);
if (isValidationFailure(artifact)) {
return artifact;
}
}
const libDir = path.join(nativeDir, 'lib');
if (validManifest.value.runtimeMode === 'system') {
try {
if (pathExistsByLstat(libDir, fileSystem)) {
return validationFailure('runtime-library-directory-invalid');
}
} catch {
return validationFailure('runtime-library-directory-invalid');
}
} else {
const bundledRuntime = validateBundledRuntimeFiles(
nativeDir,
validManifest.value.runtimeFiles,
fileSystem
);
if (isValidationFailure(bundledRuntime)) {
return bundledRuntime;
}
}
return {
value: {
manifest: validManifest.value,
helperPath,
nativeDir,
},
};
}
@@ -0,0 +1,151 @@
import { chmodSync, lstatSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
import {
cloneManifest,
createFixture,
writeManifest,
} from './runtime-fixtures.test-helpers';
import {
createRuntimeTestContext,
type RuntimeTestContext,
} from './runtime-harness.test-helpers';
describe('embedded-mpv frame-copy runtime probe orchestration', () => {
let context: RuntimeTestContext;
beforeEach(() => {
context = createRuntimeTestContext();
});
afterEach(() => {
context.dispose();
});
it('validates a system package, sanitizes loader overrides, and caches by helper/manifest identity', () => {
const fixture = createFixture(context.rootDir);
const probeRuntime = context.createProbe();
expect(probeRuntime(fixture.helperPath)).toEqual({
usable: true,
profile: 'system',
runtimeMode: 'system',
libmpv: '2.3',
renderApi: 'egl',
});
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
expect(context.spawnRuntimeProbe).toHaveBeenCalledTimes(1);
expect(context.spawnRuntimeProbe).toHaveBeenCalledWith(
fixture.helperPath,
['--runtime-probe'],
{
encoding: 'utf8',
timeout: 3000,
killSignal: 'SIGKILL',
windowsHide: true,
env: {
PATH: '/usr/bin',
},
}
);
expect(context.fileSystem.readFileSync).toHaveBeenCalled();
});
it('invalidates a cached result when helper or manifest bytes change under identical stats', () => {
const fixture = createFixture(context.rootDir);
const fixedStats = new Map([
[fixture.helperPath, lstatSync(fixture.helperPath)],
[fixture.manifestPath, lstatSync(fixture.manifestPath)],
]);
context.fileSystem = {
...context.fileSystem,
lstatSync: jest.fn(
(filePath: string) =>
fixedStats.get(filePath) ?? lstatSync(filePath)
),
};
const probeRuntime = context.createProbe();
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
const changedHelper = readFileSync(fixture.helperPath);
changedHelper[0] ^= 0xff;
writeFileSync(fixture.helperPath, changedHelper);
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
fixture.manifest.generatedAt = '2026-07-18T00:00:00.000Z';
writeManifest(fixture.manifestPath, fixture.manifest);
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
expect(context.spawnRuntimeProbe).toHaveBeenCalledTimes(3);
});
it.each(['portable', 'flatpak'] as const)(
'validates the exact %s bundled closure and uses only its private library directory',
(profile) => {
const fixture = createFixture(context.rootDir, profile);
const probeRuntime = context.createProbe();
expect(probeRuntime(fixture.helperPath)).toEqual(
expect.objectContaining({
usable: true,
profile,
runtimeMode: 'bundled',
})
);
expect(context.spawnRuntimeProbe).toHaveBeenCalledWith(
fixture.helperPath,
['--runtime-probe'],
expect.objectContaining({
env: {
PATH: '/usr/bin',
LD_LIBRARY_PATH: path.join(fixture.nativeDir, 'lib'),
},
})
);
}
);
it('reprobes when the helper identity changes', () => {
const fixture = createFixture(context.rootDir);
const probeRuntime = context.createProbe();
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
writeFileSync(fixture.helperPath, '#!/bin/sh\n# changed\n');
chmodSync(fixture.helperPath, 0o755);
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
expect(context.spawnRuntimeProbe).toHaveBeenCalledTimes(2);
});
it('reprobes when the manifest identity changes', () => {
const fixture = createFixture(context.rootDir);
const probeRuntime = context.createProbe();
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
const manifest = cloneManifest(fixture.manifest);
manifest.generatedAt = '2026-07-17T00:01:00.000Z';
writeManifest(fixture.manifestPath, manifest);
expect(probeRuntime(fixture.helperPath).usable).toBe(true);
expect(context.spawnRuntimeProbe).toHaveBeenCalledTimes(2);
});
it.each([
['linux', 'arm64', 'unsupported-architecture'],
['linux', 'arm', 'unsupported-architecture'],
['darwin', 'x64', 'unsupported-platform'],
] as const)(
'does not probe unsupported %s/%s runtimes',
(platform, arch, reason) => {
const fixture = createFixture(context.rootDir);
expect(
context.createProbe({ platform, arch })(fixture.helperPath)
).toEqual({
usable: false,
reason,
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
}
);
});
@@ -0,0 +1,236 @@
import { spawnSync as nodeSpawnSync } from 'child_process';
import * as nodeFileSystem from 'fs';
import path from 'path';
import {
RUNTIME_MANIFEST_NAME,
RUNTIME_PROBE_PROTOCOL,
RUNTIME_PROBE_TIMEOUT_MS,
} from './contracts';
import { createLinuxFrameCopyHelperEnvironment } from './helper-environment';
import { validatePackage } from './package-validator';
import type {
EmbeddedMpvFrameCopyManifestContract,
EmbeddedMpvFrameCopyRuntimeDependencies,
EmbeddedMpvFrameCopyRuntimeFileSystem,
EmbeddedMpvFrameCopyRuntimeResult,
ValidManifest,
ValidatedPackage,
} from './types';
import {
failure,
fileIdentity,
hasExactFields,
isMissingFileError,
isObject,
isValidationFailure,
} from './validation-primitives';
function parseSuccessfulProbe(
stdout: unknown,
manifest: ValidManifest
): EmbeddedMpvFrameCopyRuntimeResult {
if (typeof stdout !== 'string' || !/^[^\r\n]+\n$/.test(stdout)) {
return failure('helper-probe-invalid-output');
}
let parsed: unknown;
try {
parsed = JSON.parse(stdout.slice(0, -1));
} catch {
return failure('helper-probe-invalid-output');
}
if (!isObject(parsed)) {
return failure('helper-probe-invalid-output');
}
if (parsed.protocol !== RUNTIME_PROBE_PROTOCOL) {
return failure('helper-probe-protocol-mismatch');
}
if (parsed.usable !== true) {
return failure(
parsed.usable === false
? 'helper-probe-unusable'
: 'helper-probe-invalid-output'
);
}
if (
!hasExactFields(parsed, [
'libmpv',
'protocol',
'renderApi',
'usable',
]) ||
typeof parsed.libmpv !== 'string' ||
parsed.libmpv.trim() === '' ||
parsed.renderApi !== 'egl'
) {
return failure('helper-probe-invalid-output');
}
return {
usable: true,
profile: manifest.profile,
runtimeMode: manifest.runtimeMode,
libmpv: parsed.libmpv,
renderApi: parsed.renderApi,
};
}
function runHelperProbe(
runtimePackage: ValidatedPackage,
dependencies: EmbeddedMpvFrameCopyRuntimeDependencies
): EmbeddedMpvFrameCopyRuntimeResult {
const probeEnvironment = createLinuxFrameCopyHelperEnvironment(
dependencies.env,
runtimePackage.nativeDir,
runtimePackage.manifest.runtimeMode
);
let result: ReturnType<typeof nodeSpawnSync>;
try {
result = dependencies.spawnSync(
runtimePackage.helperPath,
['--runtime-probe'],
{
encoding: 'utf8',
timeout: RUNTIME_PROBE_TIMEOUT_MS,
killSignal: 'SIGKILL',
windowsHide: true,
env: probeEnvironment,
}
);
} catch {
return failure('helper-probe-spawn-error');
}
if (
result.error &&
'code' in result.error &&
result.error.code === 'ETIMEDOUT'
) {
return failure('helper-probe-timeout');
}
if (result.error) {
return failure('helper-probe-spawn-error');
}
if (result.signal) {
return failure('helper-probe-signaled');
}
if (result.status !== 0) {
return failure('helper-probe-failed');
}
return parseSuccessfulProbe(result.stdout, runtimePackage.manifest);
}
/**
* Creates an isolated probe/cache. Production uses the singleton below;
* tests inject filesystem and spawn collaborators through this factory.
*/
export function createEmbeddedMpvFrameCopyRuntimeProbe(
overrides: Partial<EmbeddedMpvFrameCopyRuntimeDependencies> = {}
): (
helperPath: string,
manifestContract?: EmbeddedMpvFrameCopyManifestContract
) => EmbeddedMpvFrameCopyRuntimeResult {
const defaultFileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem = {
accessSync: (filePath, mode) =>
nodeFileSystem.accessSync(filePath, mode),
lstatSync: (filePath) => nodeFileSystem.lstatSync(filePath),
readFileSync: (filePath) => nodeFileSystem.readFileSync(filePath),
readdirSync: (filePath) => nodeFileSystem.readdirSync(filePath),
};
const dependencies: EmbeddedMpvFrameCopyRuntimeDependencies = {
platform: process.platform,
arch: process.arch,
env: process.env,
fileSystem: defaultFileSystem,
spawnSync: nodeSpawnSync,
...overrides,
};
const resultCache = new Map<string, EmbeddedMpvFrameCopyRuntimeResult>();
return (
helperPath: string,
manifestContract: EmbeddedMpvFrameCopyManifestContract = 'packaged'
): EmbeddedMpvFrameCopyRuntimeResult => {
if (dependencies.platform !== 'linux') {
return failure('unsupported-platform');
}
if (dependencies.arch !== 'x64') {
return failure('unsupported-architecture');
}
const manifestPath = path.join(
path.dirname(helperPath),
RUNTIME_MANIFEST_NAME
);
let helperStat: nodeFileSystem.Stats;
let manifestStat: nodeFileSystem.Stats;
try {
helperStat = dependencies.fileSystem.lstatSync(helperPath);
} catch {
return failure('runtime-artifact-missing');
}
try {
manifestStat = dependencies.fileSystem.lstatSync(manifestPath);
} catch (error) {
return failure(
isMissingFileError(error)
? 'runtime-manifest-missing'
: 'runtime-manifest-invalid'
);
}
let helperContents: Buffer;
let manifestContents: Buffer;
try {
helperContents = dependencies.fileSystem.readFileSync(helperPath);
} catch {
return failure('runtime-artifact-invalid');
}
try {
manifestContents =
dependencies.fileSystem.readFileSync(manifestPath);
} catch {
return failure('runtime-manifest-invalid');
}
const cacheKey = `${manifestContract}\0${fileIdentity(
helperPath,
helperStat,
helperContents
)}\0${fileIdentity(manifestPath, manifestStat, manifestContents)}`;
const cached = resultCache.get(cacheKey);
if (cached) {
return cached;
}
let result: EmbeddedMpvFrameCopyRuntimeResult;
try {
const runtimePackage = validatePackage(
helperPath,
manifestPath,
dependencies.fileSystem,
manifestContract
);
result = isValidationFailure(runtimePackage)
? failure(runtimePackage.reason)
: runHelperProbe(runtimePackage.value, dependencies);
} catch {
result = failure('runtime-probe-internal-error');
}
resultCache.set(cacheKey, result);
return result;
};
}
const processRuntimeProbe = createEmbeddedMpvFrameCopyRuntimeProbe();
/**
* Fail-closed, process-lifetime Linux runtime decision shared by startup and
* the service gate. The helper and manifest identities scope cached results.
*/
export function probeEmbeddedMpvFrameCopyRuntime(
helperPath: string,
manifestContract: EmbeddedMpvFrameCopyManifestContract
): EmbeddedMpvFrameCopyRuntimeResult {
return processRuntimeProbe(helperPath, manifestContract);
}
@@ -0,0 +1,95 @@
import { isDeepStrictEqual } from 'util';
import {
ALLOWED_EXTERNAL_LIBRARY_NAMES,
EXPECTED_EXTERNAL_SYSTEM_LIBRARIES,
SAFE_RUNTIME_NAME_PATTERN,
SHARED_LIBRARY_PATTERN,
} from './contracts';
import type { RuntimeFile } from './types';
import {
hasExactFields,
isObject,
isSafeRuntimeName,
} from './validation-primitives';
export function validateRuntimeClosure(
value: unknown,
runtimeFiles: RuntimeFile[],
libmpvSoname: string,
externalSystemLibraries: unknown
): boolean {
if (
!isDeepStrictEqual(
externalSystemLibraries,
EXPECTED_EXTERNAL_SYSTEM_LIBRARIES
) ||
!isObject(value) ||
!hasExactFields(value, ['entries', 'externalDependencies']) ||
!Array.isArray(value.entries) ||
!Array.isArray(value.externalDependencies) ||
value.externalDependencies.some(
(dependency) =>
typeof dependency !== 'string' ||
!SAFE_RUNTIME_NAME_PATTERN.test(dependency) ||
!SHARED_LIBRARY_PATTERN.test(dependency)
)
) {
return false;
}
const runtimeNames = runtimeFiles.map(({ name }) => name);
const runtimeNameSet = new Set(runtimeNames);
const closureNames: string[] = [];
const computedExternalDependencies = new Set<string>();
for (const entry of value.entries) {
if (
!isObject(entry) ||
!hasExactFields(entry, [
'name',
'needed',
'rpath',
'runpath',
'soname',
]) ||
!isSafeRuntimeName(entry.name) ||
(entry.soname !== null && !isSafeRuntimeName(entry.soname)) ||
!Array.isArray(entry.needed) ||
entry.needed.some(
(dependency) =>
typeof dependency !== 'string' ||
!SAFE_RUNTIME_NAME_PATTERN.test(dependency) ||
!SHARED_LIBRARY_PATTERN.test(dependency)
) ||
!isDeepStrictEqual(entry.rpath, []) ||
!isDeepStrictEqual(entry.runpath, ['$ORIGIN']) ||
new Set(entry.needed).size !== entry.needed.length ||
!isDeepStrictEqual([...entry.needed].sort(), entry.needed)
) {
return false;
}
closureNames.push(entry.name);
for (const dependency of entry.needed) {
if (runtimeNameSet.has(dependency)) {
continue;
}
if (!ALLOWED_EXTERNAL_LIBRARY_NAMES.has(dependency)) {
return false;
}
computedExternalDependencies.add(dependency);
}
}
const linkerAlias = value.entries.find(
(entry) => isObject(entry) && entry.name === 'libmpv.so'
);
return (
isDeepStrictEqual(closureNames, runtimeNames) &&
new Set(closureNames).size === closureNames.length &&
isDeepStrictEqual(
value.externalDependencies,
[...computedExternalDependencies].sort()
) &&
isObject(linkerAlias) &&
linkerAlias.soname === libmpvSoname
);
}
@@ -0,0 +1,296 @@
import { createHash } from 'crypto';
import { chmodSync, mkdirSync, writeFileSync } from 'fs';
import path from 'path';
import type { createEmbeddedMpvFrameCopyRuntimeProbe } from '../embedded-mpv-frame-copy-runtime';
import {
EXTERNAL_SYSTEM_LIBRARIES,
PINNED_SOURCE_PACKAGE_IDENTITIES,
type RuntimeFile,
type RuntimeFixture,
} from './runtime.spec-data';
function sha256(contents: Buffer): string {
return createHash('sha256').update(contents).digest('hex');
}
function createRuntimeFiles(
runtimeContents: Record<string, Buffer>
): RuntimeFile[] {
return Object.entries(runtimeContents)
.map(([name, contents]) => ({
name,
size: contents.length,
sha256: sha256(contents),
}))
.sort((left, right) => left.name.localeCompare(right.name));
}
function createSourceRuntime(
runtimeFiles: RuntimeFile[],
runtimeDependencyClosure: Record<string, unknown>
): Record<string, unknown> {
const packages = cloneManifest(PINNED_SOURCE_PACKAGE_IDENTITIES);
(
packages.libplacebo as typeof packages.libplacebo & {
sourceSubmodules: string[];
}
).sourceSubmodules = [`${'a'.repeat(40)} 3rdparty/example`];
return {
schemaVersion: 1,
origin: 'vendored-lgpl-source-build',
platform: 'linux',
arch: 'x64',
packages,
ffmpeg: {
...(packages.ffmpeg as Record<string, unknown>),
configureFlags: ['--disable-gpl', '--disable-nonfree'],
},
mpv: {
...(packages.mpv as Record<string, unknown>),
mesonFlags: ['-Dgpl=false', '-Dlibmpv=true'],
},
sourceDistribution:
'Publish the exact hwdata archive and pnp.ids with the libdisplay-info source.',
runtimeFiles,
runtimeTotalBytes: runtimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
),
runtimeAbi: {
baseline: {
distribution: 'Ubuntu 22.04',
glibcMaximum: '2.35',
glibcxxMaximum: '3.4.30',
},
files: runtimeFiles.map(({ name }) => ({
name,
requiredGlibc: '2.34',
requiredGlibcxx: null,
})),
},
runtimeDependencyClosure,
externalSystemLibraries: cloneManifest(EXTERNAL_SYSTEM_LIBRARIES),
};
}
export function createFixture(
rootDir: string,
profile: 'system' | 'portable' | 'flatpak' = 'system'
): RuntimeFixture {
const nativeDir = path.join(rootDir, profile, 'native');
const helperPath = path.join(nativeDir, 'iptvnator_mpv_helper');
const manifestPath = path.join(nativeDir, 'embedded-mpv-runtime.json');
mkdirSync(nativeDir, { recursive: true });
writeFileSync(path.join(nativeDir, 'embedded_mpv.node'), 'addon', {
mode: 0o644,
});
writeFileSync(
path.join(nativeDir, 'embedded_mpv_frame_reader.node'),
'reader',
{ mode: 0o644 }
);
writeFileSync(helperPath, '#!/bin/sh\n', { mode: 0o755 });
const bundled = profile !== 'system';
const runtimeContents = bundled
? {
'libmpv.so': Buffer.from('libmpv-linker-alias'),
'libmpv.so.2': Buffer.from('libmpv-soname'),
}
: {};
const runtimeFiles = createRuntimeFiles(runtimeContents);
const runtimeDependencyClosure = {
entries: runtimeFiles.map(({ name }) => ({
name,
soname: name === 'libmpv.so' ? 'libmpv.so.2' : name,
needed: [],
rpath: [],
runpath: ['$ORIGIN'],
})),
externalDependencies: [],
};
const externalSystemLibraries = cloneManifest(EXTERNAL_SYSTEM_LIBRARIES);
const sourceRuntime = createSourceRuntime(
runtimeFiles,
runtimeDependencyClosure
);
const manifest: Record<string, unknown> = {
schemaVersion: 1,
origin: bundled
? 'bundled-lgpl-frame-copy'
: 'system-libmpv-frame-copy',
generatedAt: '2026-07-17T00:00:00.000Z',
platform: 'linux',
arch: 'x64',
profile,
runtimeMode: bundled ? 'bundled' : 'system',
targets:
profile === 'system'
? ['deb', 'pacman', 'rpm']
: profile === 'portable'
? ['appimage', 'snap']
: ['flatpak'],
artifacts: {
addon: {
name: 'embedded_mpv.node',
regularFile: true,
readable: true,
},
frameReader: {
name: 'embedded_mpv_frame_reader.node',
regularFile: true,
readable: true,
},
helper: {
name: 'iptvnator_mpv_helper',
regularFile: true,
readable: true,
executable: true,
},
},
processIsolation: {
addonLoadsLibmpv: false,
readerLoadsLibmpv: false,
electronLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
},
nativeViewFallback: 'process-isolated mpv --wid',
libmpvSoname: 'libmpv.so.2',
packageDependencies: bundled
? {}
: {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
},
runtimeFiles,
runtimeTotalBytes: runtimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
),
...(bundled
? {
runtimeDependencyClosure,
externalSystemLibraries,
sourceRuntime,
}
: {}),
};
writeManifest(manifestPath, manifest);
if (bundled) {
const libDir = path.join(nativeDir, 'lib');
mkdirSync(libDir);
for (const [name, contents] of Object.entries(runtimeContents)) {
writeFileSync(path.join(libDir, name), contents, {
mode: 0o644,
});
}
}
return {
nativeDir,
helperPath,
manifestPath,
manifest,
runtimeContents,
};
}
export function createDevelopmentFixture(
rootDir: string,
buildInputMode: 'system-dev' | 'system-build-inputs' | 'bundled-runtime'
): RuntimeFixture {
const bundled = buildInputMode === 'bundled-runtime';
const fixture = createFixture(rootDir, bundled ? 'portable' : 'system');
const packagedSourceRuntime = fixture.manifest.sourceRuntime;
const sourceRuntime =
buildInputMode === 'system-dev'
? {
linuxBackend: 'process-isolated mpv --wid',
warning:
'Development-only unmanaged system libmpv toolchain.',
}
: buildInputMode === 'system-build-inputs'
? {
linuxBackend: 'process-isolated mpv --wid',
buildInputs: {
libmpvDevPackage: 'libmpv-dev',
mpvPackage: 'mpv',
},
sourceDistribution:
'Linux development inputs are supplied by the host package manager.',
}
: packagedSourceRuntime;
const manifest: Record<string, unknown> = {
schemaVersion: 1,
origin: 'linux-frame-copy-build',
generatedAt: '2026-07-17T00:00:00.000Z',
platform: 'linux',
arch: 'x64',
buildInputMode,
sourceRuntimeValidated: bundled,
allowedPackageRuntimeModes: ['system', 'bundled'],
packageRuntimeAvailability: {
system: bundled,
bundled,
},
artifacts: {
addon: 'embedded_mpv.node',
frameReader: 'embedded_mpv_frame_reader.node',
helper: 'iptvnator_mpv_helper',
},
processIsolation: {
addonLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
},
nativeViewFallback: 'process-isolated mpv --wid',
libmpvSoname: bundled ? 'libmpv.so.2' : null,
runtimeFiles: fixture.manifest.runtimeFiles,
runtimeTotalBytes: fixture.manifest.runtimeTotalBytes,
sourceRuntime,
};
fixture.manifest = manifest;
writeManifest(fixture.manifestPath, manifest);
return fixture;
}
export function probeDevelopmentRuntime(
probeRuntime: ReturnType<typeof createEmbeddedMpvFrameCopyRuntimeProbe>,
helperPath: string
) {
return (
probeRuntime as unknown as (
path: string,
contract: 'development'
) => ReturnType<typeof probeRuntime>
)(helperPath, 'development');
}
export function mirrorBundledManifestFields(
manifest: Record<string, unknown>
): void {
const sourceRuntime = manifest.sourceRuntime as Record<string, unknown>;
sourceRuntime.runtimeFiles = cloneManifest(manifest.runtimeFiles);
sourceRuntime.runtimeTotalBytes = manifest.runtimeTotalBytes;
sourceRuntime.runtimeDependencyClosure = cloneManifest(
manifest.runtimeDependencyClosure
);
sourceRuntime.externalSystemLibraries = cloneManifest(
manifest.externalSystemLibraries
);
}
export function writeManifest(
manifestPath: string,
manifest: Record<string, unknown>
): void {
writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, {
mode: 0o644,
});
chmodSync(manifestPath, 0o644);
}
export function cloneManifest<T>(manifest: T): T {
return JSON.parse(JSON.stringify(manifest)) as T;
}
@@ -0,0 +1,69 @@
import { spawnSync } from 'child_process';
import {
accessSync,
lstatSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
} from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import {
createEmbeddedMpvFrameCopyRuntimeProbe,
type EmbeddedMpvFrameCopyRuntimeDependencies,
} from '../embedded-mpv-frame-copy-runtime';
import { SUCCESS_OUTPUT } from './runtime.spec-data';
export interface RuntimeTestContext {
rootDir: string;
spawnRuntimeProbe: jest.Mock;
fileSystem: EmbeddedMpvFrameCopyRuntimeDependencies['fileSystem'];
createProbe(
overrides?: Partial<EmbeddedMpvFrameCopyRuntimeDependencies>
): ReturnType<typeof createEmbeddedMpvFrameCopyRuntimeProbe>;
dispose(): void;
}
export function createRuntimeTestContext(): RuntimeTestContext {
const rootDir = mkdtempSync(path.join(tmpdir(), 'iptvnator-fc-runtime-'));
const spawnRuntimeProbe = jest.fn(() => ({
status: 0,
signal: null,
stdout: SUCCESS_OUTPUT,
stderr: '',
}));
const fileSystem: EmbeddedMpvFrameCopyRuntimeDependencies['fileSystem'] = {
accessSync: jest.fn((filePath: string, mode: number) =>
accessSync(filePath, mode)
),
lstatSync: jest.fn((filePath: string) => lstatSync(filePath)),
readFileSync: jest.fn((filePath: string) => readFileSync(filePath)),
readdirSync: jest.fn((filePath: string) => readdirSync(filePath)),
};
const context: RuntimeTestContext = {
rootDir,
spawnRuntimeProbe,
fileSystem,
createProbe(
overrides: Partial<EmbeddedMpvFrameCopyRuntimeDependencies> = {}
) {
return createEmbeddedMpvFrameCopyRuntimeProbe({
platform: 'linux',
arch: 'x64',
env: {
PATH: '/usr/bin',
LD_LIBRARY_PATH: '/ambient/libs',
LD_PRELOAD: '/tmp/inject.so',
},
fileSystem: context.fileSystem,
spawnSync: context.spawnRuntimeProbe as typeof spawnSync,
...overrides,
});
},
dispose() {
rmSync(context.rootDir, { recursive: true, force: true });
},
};
return context;
}
@@ -0,0 +1,171 @@
export const SUCCESS_OUTPUT =
'{"protocol":1,"usable":true,"libmpv":"2.3","renderApi":"egl"}\n';
export const EXTERNAL_SYSTEM_LIBRARIES = [
{
name: 'libEGL.so.1',
interface: 'EGL',
reason: 'System graphics-driver interface used by the frame-copy helper.',
},
{
name: 'libGL.so.1',
interface: 'OpenGL',
reason: 'System OpenGL compatibility interface supplied by the graphics stack.',
},
{
name: 'libGLX.so.0',
interface: 'OpenGL',
reason: 'GLVND OpenGL dispatch interface supplied by the graphics stack.',
},
{
name: 'libOpenGL.so.0',
interface: 'OpenGL',
reason: 'GLVND OpenGL interface supplied by the graphics stack.',
},
{
name: 'libasound.so.2',
interface: 'ALSA',
reason: 'Linux system audio interface intentionally used by libmpv.',
},
{
name: 'libdrm.so.2',
interface: 'DRM',
reason: 'Kernel graphics interface used by system GBM and VA-API drivers.',
},
{
name: 'libgbm.so.1',
interface: 'GBM',
reason: 'System graphics-buffer interface used by headless EGL rendering.',
},
{
name: 'libpulse.so.0',
interface: 'PulseAudio',
reason: 'Linux desktop audio interface intentionally used by libmpv.',
},
{
name: 'libva-drm.so.2',
interface: 'VA-API DRM',
reason: 'System VA-API DRM interface used for hardware decoding.',
},
{
name: 'libva.so.2',
interface: 'VA-API',
reason: 'System video-acceleration interface used for hardware decoding.',
},
];
export const PINNED_SOURCE_PACKAGE_IDENTITIES = {
freetype: {
version: '2.13.3',
sourceUrl:
'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
sourceSha256:
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
license: 'FreeType License (FTL)',
},
fribidi: {
version: '1.0.16',
sourceUrl:
'https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz',
sourceSha256:
'1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c',
license: 'LGPL-2.1-or-later',
},
harfbuzz: {
version: '8.5.0',
sourceUrl:
'https://github.com/harfbuzz/harfbuzz/releases/download/8.5.0/harfbuzz-8.5.0.tar.xz',
sourceSha256:
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
license: 'MIT',
},
expat: {
version: '2.8.2',
sourceUrl:
'https://github.com/libexpat/libexpat/releases/download/R_2_8_2/expat-2.8.2.tar.xz',
sourceSha256:
'3ad89b8588e6644bd4e49981480d48b21289eebbcd4f0a1a4afb1c29f99b6ab4',
license: 'MIT',
},
fontconfig: {
version: '2.16.0',
sourceUrl:
'https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.16.0.tar.xz',
sourceSha256:
'6a33dc555cc9ba8b10caf7695878ef134eeb36d0af366041f639b1da9b6ed220',
license: 'MIT',
},
libass: {
version: '0.17.3',
sourceUrl:
'https://github.com/libass/libass/releases/download/0.17.3/libass-0.17.3.tar.xz',
sourceSha256:
'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
license: 'ISC',
},
openssl: {
version: '3.5.7',
sourceUrl:
'https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz',
sourceSha256:
'a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8',
license: 'Apache-2.0',
},
ffmpeg: {
version: '8.1',
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
sourceSha256:
'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
license: 'LGPL-2.1-or-later',
},
libplacebo: {
version: '7.360.1',
sourceUrl: 'https://github.com/haasn/libplacebo.git',
sourceTag: 'v7.360.1',
sourceGitCommit: 'cee9b076f2c63104ccfd497fa79c39a867293ec4',
license: 'LGPL-2.1-or-later',
},
hwdata: {
version: '0.409',
sourceUrl:
'https://github.com/vcrhonek/hwdata/archive/refs/tags/v0.409.tar.gz',
sourceSha256:
'23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd',
buildInput: {
consumer: 'libdisplay-info',
relativePath: 'pnp.ids',
purpose: 'PNP vendor lookup table compiled into libdisplay-info.',
},
license: 'GPL-2.0-or-later OR XFree86-1.0',
},
'libdisplay-info': {
version: '0.1.1',
sourceUrl:
'https://gitlab.freedesktop.org/emersion/libdisplay-info/-/releases/0.1.1/downloads/libdisplay-info-0.1.1.tar.xz',
sourceSha256:
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
license: 'MIT',
},
mpv: {
version: '0.41.0',
sourceUrl:
'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
sourceSha256:
'ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209',
license: 'LGPL-2.1-or-later with -Dgpl=false',
},
};
export interface RuntimeFile {
name: string;
size: number;
sha256: string;
}
export interface RuntimeFixture {
nativeDir: string;
helperPath: string;
manifestPath: string;
manifest: Record<string, unknown>;
runtimeContents: Record<string, Buffer>;
}
@@ -0,0 +1,137 @@
import {
cloneManifest,
createFixture,
writeManifest,
} from './runtime-fixtures.test-helpers';
import {
createRuntimeTestContext,
type RuntimeTestContext,
} from './runtime-harness.test-helpers';
describe('embedded-mpv frame-copy source runtime policy', () => {
let context: RuntimeTestContext;
beforeEach(() => {
context = createRuntimeTestContext();
});
afterEach(() => {
context.dispose();
});
it.each([
{
label: 'pinned source identity',
mutate(sourceRuntime: Record<string, unknown>) {
const packages = sourceRuntime.packages as Record<
string,
Record<string, unknown>
>;
packages.mpv.sourceSha256 = '0'.repeat(64);
},
},
{
label: 'pinned source URL',
mutate(sourceRuntime: Record<string, unknown>) {
const packages = sourceRuntime.packages as Record<
string,
Record<string, unknown>
>;
packages.freetype.sourceUrl =
'https://example.invalid/freetype.tar.xz';
},
},
{
label: 'pinned git tag',
mutate(sourceRuntime: Record<string, unknown>) {
const packages = sourceRuntime.packages as Record<
string,
Record<string, unknown>
>;
packages.libplacebo.sourceTag = 'main';
},
},
{
label: 'pinned hwdata build input',
mutate(sourceRuntime: Record<string, unknown>) {
const packages = sourceRuntime.packages as Record<
string,
Record<string, unknown>
>;
packages.hwdata.buildInput = {
consumer: 'libdisplay-info',
relativePath: '../pnp.ids',
purpose:
'PNP vendor lookup table compiled into libdisplay-info.',
};
},
},
{
label: 'git submodule record',
mutate(sourceRuntime: Record<string, unknown>) {
const packages = sourceRuntime.packages as Record<
string,
Record<string, unknown>
>;
packages.libplacebo.sourceSubmodules = [
`${'a'.repeat(40)} ../outside`,
];
},
},
{
label: 'duplicate git submodule records',
mutate(sourceRuntime: Record<string, unknown>) {
const packages = sourceRuntime.packages as Record<
string,
Record<string, unknown>
>;
const record = `${'a'.repeat(40)} 3rdparty/example`;
packages.libplacebo.sourceSubmodules = [record, record];
},
},
{
label: 'portable ABI baseline',
mutate(sourceRuntime: Record<string, unknown>) {
const runtimeAbi = sourceRuntime.runtimeAbi as {
baseline: Record<string, unknown>;
};
runtimeAbi.baseline.glibcMaximum = '9.99';
},
},
{
label: 'source-distribution obligation',
mutate(sourceRuntime: Record<string, unknown>) {
sourceRuntime.sourceDistribution = 'Sources available.';
},
},
{
label: 'FFmpeg LGPL flags',
mutate(sourceRuntime: Record<string, unknown>) {
const ffmpeg = sourceRuntime.ffmpeg as {
configureFlags: string[];
};
ffmpeg.configureFlags = ['--disable-nonfree', '--enable-gpl'];
},
},
{
label: 'mpv LGPL flags',
mutate(sourceRuntime: Record<string, unknown>) {
const mpv = sourceRuntime.mpv as {
mesonFlags: string[];
};
mpv.mesonFlags = ['-Dgpl=true', '-Dlibmpv=true'];
},
},
])('rejects an invalid $label', ({ mutate }) => {
const fixture = createFixture(context.rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
mutate(manifest.sourceRuntime as Record<string, unknown>);
writeManifest(fixture.manifestPath, manifest);
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(context.spawnRuntimeProbe).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,245 @@
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 (
candidate.sourceGitCommit === expected.sourceGitCommit &&
GIT_COMMIT_PATTERN.test(candidate.sourceGitCommit) &&
candidate.sourceSha256 === undefined &&
validatesGitSubmoduleRecords(candidate.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;
}
@@ -0,0 +1,86 @@
import type { spawnSync as nodeSpawnSync } from 'child_process';
import type * as nodeFileSystem from 'fs';
export type EmbeddedMpvFrameCopyRuntimeFailureReason =
| 'unsupported-platform'
| 'unsupported-architecture'
| 'runtime-manifest-missing'
| 'runtime-manifest-invalid'
| 'runtime-artifact-missing'
| 'runtime-artifact-invalid'
| 'runtime-library-directory-invalid'
| 'runtime-library-missing'
| 'runtime-library-undeclared'
| 'runtime-library-invalid'
| 'runtime-library-size-mismatch'
| 'runtime-library-hash-mismatch'
| 'helper-probe-timeout'
| 'helper-probe-spawn-error'
| 'helper-probe-signaled'
| 'helper-probe-failed'
| 'helper-probe-invalid-output'
| 'helper-probe-protocol-mismatch'
| 'helper-probe-unusable'
| 'runtime-probe-internal-error';
export type EmbeddedMpvFrameCopyManifestContract = 'packaged' | 'development';
export type EmbeddedMpvFrameCopyRuntimeMode = 'system' | 'bundled';
export type RuntimeProfile = 'system' | 'portable' | 'flatpak';
export type RuntimeProbeProfile = RuntimeProfile | 'development';
export type EmbeddedMpvFrameCopyRuntimeResult =
| {
usable: true;
profile: RuntimeProbeProfile;
runtimeMode: EmbeddedMpvFrameCopyRuntimeMode;
libmpv: string;
renderApi: 'egl';
}
| {
usable: false;
reason: EmbeddedMpvFrameCopyRuntimeFailureReason;
};
export interface EmbeddedMpvFrameCopyRuntimeFileSystem {
accessSync(filePath: string, mode: number): void;
lstatSync(filePath: string): nodeFileSystem.Stats;
readFileSync(filePath: string): Buffer;
readdirSync(filePath: string): string[];
}
export interface EmbeddedMpvFrameCopyRuntimeDependencies {
platform: NodeJS.Platform;
arch: string;
env: NodeJS.ProcessEnv;
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem;
spawnSync: typeof nodeSpawnSync;
}
export interface RuntimeFile {
name: string;
size: number;
sha256: string;
}
export interface ValidManifest {
profile: RuntimeProbeProfile;
runtimeMode: EmbeddedMpvFrameCopyRuntimeMode;
runtimeFiles: RuntimeFile[];
}
export interface ValidatedPackage {
manifest: ValidManifest;
helperPath: string;
nativeDir: string;
}
export interface ValidationSuccess<T> {
value: T;
}
export interface ValidationFailure {
reason: EmbeddedMpvFrameCopyRuntimeFailureReason;
}
export type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
@@ -0,0 +1,162 @@
import { createHash } from 'crypto';
import type * as nodeFileSystem from 'fs';
import path from 'path';
import { isDeepStrictEqual } from 'util';
import {
SAFE_RUNTIME_NAME_PATTERN,
SHA256_PATTERN,
SHARED_LIBRARY_PATTERN,
} from './contracts';
import type {
EmbeddedMpvFrameCopyRuntimeFailureReason,
EmbeddedMpvFrameCopyRuntimeFileSystem,
EmbeddedMpvFrameCopyRuntimeResult,
RuntimeFile,
ValidationFailure,
ValidationResult,
} from './types';
export function failure(
reason: EmbeddedMpvFrameCopyRuntimeFailureReason
): EmbeddedMpvFrameCopyRuntimeResult {
return { usable: false, reason };
}
export function validationFailure(
reason: EmbeddedMpvFrameCopyRuntimeFailureReason
): ValidationFailure {
return { reason };
}
export function isValidationFailure<T>(
result: ValidationResult<T>
): result is ValidationFailure {
return 'reason' in result;
}
export function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
export function hasExactFields(
value: Record<string, unknown>,
fields: readonly string[]
): boolean {
return isDeepStrictEqual(Object.keys(value).sort(), [...fields].sort());
}
export function isSafeRuntimeName(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value !== '.' &&
value !== '..' &&
path.basename(value) === value &&
!value.includes('/') &&
!value.includes('\\') &&
SAFE_RUNTIME_NAME_PATTERN.test(value) &&
SHARED_LIBRARY_PATTERN.test(value)
);
}
export function isMissingFileError(error: unknown): boolean {
return (
isObject(error) &&
typeof error.code === 'string' &&
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
);
}
export function fileIdentity(
filePath: string,
stat: nodeFileSystem.Stats,
contents: Buffer
): string {
return [
path.resolve(filePath),
stat.dev,
stat.ino,
stat.mode,
stat.size,
stat.mtimeMs,
stat.ctimeMs,
createHash('sha256').update(contents).digest('hex'),
].join(':');
}
export function readManifest(
manifestPath: string,
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem
): ValidationResult<Record<string, unknown>> {
let contents: Buffer;
try {
contents = fileSystem.readFileSync(manifestPath);
} catch {
return validationFailure('runtime-manifest-invalid');
}
try {
const parsed: unknown = JSON.parse(contents.toString('utf8'));
return isObject(parsed)
? { value: parsed }
: validationFailure('runtime-manifest-invalid');
} catch {
return validationFailure('runtime-manifest-invalid');
}
}
export function validateTargets(
targets: unknown,
allowedTargets: ReadonlySet<string>
): boolean {
const expectedTargets = [...allowedTargets].sort();
if (
!Array.isArray(targets) ||
targets.length !== expectedTargets.length ||
targets.some(
(target) =>
typeof target !== 'string' ||
target.trim() !== target ||
target.toLowerCase() !== target ||
!allowedTargets.has(target)
)
) {
return false;
}
return isDeepStrictEqual(targets, expectedTargets);
}
export function validateRuntimeFiles(value: unknown): RuntimeFile[] | null {
if (!Array.isArray(value) || value.length === 0) {
return null;
}
const runtimeFiles: RuntimeFile[] = [];
const names = new Set<string>();
for (const candidate of value) {
if (
!isObject(candidate) ||
!hasExactFields(candidate, ['name', 'sha256', 'size']) ||
!isSafeRuntimeName(candidate.name) ||
!Number.isSafeInteger(candidate.size) ||
(candidate.size as number) <= 0 ||
typeof candidate.sha256 !== 'string' ||
!SHA256_PATTERN.test(candidate.sha256) ||
names.has(candidate.name)
) {
return null;
}
names.add(candidate.name);
runtimeFiles.push({
name: candidate.name,
size: candidate.size as number,
sha256: candidate.sha256,
});
}
return isDeepStrictEqual(
runtimeFiles.map(({ name }) => name).sort(),
runtimeFiles.map(({ name }) => name)
)
? runtimeFiles
: null;
}
+4 -1
View File
@@ -779,7 +779,10 @@ The build workflow creates a draft GitHub release but never publishes Snap in
parallel with that draft. The separate Snap workflow runs only for a public
`release.published` event whose tag starts with `v`; before any Store upload it
requires at least one exact `.snap` asset and exactly one non-empty
`linux-frame-copy-runtime-sources.tar.xz` in that public release.
`linux-frame-copy-runtime-sources.tar.xz` in that public release. The workflow
uploads only to the Store's edge channel. Candidate/stable promotion is manual
after installed-Snap frame-copy and missing-runtime fallback smoke; GitHub
Actions never promotes automatically.
During temporary artifact tests, CI may also set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for PR and `master` push jobs where a runtime is known to exist. After the artifacts are manually validated, remove temporary conditions so ordinary development builds leave `IPTVNATOR_REQUIRE_EMBEDDED_MPV` unset or `0`. This keeps the native feature in-tree without making every non-release build depend on runtime artifacts.
+2 -1
View File
@@ -212,7 +212,8 @@
"tslib": "^2.8.1",
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "^8.46.2"
"typescript-eslint": "^8.46.2",
"yaml": "2.8.2"
},
"pnpm": {
"overrides": {
+3
View File
@@ -447,6 +447,9 @@ importers:
typescript-eslint:
specifier: ^8.46.2
version: 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
yaml:
specifier: 2.8.2
version: 2.8.2
packages:
+4 -2
View File
@@ -209,8 +209,10 @@ or hash-mismatched license files.
Snap publication is a separate `release.published` workflow for public `v*`
GitHub releases. It verifies that the public release already contains at least
one Snap and exactly one non-empty
`linux-frame-copy-runtime-sources.tar.xz` before uploading any Snap to the
Store's edge channel.
`linux-frame-copy-runtime-sources.tar.xz` before uploading anything. The
workflow uploads only to the Store's edge channel.
Candidate/stable promotion is manual after installed-Snap frame-copy and
missing-runtime fallback smoke; GitHub Actions never promotes automatically.
Windows CI stages a checksum-pinned x64 LGPL archive. The DLL basename encoded
in its import library is preserved and must be present beside
+2 -1
View File
@@ -382,7 +382,7 @@ function pathInsideDestdir(destdir, absolutePath) {
);
}
function copyDirectoryContents(sourceDirectory, destinationDirectory) {
export function copyDirectoryContents(sourceDirectory, destinationDirectory) {
if (!fs.existsSync(sourceDirectory)) {
return;
}
@@ -394,6 +394,7 @@ function copyDirectoryContents(sourceDirectory, destinationDirectory) {
{
recursive: true,
force: true,
verbatimSymlinks: true,
}
);
}
@@ -810,6 +810,42 @@ test('uses DESTDIR with standard fontconfig and OpenSSL runtime paths', () => {
);
});
test('preserves relative library symlinks copied out of DESTDIR', async (t) => {
const { copyDirectoryContents } = await import(
pathToFileURL(builderScript).href
);
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-destdir-links-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const sourceDirectory = path.join(root, 'destdir', 'lib');
const destinationDirectory = path.join(root, 'prefix', 'lib');
fs.mkdirSync(sourceDirectory, { recursive: true });
fs.writeFileSync(path.join(sourceDirectory, 'libcrypto.so.3'), 'crypto');
fs.symlinkSync(
'libcrypto.so.3',
path.join(sourceDirectory, 'libcrypto.so')
);
copyDirectoryContents(sourceDirectory, destinationDirectory);
fs.rmSync(path.join(root, 'destdir'), {
recursive: true,
force: true,
});
assert.equal(
fs.readlinkSync(path.join(destinationDirectory, 'libcrypto.so')),
'libcrypto.so.3'
);
assert.equal(
fs.readFileSync(
path.join(destinationDirectory, 'libcrypto.so'),
'utf8'
),
'crypto'
);
});
test('uses an explicit LGPL FFmpeg HTTPS and HLS protocol baseline', () => {
const required = [
'--enable-shared',
@@ -1557,4 +1593,16 @@ test('registers the builder script and focused test with package and Nx', () =>
packagingProject.targets.test.options.command,
/tools\/embedded-mpv\/build-linux-runtime\.test\.mjs/
);
const lintInputs = packagingProject.targets.lint.inputs;
for (const registeredInput of [
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.mjs',
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.test.mjs',
]) {
assert.ok(lintInputs.includes(registeredInput), registeredInput);
}
assert.match(
packagingProject.targets.lint.command,
/tools\/embedded-mpv\/build-linux-runtime\.\{mjs,test\.mjs\}/
);
});
@@ -0,0 +1,5 @@
const RUNTIME_PROBE_CONTRACT = Object.freeze({
RUNTIME_PROBE_TIMEOUT_MS: 3000,
});
module.exports = RUNTIME_PROBE_CONTRACT;
@@ -0,0 +1,5 @@
declare const RUNTIME_PROBE_CONTRACT: Readonly<{
readonly RUNTIME_PROBE_TIMEOUT_MS: 3000;
}>;
export = RUNTIME_PROBE_CONTRACT;
+8 -2
View File
@@ -39,6 +39,8 @@
"{workspaceRoot}/tools/embedded-mpv/generate-linux-runtime-notices.test.mjs",
"{workspaceRoot}/tools/embedded-mpv/linux-runtime-manifest.cjs",
"{workspaceRoot}/tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts",
"{workspaceRoot}/tools/embedded-mpv/stage-runtime.mjs",
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
],
@@ -55,9 +57,13 @@
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.cjs",
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs",
"{workspaceRoot}/tools/packaging/publish-snap-workflow.test.mjs",
"{workspaceRoot}/tools/packaging/release-snap-assets.cjs"
"{workspaceRoot}/tools/packaging/release-snap-assets.cjs",
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.mjs",
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.test.mjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts"
],
"command": "eslint \"tools/packaging/**/*.{js,cjs,mjs,ts}\" \"tools/embedded-mpv/generate-linux-runtime-notices.{cjs,test.mjs}\""
"command": "eslint \"tools/packaging/**/*.{js,cjs,mjs,ts}\" \"tools/embedded-mpv/build-linux-runtime.{mjs,test.mjs}\" \"tools/embedded-mpv/generate-linux-runtime-notices.{cjs,test.mjs}\" \"tools/embedded-mpv/runtime-probe-contract.cjs\""
}
},
"tags": ["scope:tools", "domain:packaging", "type:tool"]
+237 -6
View File
@@ -4,6 +4,11 @@ import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parse } from 'yaml';
import {
assertBuildSnapWorkflowPolicy,
assertPublishSnapWorkflowPolicy,
} from './snap-workflow-policy.test-helpers.mjs';
const workspaceRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
@@ -36,6 +41,64 @@ async function loadReleaseAssetHelper() {
return import(pathToFileURL(releaseAssetHelperPath).href);
}
function insertFirstJobStep(workflowText, stepSource) {
const stepsHeader = /^([ \t]+)steps:\r?\n/m.exec(workflowText);
assert.ok(stepsHeader, 'workflow must contain a steps list');
const insertionIndex = stepsHeader.index + stepsHeader[0].length;
const stepIndent = `${stepsHeader[1]} `;
const indentedStep = stepSource
.split('\n')
.map((line) => `${stepIndent}${line}`)
.join('\n');
return `${workflowText.slice(
0,
insertionIndex
)}${indentedStep}\n${workflowText.slice(insertionIndex)}`;
}
function insertWorkflowJob(workflowText, jobSource) {
const jobsHeader = /^([ \t]*)jobs:\r?\n/m.exec(workflowText);
assert.ok(jobsHeader, 'workflow must contain a jobs mapping');
const insertionIndex = jobsHeader.index + jobsHeader[0].length;
const jobIndent = `${jobsHeader[1]} `;
const indentedJob = jobSource
.split('\n')
.map((line) => `${jobIndent}${line}`)
.join('\n');
return `${workflowText.slice(
0,
insertionIndex
)}${indentedJob}\n${workflowText.slice(insertionIndex)}`;
}
function insertFirstJobField(workflowText, fieldSource) {
const stepsHeader = /^([ \t]+)steps:\r?\n/m.exec(workflowText);
assert.ok(stepsHeader, 'workflow must contain a steps list');
const fieldIndent = stepsHeader[1];
const indentedField = fieldSource
.split('\n')
.map((line) => `${fieldIndent}${line}`)
.join('\n');
return `${workflowText.slice(
0,
stepsHeader.index
)}${indentedField}\n${workflowText.slice(stepsHeader.index)}`;
}
function assertStepRejectedByBothPolicies(stepSource) {
for (const [workflowPath, assertPolicy] of [
[publishWorkflowPath, assertPublishSnapWorkflowPolicy],
[buildWorkflowPath, assertBuildSnapWorkflowPolicy],
]) {
const workflowText = insertFirstJobStep(
fs.readFileSync(workflowPath, 'utf8'),
stepSource
);
assert.doesNotThrow(() => parse(workflowText));
assert.throws(() => assertPolicy(workflowText));
}
}
test('publishes Snap only after a public v-tag release contains binary and source assets', () => {
assert.equal(
fs.existsSync(publishWorkflowPath),
@@ -43,10 +106,6 @@ test('publishes Snap only after a public v-tag release contains binary and sourc
'the release-published Snap workflow must exist'
);
const workflowText = fs.readFileSync(publishWorkflowPath, 'utf8');
assert.match(
workflowText,
/^on:\n {4}release:\n {8}types:\n {12}- published$/m
);
assert.match(workflowText, /^permissions:\n {4}contents: read$/m);
assert.match(
workflowText,
@@ -73,11 +132,183 @@ test('publishes Snap only after a public v-tag release contains binary and sourc
);
assert.match(workflowText, /linux-frame-copy-runtime-sources\.tar\.xz/);
assert.match(workflowText, /release-snap-assets\.cjs verify/);
assert.match(workflowText, /snapcraft upload --release=edge/);
assertPublishSnapWorkflowPolicy(workflowText);
const disabledWorkflow = workflowText.replace(
'github.event.release.draft == false }}',
'github.event.release.draft == false && false }}'
);
assert.notEqual(disabledWorkflow, workflowText);
assert.doesNotThrow(() => parse(disabledWorkflow));
assert.throws(() => assertPublishSnapWorkflowPolicy(disabledWorkflow));
const extraTrigger = workflowText.replace(
' - published',
' - published\n - edited'
);
assert.doesNotThrow(() => parse(extraTrigger));
assert.throws(() => assertPublishSnapWorkflowPolicy(extraTrigger));
assert.match(
workflowText,
/Candidate\/stable promotion is manual after installed-Snap frame-copy and missing-runtime fallback smoke/
);
const buildWorkflow = fs.readFileSync(buildWorkflowPath, 'utf8');
assert.doesNotMatch(buildWorkflow, /^ {4}publish-snap:/m);
assert.doesNotMatch(buildWorkflow, /snapcraft upload/);
assertBuildSnapWorkflowPolicy(buildWorkflow);
});
test('rejects Snap uploads that target candidate or stable channels', () => {
const workflowText = fs.readFileSync(publishWorkflowPath, 'utf8');
for (const forbiddenReleaseArgument of [
'--release=edge,candidate,stable',
'--release edge,candidate,stable',
'--release=candidate',
'--release stable',
]) {
const mixedChannelWorkflow = workflowText.replace(
'--release=edge',
forbiddenReleaseArgument
);
assert.notEqual(mixedChannelWorkflow, workflowText);
assert.doesNotThrow(() => parse(mixedChannelWorkflow));
assert.throws(() =>
assertPublishSnapWorkflowPolicy(mixedChannelWorkflow)
);
}
});
test('rejects edge upload text in non-executing shell contexts', () => {
const workflowText = fs.readFileSync(publishWorkflowPath, 'utf8');
const edgeUpload = 'snapcraft upload --release=edge "${SNAP_FILE}"';
const blockIndent = ' '.repeat(18);
for (const replacement of [
`cat <<123\n${edgeUpload}\n123`,
`cat <<\\EOF\n${edgeUpload}\nEOF`,
`printf '%s\\n' "\n${edgeUpload}\n"`,
`publish_snap() {\n${edgeUpload}\n}`,
`if false; then\n${edgeUpload}\nfi`,
`cat <<FIRST <<SECOND\nfirst\nFIRST\n${edgeUpload}\nSECOND`,
`cat <<-'EOF'\n\t${edgeUpload}\n\tEOF`,
`cat <<'EOF'\n${edgeUpload}\nEOF`,
]) {
const indentedReplacement = replacement.replaceAll(
'\n',
`\n${blockIndent}`
);
const mutatedWorkflow = workflowText.replace(
edgeUpload,
indentedReplacement
);
assert.notEqual(mutatedWorkflow, workflowText);
assert.doesNotThrow(() => parse(mutatedWorkflow));
assert.throws(() => assertPublishSnapWorkflowPolicy(mutatedWorkflow));
}
});
test('rejects extra CLI tokens across wrappers, YAML forms, quotes, and heredocs', () => {
const workflowText = fs.readFileSync(publishWorkflowPath, 'utf8');
for (const stepSource of [
'- run: command snapcraft upload --release=stable package.snap',
'- run: |\n if true; then command snapcraft \\\n upload --release=edge,candidate,stable package.snap; fi',
'- run: |\n snap\\\n craft upload --release=stable package.snap',
'- run: |\n snapcraft up\\\n load --release=stable package.snap',
'- run: |2\n snapcraft upload --release=stable package.snap',
'- run: snapcraft upload --release=stable package.snap',
'- { run: snapcraft upload --release=stable package.snap }',
'- run: echo "snapcraft upload --release=edge package.snap"',
"- run: |\n cat <<'EOF'\n snapcraft upload --release=edge package.snap\n EOF",
'- run: command snapcraft release iptvnator stable',
]) {
const mutatedWorkflow = insertFirstJobStep(workflowText, stepSource);
assert.doesNotThrow(() => parse(mutatedWorkflow));
assert.throws(() => assertPublishSnapWorkflowPolicy(mutatedWorkflow));
}
});
test('rejects continued uploads and release commands in the build workflow', () => {
const workflowText = fs.readFileSync(buildWorkflowPath, 'utf8');
for (const stepSource of [
'- run: |\n if true; then command snapcraft \\\n upload --release=edge package.snap; fi',
'- run: command snapcraft release iptvnator edge',
]) {
const mutatedWorkflow = insertFirstJobStep(workflowText, stepSource);
assert.doesNotThrow(() => parse(mutatedWorkflow));
assert.throws(() => assertBuildSnapWorkflowPolicy(mutatedWorkflow));
}
});
test('rejects encoded, indirect, and unknown actions in both workflows', () => {
for (const stepSource of [
'- uses: snapcore/action-publish@v1\n with:\n release: stable',
'- "uses": snapcore/action-publish@v1\n with:\n release: stable',
"- 'uses' : snapcore/action-publish@v1\n with:\n release: stable",
'- "\\x75ses": snapcore/action-publish@v1\n with:\n release: stable',
'- ? uses\n : snapcore/action-publish@v1\n with:\n release: stable',
'- { uses: snapcore/action-publish@v1, with: { release: stable } }',
'- uses: >-\n snapcore/action-publish@v1\n with:\n release: stable',
'- name: Alias publisher\n env:\n PUBLISHER: &publisher snapcore/action-publish@v1\n uses: *publisher\n with:\n release: stable',
'- uses: "\\x73napcore/action-publish@v1"\n with:\n release: stable',
'- uses: example/action-publish@v1\n with:\n release: stable',
]) {
assertStepRejectedByBothPolicies(stepSource);
}
});
test('rejects job-level reusable workflow publication in both workflows', () => {
const reusableJob =
'synthetic-publisher:\n uses: snapcore/action-publish/.github/workflows/publish.yml@v1\n with:\n release: stable\n secrets: inherit';
for (const [workflowPath, assertPolicy] of [
[publishWorkflowPath, assertPublishSnapWorkflowPolicy],
[buildWorkflowPath, assertBuildSnapWorkflowPolicy],
]) {
const workflowText = insertWorkflowJob(
fs.readFileSync(workflowPath, 'utf8'),
reusableJob
);
assert.doesNotThrow(() => parse(workflowText));
assert.throws(() => assertPolicy(workflowText));
}
});
test('rejects command-bearing explicit shell templates in both workflows', () => {
const maliciousShell = '"snapcraft release iptvnator stable; bash {0}"';
for (const [workflowPath, assertPolicy] of [
[publishWorkflowPath, assertPublishSnapWorkflowPolicy],
[buildWorkflowPath, assertBuildSnapWorkflowPolicy],
]) {
const workflowText = fs.readFileSync(workflowPath, 'utf8');
for (const mutatedWorkflow of [
insertFirstJobStep(
workflowText,
`- shell: ${maliciousShell}\n run: echo safe`
),
`${workflowText}\ndefaults:\n run:\n shell: ${maliciousShell}\n`,
insertFirstJobField(
workflowText,
`defaults:\n run:\n shell: ${maliciousShell}`
),
]) {
assert.doesNotThrow(() => parse(mutatedWorkflow));
assert.throws(() => assertPolicy(mutatedWorkflow));
}
}
});
test('ignores Snapcraft names in comments and allowlisted action uses', () => {
const commentStep =
'- run: |\n # snapcraft upload --release=stable package.snap\n # snapcraft release iptvnator stable';
const publishWorkflow = insertFirstJobStep(
fs.readFileSync(publishWorkflowPath, 'utf8'),
commentStep
);
const buildWorkflow = insertFirstJobStep(
fs.readFileSync(buildWorkflowPath, 'utf8'),
commentStep
);
assert.doesNotThrow(() => parse(publishWorkflow));
assert.doesNotThrow(() => parse(buildWorkflow));
assert.doesNotThrow(() => assertPublishSnapWorkflowPolicy(publishWorkflow));
assert.doesNotThrow(() => assertBuildSnapWorkflowPolicy(buildWorkflow));
});
test('selects every exact Snap and exactly one compliance source asset', async () => {
@@ -0,0 +1,253 @@
import assert from 'node:assert/strict';
import { parse } from 'yaml';
const PUBLISH_ACTION_ALLOWLIST = Object.freeze([
'actions/checkout@v4',
'samuelmeuli/action-snapcraft@v3',
]);
const BUILD_ACTION_ALLOWLIST = Object.freeze([
'actions/cache/restore@v4',
'actions/cache/save@v4',
'actions/cache@v4',
'actions/checkout@v4',
'actions/download-artifact@v4',
'actions/setup-node@v4',
'actions/upload-artifact@v4',
'pnpm/action-setup@v4',
'softprops/action-gh-release@v2',
]);
const PUBLISH_JOB_ID = 'publish-snap';
const PUBLISH_JOB_CONDITION =
"${{ startsWith(github.event.release.tag_name, 'v') && github.event.release.draft == false }}";
const PUBLISH_STEP_NAME = 'Publish all public-release snaps to edge';
const PUBLISH_STEP_CONTRACT = Object.freeze({
name: PUBLISH_STEP_NAME,
shell: 'bash',
run: [
'set -euo pipefail',
'',
'node -e \\',
` "const fs=require('node:fs'); const selected=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); for (const asset of selected.snapAssets) console.log(asset.name);" \\`,
' "${RUNNER_TEMP}/selected-snap-release-assets.json" |',
' while IFS= read -r SNAP_NAME; do',
' SNAP_FILE="${RUNNER_TEMP}/snap-release-downloads/${SNAP_NAME}"',
' echo "Publishing public release asset: ${SNAP_NAME}"',
' # Candidate/stable promotion is manual after installed-Snap frame-copy and missing-runtime fallback smoke.',
' # GitHub Actions never promotes automatically.',
' snapcraft upload --release=edge "${SNAP_FILE}"',
' done',
'',
].join('\n'),
});
function stripShellComment(line) {
let quote = null;
for (let index = 0; index < line.length; index += 1) {
const character = line[index];
if (quote === "'") {
if (character === quote) {
quote = null;
}
continue;
}
if (character === '\\') {
index += 1;
continue;
}
if (quote === '"') {
if (character === quote) {
quote = null;
}
continue;
}
if (character === "'" || character === '"') {
quote = character;
continue;
}
if (
character === '#' &&
(index === 0 || /[\s;|&()]/.test(line[index - 1]))
) {
return line.slice(0, index);
}
}
return line;
}
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function normalizeRunSource(runSource) {
return runSource
.split('\n')
.map(stripShellComment)
.join('\n')
.replace(/\\\r?\n[ \t]*/g, '');
}
function collectDefaultShell(container, containerName, explicitShells) {
if (!Object.hasOwn(container, 'defaults')) {
return;
}
assert.ok(
isRecord(container.defaults),
`${containerName} defaults must be a mapping`
);
if (!Object.hasOwn(container.defaults, 'run')) {
return;
}
assert.ok(
isRecord(container.defaults.run),
`${containerName} run defaults must be a mapping`
);
if (!Object.hasOwn(container.defaults.run, 'shell')) {
return;
}
assert.equal(
typeof container.defaults.run.shell,
'string',
`${containerName} default shell must resolve to a string`
);
explicitShells.push(container.defaults.run.shell);
}
function collectWorkflowPolicyInputs(workflowText) {
const workflow = parse(workflowText);
assert.ok(isRecord(workflow), 'workflow must be a YAML mapping');
assert.ok(isRecord(workflow.jobs), 'workflow jobs must be a YAML mapping');
const actions = [];
const explicitShells = [];
const jobActions = [];
const jobsWithoutSteps = [];
const runSources = [];
collectDefaultShell(workflow, 'workflow', explicitShells);
for (const [jobName, job] of Object.entries(workflow.jobs)) {
assert.ok(isRecord(job), `workflow job "${jobName}" must be a mapping`);
collectDefaultShell(job, `workflow job "${jobName}"`, explicitShells);
if (Object.hasOwn(job, 'uses')) {
assert.equal(
typeof job.uses,
'string',
'reusable workflow identifiers must resolve to strings'
);
jobActions.push(job.uses);
}
if (!Array.isArray(job.steps)) {
jobsWithoutSteps.push(jobName);
continue;
}
for (const step of job.steps) {
assert.ok(isRecord(step), 'workflow steps must be mappings');
if (Object.hasOwn(step, 'uses')) {
assert.equal(
typeof step.uses,
'string',
'workflow action identifiers must resolve to strings'
);
actions.push(step.uses);
}
if (Object.hasOwn(step, 'run')) {
assert.equal(
typeof step.run,
'string',
'workflow run commands must resolve to strings'
);
runSources.push(normalizeRunSource(step.run));
}
if (Object.hasOwn(step, 'shell')) {
assert.equal(
typeof step.shell,
'string',
'workflow step shells must resolve to strings'
);
explicitShells.push(step.shell);
}
}
}
return {
actions,
commandSource: runSources.join('\n'),
explicitShells,
jobActions,
jobsWithoutSteps,
workflow,
};
}
function assertWorkflowExecutionShape({
explicitShells,
jobActions,
jobsWithoutSteps,
}) {
assert.deepEqual(
jobActions,
[],
'job-level reusable workflow delegation is not allowed'
);
assert.deepEqual(
jobsWithoutSteps,
[],
'every workflow job must define a concrete steps sequence'
);
assert.deepEqual(
explicitShells.filter((shell) => shell !== 'bash'),
[],
'every explicit workflow shell must be exactly "bash"'
);
}
function literalSnapcraftTokens(commandSource) {
return commandSource.match(/\bsnapcraft\b/g) ?? [];
}
export function assertPublishSnapWorkflowPolicy(workflowText) {
const policyInputs = collectWorkflowPolicyInputs(workflowText);
const { actions, commandSource, workflow } = policyInputs;
assertWorkflowExecutionShape(policyInputs);
assert.deepEqual(
workflow.on,
{ release: { types: ['published'] } },
'the publish workflow must retain its exact release trigger'
);
assert.deepEqual(
[...actions].sort(),
[...PUBLISH_ACTION_ALLOWLIST].sort(),
'the publish workflow must use exactly the allowlisted actions'
);
const publishJob = workflow.jobs[PUBLISH_JOB_ID];
assert.ok(isRecord(publishJob), 'the canonical publish job must exist');
assert.equal(
publishJob.if,
PUBLISH_JOB_CONDITION,
'the publish job must retain its exact release condition'
);
assert.deepEqual(
publishJob.steps.filter((step) => step.name === PUBLISH_STEP_NAME),
[PUBLISH_STEP_CONTRACT],
'the publish workflow must retain the exact reviewed publication step'
);
assert.equal(
literalSnapcraftTokens(commandSource).length,
1,
'the publish workflow must contain exactly one literal Snapcraft command'
);
}
export function assertBuildSnapWorkflowPolicy(workflowText) {
const policyInputs = collectWorkflowPolicyInputs(workflowText);
const { actions, commandSource } = policyInputs;
assertWorkflowExecutionShape(policyInputs);
assert.deepEqual(
actions.filter((action) => !BUILD_ACTION_ALLOWLIST.includes(action)),
[],
'the build workflow must use only allowlisted actions'
);
assert.equal(
literalSnapcraftTokens(commandSource).length,
0,
'the build workflow must not contain a literal Snapcraft token'
);
}
@@ -16,9 +16,11 @@ const {
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
} = require('./linux-frame-copy-profile.cjs');
const {
RUNTIME_PROBE_TIMEOUT_MS,
} = require('../embedded-mpv/runtime-probe-contract.cjs');
const scriptPath = fileURLToPath(import.meta.url);
const RUNTIME_PROBE_TIMEOUT_MS = 3000;
const LIBMPV_DEPENDENCY_PATTERN = /^libmpv\.so(?:\.|$)/;
function defaultRunCommand(command, args, options = {}) {
@@ -19,6 +19,26 @@ import {
verifyLinuxFrameCopyArtifact,
} from './verify-linux-frame-copy-runtime.mjs';
const runtimeProbeContractUrl = new URL(
'../embedded-mpv/runtime-probe-contract.cjs',
import.meta.url
);
const runtimeProbeContractTypesUrl = new URL(
'../embedded-mpv/runtime-probe-contract.d.cts',
import.meta.url
);
const verifierUrl = new URL(
'./verify-linux-frame-copy-runtime.mjs',
import.meta.url
);
const backendRuntimeContractsUrl = new URL(
'../../apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/contracts.ts',
import.meta.url
);
const electronBackendProjectUrl = new URL(
'../../apps/electron-backend/project.json',
import.meta.url
);
const SYSTEM_TARGETS = ['deb', 'pacman', 'rpm'];
const SYSTEM_MANIFEST = {
schemaVersion: 1,
@@ -65,6 +85,85 @@ const SYSTEM_MANIFEST = {
runtimeTotalBytes: 0,
};
test('uses the shared frozen 3000 ms runtime probe timeout contract', async () => {
assert.equal(
fs.existsSync(runtimeProbeContractUrl),
true,
'the shared runtime probe contract must exist'
);
assert.equal(
fs.existsSync(runtimeProbeContractTypesUrl),
true,
'the shared runtime probe contract types must exist'
);
const { default: runtimeProbeContract } = await import(
runtimeProbeContractUrl.href
);
assert.equal(Object.isFrozen(runtimeProbeContract), true);
assert.equal(runtimeProbeContract.RUNTIME_PROBE_TIMEOUT_MS, 3000);
assert.match(
fs.readFileSync(runtimeProbeContractTypesUrl, 'utf8'),
/readonly RUNTIME_PROBE_TIMEOUT_MS: 3000/
);
const verifierSource = fs.readFileSync(verifierUrl, 'utf8');
assert.match(
verifierSource,
/require\(['"]\.\.\/embedded-mpv\/runtime-probe-contract\.cjs['"]\)/
);
assert.doesNotMatch(
verifierSource,
/const RUNTIME_PROBE_TIMEOUT_MS\s*=\s*3000/
);
const backendContractsSource = fs.readFileSync(
backendRuntimeContractsUrl,
'utf8'
);
assert.match(
backendContractsSource,
/import runtimeProbeContract = require\(['"][^'"]*\/runtime-probe-contract\.cjs['"]\)/
);
assert.match(
backendContractsSource,
/export const \{ RUNTIME_PROBE_TIMEOUT_MS \} = runtimeProbeContract/
);
assert.doesNotMatch(
backendContractsSource,
/RUNTIME_PROBE_TIMEOUT_MS\s*=\s*3000/
);
});
test('tracks the shared probe contract in cached backend targets and runtime lint', () => {
const backendProject = JSON.parse(
fs.readFileSync(electronBackendProjectUrl, 'utf8')
);
const contractInputs = [
'{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs',
'{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts',
];
for (const targetName of ['build', 'build-e2e', 'test', 'lint']) {
const inputs = backendProject.targets[targetName]?.inputs ?? [];
for (const contractInput of contractInputs) {
assert.ok(
inputs.includes(contractInput),
`${targetName} must track ${contractInput}`
);
}
}
assert.match(
backendProject.targets.lint.command,
/embedded-mpv-frame-copy-runtime\.ts/
);
assert.match(
backendProject.targets.lint.command,
/embedded-mpv-frame-copy-runtime\/\*\*\/\*\.ts/
);
});
function elfHeader(architecture) {
const machines = {
x64: 62,