fix(embedded-mpv): harden Linux runtime capability gate

This commit is contained in:
4gray committed 2026-07-17 22:42:37 +02:00
1 parent 547291a7e9
commit 3387f7817d
9 files changed
+1429 -122

No files matched your search

+29 -3
View File
@@ -133,8 +133,34 @@ describe('Electron app security helpers', () => {
it('keeps the renderer sandboxed when frame-copy is requested without a usable runtime', () => {
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
mockIsEmbeddedMpvFeatureEnabled.mockReturnValue(true);
expect(getMainWindowWebPreferences()?.sandbox).toBe(true);
expect(mockIsFrameCopyRuntimeUsable).toHaveBeenCalledTimes(1);
});
it.each([undefined, '0'])(
'does not probe frame-copy runtime for an inactive %s opt-in',
(explicitFrameCopy) => {
mockIsEmbeddedMpvFeatureEnabled.mockReturnValue(true);
if (explicitFrameCopy === undefined) {
delete process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY;
} else {
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY =
explicitFrameCopy;
}
expect(getMainWindowWebPreferences()?.sandbox).toBe(true);
expect(mockIsFrameCopyRuntimeUsable).not.toHaveBeenCalled();
}
);
it('probes frame-copy runtime for an explicit opt-in', () => {
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
mockIsEmbeddedMpvFeatureEnabled.mockReturnValue(true);
expect(getMainWindowWebPreferences()?.sandbox).toBe(true);
expect(mockIsFrameCopyRuntimeUsable).toHaveBeenCalledTimes(1);
});
it('keeps the renderer sandboxed when frame-copy is requested but embedded MPV is disabled', () => {
@@ -215,9 +241,9 @@ describe('Electron app security helpers', () => {
expect(mainWindow.loadFile).toHaveBeenCalledWith(
expect.stringContaining('index.html')
);
expect(
mockClearStorageData.mock.invocationCallOrder[0]
).toBeLessThan(mainWindow.loadFile.mock.invocationCallOrder[0]);
expect(mockClearStorageData.mock.invocationCallOrder[0]).toBeLessThan(
mainWindow.loadFile.mock.invocationCallOrder[0]
);
});
it('continues packaged renderer loading when Electron service worker cleanup fails', async () => {
@@ -14,9 +14,12 @@ import {
isFrameCopyPlatformSupported,
isFrameCopyRuntimeUsable,
resolveFrameCopyHelperPath,
shouldPromotePersistedFrameCopyOptIn,
} from './embedded-mpv-frame-copy-platform.util';
import * as frameCopyPlatform from './embedded-mpv-frame-copy-platform.util';
import type { EmbeddedMpvFrameCopyRuntimeResult } from './embedded-mpv-frame-copy-runtime';
import type {
EmbeddedMpvFrameCopyManifestContract,
EmbeddedMpvFrameCopyRuntimeResult,
} from './embedded-mpv-frame-copy-runtime';
describe('embedded-mpv-frame-copy-platform.util', () => {
describe('isFrameCopyPlatformSupported', () => {
@@ -161,11 +164,16 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
beforeEach(() => {
mockElectronApp.isPackaged = false;
});
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
Object.defineProperty(process, 'arch', { value: originalArch });
mockElectronApp.isPackaged = false;
});
it('requires a successful Linux x64 runtime probe', () => {
@@ -174,7 +182,7 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
const resolveHelper = jest.fn(() => '/native/iptvnator_mpv_helper');
const probeRuntime = jest.fn<
EmbeddedMpvFrameCopyRuntimeResult,
[string]
[string, EmbeddedMpvFrameCopyManifestContract]
>(() => ({
usable: true,
profile: 'system',
@@ -187,7 +195,8 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
true
);
expect(probeRuntime).toHaveBeenCalledWith(
'/native/iptvnator_mpv_helper'
'/native/iptvnator_mpv_helper',
'development'
);
probeRuntime.mockReturnValueOnce({
@@ -202,6 +211,31 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
});
});
it('selects the packaged manifest contract only from app.isPackaged', () => {
Object.defineProperty(process, 'platform', { value: 'linux' });
Object.defineProperty(process, 'arch', { value: 'x64' });
mockElectronApp.isPackaged = true;
const resolveHelper = jest.fn(() => '/native/iptvnator_mpv_helper');
const probeRuntime = jest.fn<
EmbeddedMpvFrameCopyRuntimeResult,
[string, EmbeddedMpvFrameCopyManifestContract]
>(() => ({
usable: true,
profile: 'system',
runtimeMode: 'system',
libmpv: '2.3',
renderApi: 'egl',
}));
expect(isFrameCopyRuntimeUsable(resolveHelper, probeRuntime)).toBe(
true
);
expect(probeRuntime).toHaveBeenCalledWith(
'/native/iptvnator_mpv_helper',
'packaged'
);
});
it.each<[NodeJS.Platform, string]>([
['darwin', 'arm64'],
['win32', 'x64'],
@@ -236,24 +270,50 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
expect(resolveHelper).not.toHaveBeenCalled();
expect(probeRuntime).not.toHaveBeenCalled();
});
it('reports unsupported architecture for Intel macOS', () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });
Object.defineProperty(process, 'arch', { value: 'x64' });
const resolveHelper = jest.fn(() => '/native/iptvnator_mpv_helper');
const probeRuntime = jest.fn();
expect(
getFrameCopyRuntimeAvailability(resolveHelper, probeRuntime)
).toEqual({
usable: false,
reason: 'unsupported-architecture',
});
expect(resolveHelper).not.toHaveBeenCalled();
expect(probeRuntime).not.toHaveBeenCalled();
});
});
it('promotes a stored opt-in only without an explicit env override and with a usable runtime', () => {
const shouldPromote = (
frameCopyPlatform as typeof frameCopyPlatform & {
shouldPromotePersistedFrameCopyOptIn?: (
storedEnabled: boolean,
explicitEnv: string | undefined,
runtimeUsable: boolean
) => boolean;
}
).shouldPromotePersistedFrameCopyOptIn;
it('lazily probes only a stored opt-in without an explicit env override', () => {
const runtimeUsable = jest.fn(() => true);
expect(
shouldPromotePersistedFrameCopyOptIn(
false,
undefined,
runtimeUsable
)
).toBe(false);
expect(
shouldPromotePersistedFrameCopyOptIn(true, '0', runtimeUsable)
).toBe(false);
expect(
shouldPromotePersistedFrameCopyOptIn(true, '1', runtimeUsable)
).toBe(false);
expect(runtimeUsable).not.toHaveBeenCalled();
expect(shouldPromote).toBeDefined();
expect(shouldPromote?.(true, undefined, true)).toBe(true);
expect(shouldPromote?.(true, undefined, false)).toBe(false);
expect(shouldPromote?.(true, '0', true)).toBe(false);
expect(shouldPromote?.(true, '1', true)).toBe(false);
expect(shouldPromote?.(false, undefined, true)).toBe(false);
expect(
shouldPromotePersistedFrameCopyOptIn(true, undefined, runtimeUsable)
).toBe(true);
expect(runtimeUsable).toHaveBeenCalledTimes(1);
runtimeUsable.mockReturnValue(false);
expect(
shouldPromotePersistedFrameCopyOptIn(true, undefined, runtimeUsable)
).toBe(false);
expect(runtimeUsable).toHaveBeenCalledTimes(2);
});
});
@@ -2,7 +2,10 @@ import { app } from 'electron';
import { accessSync, constants as fsConstants, statSync } from 'fs';
import path from 'path';
import { probeEmbeddedMpvFrameCopyRuntime } from './embedded-mpv-frame-copy-runtime';
import type { EmbeddedMpvFrameCopyRuntimeResult } from './embedded-mpv-frame-copy-runtime';
import type {
EmbeddedMpvFrameCopyManifestContract,
EmbeddedMpvFrameCopyRuntimeResult,
} from './embedded-mpv-frame-copy-runtime';
/**
* Platform gate + helper discovery for the embedded MPV frame-copy engine,
@@ -127,7 +130,8 @@ export function resolveFrameCopyHelperPath(): string | null {
}
type FrameCopyRuntimeProbe = (
helperPath: string
helperPath: string,
manifestContract: EmbeddedMpvFrameCopyManifestContract
) => EmbeddedMpvFrameCopyRuntimeResult;
export type FrameCopyRuntimeAvailability =
@@ -152,7 +156,7 @@ export function getFrameCopyRuntimeAvailability(
availability = {
usable: false,
reason:
process.platform === 'linux'
process.platform === 'linux' || process.platform === 'darwin'
? 'unsupported-architecture'
: 'unsupported-platform',
};
@@ -167,7 +171,13 @@ export function getFrameCopyRuntimeAvailability(
availability = { usable: true };
} else {
try {
availability = probeRuntime(helperPath);
// app.isPackaged is the trusted boundary. Environment flags
// can request the feature, but cannot weaken its manifest
// contract or make development artifacts package-trusted.
availability = probeRuntime(
helperPath,
app.isPackaged ? 'packaged' : 'development'
);
} catch {
availability = {
usable: false,
@@ -192,7 +202,7 @@ export function isFrameCopyRuntimeUsable(
export function shouldPromotePersistedFrameCopyOptIn(
storedEnabled: boolean,
explicitEnv: string | undefined,
runtimeUsable = isFrameCopyRuntimeUsable()
runtimeUsable: () => boolean = isFrameCopyRuntimeUsable
): boolean {
return explicitEnv === undefined && storedEnabled && runtimeUsable;
return explicitEnv === undefined && storedEnabled && runtimeUsable();
}
@@ -24,6 +24,161 @@ import {
const SUCCESS_OUTPUT =
'{"protocol":1,"usable":true,"libmpv":"2.3","renderApi":"egl"}\n';
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.',
},
];
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',
},
};
interface RuntimeFile {
name: string;
size: number;
@@ -54,6 +209,54 @@ function createRuntimeFiles(
.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),
};
}
function createFixture(
rootDir: string,
profile: 'system' | 'portable' | 'flatpak' = 'system'
@@ -90,7 +293,11 @@ function createFixture(
})),
externalDependencies: [],
};
const externalSystemLibraries: unknown[] = [];
const externalSystemLibraries = cloneManifest(EXTERNAL_SYSTEM_LIBRARIES);
const sourceRuntime = createSourceRuntime(
runtimeFiles,
runtimeDependencyClosure
);
const manifest: Record<string, unknown> = {
schemaVersion: 1,
origin: bundled
@@ -150,19 +357,7 @@ function createFixture(
? {
runtimeDependencyClosure,
externalSystemLibraries,
sourceRuntime: {
schemaVersion: 1,
origin: 'vendored-lgpl-source-build',
platform: 'linux',
arch: 'x64',
runtimeFiles,
runtimeTotalBytes: runtimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
),
runtimeDependencyClosure,
externalSystemLibraries,
},
sourceRuntime,
}
: {}),
};
@@ -185,6 +380,89 @@ function createFixture(
};
}
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;
}
function probeDevelopmentRuntime(
probeRuntime: ReturnType<typeof createEmbeddedMpvFrameCopyRuntimeProbe>,
helperPath: string
) {
return (
probeRuntime as unknown as (
path: string,
contract: 'development'
) => ReturnType<typeof probeRuntime>
)(helperPath, 'development');
}
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
);
}
function writeManifest(
manifestPath: string,
manifest: Record<string, unknown>
@@ -195,10 +473,8 @@ function writeManifest(
chmodSync(manifestPath, 0o644);
}
function cloneManifest(
manifest: Record<string, unknown>
): Record<string, unknown> {
return JSON.parse(JSON.stringify(manifest)) as Record<string, unknown>;
function cloneManifest<T>(manifest: T): T {
return JSON.parse(JSON.stringify(manifest)) as T;
}
describe('embedded-mpv-frame-copy-runtime', () => {
@@ -234,14 +510,18 @@ describe('embedded-mpv-frame-copy-runtime', () => {
return createEmbeddedMpvFrameCopyRuntimeProbe({
platform: 'linux',
arch: 'x64',
env: { PATH: '/usr/bin', LD_LIBRARY_PATH: '/system/libs' },
env: {
PATH: '/usr/bin',
LD_LIBRARY_PATH: '/ambient/libs',
LD_PRELOAD: '/tmp/inject.so',
},
fileSystem,
spawnSync: spawnRuntimeProbe as typeof spawnSync,
...overrides,
});
}
it('validates a system package, preserves its environment, and caches by helper/manifest identity', () => {
it('validates a system package, sanitizes loader overrides, and caches by helper/manifest identity', () => {
const fixture = createFixture(rootDir);
const probeRuntime = createProbe();
@@ -263,7 +543,6 @@ describe('embedded-mpv-frame-copy-runtime', () => {
windowsHide: true,
env: {
PATH: '/usr/bin',
LD_LIBRARY_PATH: '/system/libs',
},
}
);
@@ -271,7 +550,7 @@ describe('embedded-mpv-frame-copy-runtime', () => {
});
it.each(['portable', 'flatpak'] as const)(
'validates the exact %s bundled closure and prepends only its private library directory',
'validates the exact %s bundled closure and uses only its private library directory',
(profile) => {
const fixture = createFixture(rootDir, profile);
const probeRuntime = createProbe();
@@ -289,16 +568,126 @@ describe('embedded-mpv-frame-copy-runtime', () => {
expect.objectContaining({
env: {
PATH: '/usr/bin',
LD_LIBRARY_PATH: `${path.join(
fixture.nativeDir,
'lib'
)}:/system/libs`,
LD_LIBRARY_PATH: path.join(fixture.nativeDir, 'lib'),
},
})
);
}
);
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(rootDir, buildInputMode);
expect(
probeDevelopmentRuntime(createProbe(), fixture.helperPath)
).toEqual(
expect.objectContaining({
usable: true,
runtimeMode:
buildInputMode === 'bundled-runtime'
? 'bundled'
: 'system',
})
);
expect(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(rootDir, 'bundled-runtime');
expect(createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(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(rootDir, 'system-dev');
const manifest = cloneManifest(fixture.manifest);
mutate(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(
probeDevelopmentRuntime(createProbe(), fixture.helperPath)
).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
}
);
it('rejects a system development manifest with a private runtime directory', () => {
const fixture = createDevelopmentFixture(rootDir, 'system-dev');
mkdirSync(path.join(fixture.nativeDir, 'lib'));
expect(
probeDevelopmentRuntime(createProbe(), fixture.helperPath)
).toEqual({
usable: false,
reason: 'runtime-library-directory-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('reprobes when the helper identity changes', () => {
const fixture = createFixture(rootDir);
const probeRuntime = createProbe();
@@ -487,6 +876,211 @@ describe('embedded-mpv-frame-copy-runtime', () => {
expect(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(rootDir, profile);
const manifest = cloneManifest(fixture.manifest);
manifest.targets = targets;
writeManifest(fixture.manifestPath, manifest);
expect(createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
}
);
it('rejects a bundled closure dependency outside the deterministic system allowlist', () => {
const fixture = createFixture(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(createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('requires externalDependencies to exactly equal the sorted external closure', () => {
const fixture = createFixture(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(createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('accepts only the exact deterministic external-system library declaration', () => {
const fixture = createFixture(rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
(manifest.externalSystemLibraries as unknown[]).pop();
mirrorBundledManifestFields(manifest);
writeManifest(fixture.manifestPath, manifest);
expect(createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('accepts bundled dependencies on declared system interfaces and the glibc toolchain', () => {
const fixture = createFixture(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(createProbe()(fixture.helperPath)).toEqual(
expect.objectContaining({ usable: true, runtimeMode: 'bundled' })
);
expect(spawnRuntimeProbe).toHaveBeenCalledTimes(1);
});
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(rootDir, 'portable');
const manifest = cloneManifest(fixture.manifest);
mutate(manifest.sourceRuntime as Record<string, unknown>);
writeManifest(fixture.manifestPath, manifest);
expect(createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'runtime-manifest-invalid',
});
expect(spawnRuntimeProbe).not.toHaveBeenCalled();
});
it('rejects system manifests with a private library directory', () => {
const fixture = createFixture(rootDir);
mkdirSync(path.join(fixture.nativeDir, 'lib'));
@@ -10,11 +10,191 @@ const FRAME_COPY_READER_NAME = 'embedded_mpv_frame_reader.node';
const FRAME_COPY_HELPER_NAME = 'iptvnator_mpv_helper';
const RUNTIME_PROBE_PROTOCOL = 1;
const RUNTIME_PROBE_TIMEOUT_MS = 3000;
const LINUX_LIBRARY_PATH_DELIMITER = ':';
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
const SAFE_RUNTIME_NAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
const SUBMODULE_RECORD_PATTERN =
/^[a-f0-9]{40,64}\s+([A-Za-z0-9_+./-]+)(?:\s+\(.+\))?$/;
const VERSION_PATTERN = /^\d+(?:\.\d+)+$/;
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;
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;
const ALLOWED_EXTERNAL_LIBRARY_NAMES = new Set<string>([
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXPECTED_EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
]);
const PORTABLE_ABI_BASELINE = {
distribution: 'Ubuntu 22.04',
glibcMaximum: '2.35',
glibcxxMaximum: '3.4.30',
} as const;
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;
const EXPECTED_ARTIFACTS = {
addon: {
@@ -43,6 +223,37 @@ const EXPECTED_PROCESS_ISOLATION = {
helperRunpath: ['$ORIGIN/lib'],
};
const EXPECTED_DEVELOPMENT_ARTIFACTS = {
addon: FRAME_COPY_ADDON_NAME,
frameReader: FRAME_COPY_READER_NAME,
helper: FRAME_COPY_HELPER_NAME,
};
const EXPECTED_DEVELOPMENT_PROCESS_ISOLATION = {
addonLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
};
const DEVELOPMENT_MANIFEST_FIELDS = [
'allowedPackageRuntimeModes',
'arch',
'artifacts',
'buildInputMode',
'generatedAt',
'libmpvSoname',
'nativeViewFallback',
'origin',
'packageRuntimeAvailability',
'platform',
'processIsolation',
'runtimeFiles',
'runtimeTotalBytes',
'schemaVersion',
'sourceRuntime',
'sourceRuntimeValidated',
] as const;
const SYSTEM_PACKAGE_DEPENDENCIES = {
deb: 'libmpv2',
rpm: 'mpv-libs',
@@ -114,10 +325,15 @@ export type EmbeddedMpvFrameCopyRuntimeFailureReason =
| 'helper-probe-unusable'
| 'runtime-probe-internal-error';
export type EmbeddedMpvFrameCopyManifestContract = 'packaged' | 'development';
type RuntimeProfile = keyof typeof PROFILE_CONTRACTS;
type RuntimeProbeProfile = RuntimeProfile | 'development';
export type EmbeddedMpvFrameCopyRuntimeResult =
| {
usable: true;
profile: keyof typeof PROFILE_CONTRACTS;
profile: RuntimeProbeProfile;
runtimeMode: 'system' | 'bundled';
libmpv: string;
renderApi: 'egl';
@@ -142,8 +358,6 @@ export interface EmbeddedMpvFrameCopyRuntimeDependencies {
spawnSync: typeof nodeSpawnSync;
}
type RuntimeProfile = keyof typeof PROFILE_CONTRACTS;
interface RuntimeFile {
name: string;
size: number;
@@ -151,7 +365,7 @@ interface RuntimeFile {
}
interface ValidManifest {
profile: RuntimeProfile;
profile: RuntimeProbeProfile;
runtimeMode: 'system' | 'bundled';
runtimeFiles: RuntimeFile[];
}
@@ -260,9 +474,10 @@ function validateTargets(
targets: unknown,
allowedTargets: ReadonlySet<string>
): boolean {
const expectedTargets = [...allowedTargets].sort();
if (
!Array.isArray(targets) ||
targets.length === 0 ||
targets.length !== expectedTargets.length ||
targets.some(
(target) =>
typeof target !== 'string' ||
@@ -273,10 +488,7 @@ function validateTargets(
) {
return false;
}
return (
new Set(targets).size === targets.length &&
isDeepStrictEqual([...targets].sort(), targets)
);
return isDeepStrictEqual(targets, expectedTargets);
}
function validateRuntimeFiles(value: unknown): RuntimeFile[] | null {
@@ -317,9 +529,14 @@ function validateRuntimeFiles(value: unknown): RuntimeFile[] | null {
function validateRuntimeClosure(
value: unknown,
runtimeFiles: RuntimeFile[],
libmpvSoname: string
libmpvSoname: string,
externalSystemLibraries: unknown
): boolean {
if (
!isDeepStrictEqual(
externalSystemLibraries,
EXPECTED_EXTERNAL_SYSTEM_LIBRARIES
) ||
!isObject(value) ||
!hasExactFields(value, ['entries', 'externalDependencies']) ||
!Array.isArray(value.entries) ||
@@ -327,14 +544,17 @@ function validateRuntimeClosure(
value.externalDependencies.some(
(dependency) =>
typeof dependency !== 'string' ||
!SAFE_RUNTIME_NAME_PATTERN.test(dependency)
!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) ||
@@ -351,28 +571,274 @@ function validateRuntimeClosure(
entry.needed.some(
(dependency) =>
typeof dependency !== 'string' ||
!SAFE_RUNTIME_NAME_PATTERN.test(dependency)
!SAFE_RUNTIME_NAME_PATTERN.test(dependency) ||
!SHARED_LIBRARY_PATTERN.test(dependency)
) ||
!isDeepStrictEqual(entry.rpath, []) ||
!isDeepStrictEqual(entry.runpath, ['$ORIGIN'])
!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].sort(), [...runtimeNames].sort()) &&
isDeepStrictEqual(closureNames, runtimeNames) &&
new Set(closureNames).size === closureNames.length &&
isDeepStrictEqual(
value.externalDependencies,
[...computedExternalDependencies].sort()
) &&
isObject(linkerAlias) &&
linkerAlias.soname === libmpvSoname
);
}
function validateManifest(
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.
*/
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;
}
function validatePackagedManifest(
manifest: Record<string, unknown>
): ValidationResult<ValidManifest> {
if (
@@ -449,28 +915,13 @@ function validateManifest(
!validateRuntimeClosure(
manifest.runtimeDependencyClosure,
runtimeFiles,
manifest.libmpvSoname
manifest.libmpvSoname,
manifest.externalSystemLibraries
) ||
!Array.isArray(manifest.externalSystemLibraries) ||
!isObject(manifest.sourceRuntime)
) {
return validationFailure('runtime-manifest-invalid');
}
const sourceRuntime = manifest.sourceRuntime;
if (
sourceRuntime.schemaVersion !== 1 ||
sourceRuntime.origin !== 'vendored-lgpl-source-build' ||
sourceRuntime.platform !== 'linux' ||
sourceRuntime.arch !== 'x64' ||
sourceRuntime.runtimeTotalBytes !== manifest.runtimeTotalBytes ||
!isDeepStrictEqual(sourceRuntime.runtimeFiles, manifest.runtimeFiles) ||
!isDeepStrictEqual(
sourceRuntime.runtimeDependencyClosure,
manifest.runtimeDependencyClosure
) ||
!isDeepStrictEqual(
sourceRuntime.externalSystemLibraries,
!validateSourceRuntimePolicy(
manifest.sourceRuntime,
runtimeFiles,
manifest.runtimeDependencyClosure,
manifest.externalSystemLibraries
)
) {
@@ -486,6 +937,144 @@ function validateManifest(
};
}
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;
});
}
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,
},
};
}
function validateRegularArtifact(
filePath: string,
accessMode: number,
@@ -606,7 +1195,8 @@ function validateBundledRuntimeFiles(
function validatePackage(
helperPath: string,
manifestPath: string,
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem,
manifestContract: EmbeddedMpvFrameCopyManifestContract
): ValidationResult<ValidatedPackage> {
const nativeDir = path.dirname(helperPath);
if (
@@ -633,7 +1223,10 @@ function validatePackage(
if (isValidationFailure(manifestResult)) {
return manifestResult;
}
const validManifest = validateManifest(manifestResult.value);
const validManifest =
manifestContract === 'packaged'
? validatePackagedManifest(manifestResult.value)
: validateDevelopmentManifest(manifestResult.value);
if (isValidationFailure(validManifest)) {
return validManifest;
}
@@ -749,13 +1342,13 @@ function runHelperProbe(
dependencies: EmbeddedMpvFrameCopyRuntimeDependencies
): EmbeddedMpvFrameCopyRuntimeResult {
const probeEnvironment = { ...dependencies.env };
// The manifest/OS package is the loader contract. Ambient overrides could
// replace libmpv or inject code into the helper before its bounded probe.
delete probeEnvironment.LD_LIBRARY_PATH;
delete probeEnvironment.LD_PRELOAD;
if (runtimePackage.manifest.runtimeMode === 'bundled') {
const bundledLibDir = path.join(runtimePackage.nativeDir, 'lib');
probeEnvironment.LD_LIBRARY_PATH =
bundledLibDir +
(probeEnvironment.LD_LIBRARY_PATH
? `${LINUX_LIBRARY_PATH_DELIMITER}${probeEnvironment.LD_LIBRARY_PATH}`
: '');
probeEnvironment.LD_LIBRARY_PATH = bundledLibDir;
}
let result: ReturnType<typeof nodeSpawnSync>;
@@ -799,7 +1392,10 @@ function runHelperProbe(
*/
export function createEmbeddedMpvFrameCopyRuntimeProbe(
overrides: Partial<EmbeddedMpvFrameCopyRuntimeDependencies> = {}
): (helperPath: string) => EmbeddedMpvFrameCopyRuntimeResult {
): (
helperPath: string,
manifestContract?: EmbeddedMpvFrameCopyManifestContract
) => EmbeddedMpvFrameCopyRuntimeResult {
const defaultFileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem = {
accessSync: (filePath, mode) =>
nodeFileSystem.accessSync(filePath, mode),
@@ -817,7 +1413,10 @@ export function createEmbeddedMpvFrameCopyRuntimeProbe(
};
const resultCache = new Map<string, EmbeddedMpvFrameCopyRuntimeResult>();
return (helperPath: string): EmbeddedMpvFrameCopyRuntimeResult => {
return (
helperPath: string,
manifestContract: EmbeddedMpvFrameCopyManifestContract = 'packaged'
): EmbeddedMpvFrameCopyRuntimeResult => {
if (dependencies.platform !== 'linux') {
return failure('unsupported-platform');
}
@@ -846,7 +1445,7 @@ export function createEmbeddedMpvFrameCopyRuntimeProbe(
);
}
const cacheKey = `${fileIdentity(
const cacheKey = `${manifestContract}\0${fileIdentity(
helperPath,
helperStat
)}\0${fileIdentity(manifestPath, manifestStat)}`;
@@ -860,7 +1459,8 @@ export function createEmbeddedMpvFrameCopyRuntimeProbe(
const runtimePackage = validatePackage(
helperPath,
manifestPath,
dependencies.fileSystem
dependencies.fileSystem,
manifestContract
);
result = isValidationFailure(runtimePackage)
? failure(runtimePackage.reason)
@@ -880,7 +1480,8 @@ const processRuntimeProbe = createEmbeddedMpvFrameCopyRuntimeProbe();
* the service gate. The helper and manifest identities scope cached results.
*/
export function probeEmbeddedMpvFrameCopyRuntime(
helperPath: string
helperPath: string,
manifestContract: EmbeddedMpvFrameCopyManifestContract
): EmbeddedMpvFrameCopyRuntimeResult {
return processRuntimeProbe(helperPath);
return processRuntimeProbe(helperPath, manifestContract);
}
@@ -703,17 +703,35 @@ describe('Embedded MPV native source recording invariants', () => {
);
});
it('requires Linux embedded MPV build inputs and validates process isolation in CI', () => {
it('requires the pinned Linux source runtime artifact and validates process isolation in CI', () => {
expect(buildAndMakeWorkflowSource).toContain(
'Build and stage pinned LGPL Linux runtime'
);
expect(buildAndMakeWorkflowSource).toContain(
'node tools/embedded-mpv/build-linux-runtime.mjs "${RUNTIME_PREFIX}"'
);
expect(buildAndMakeWorkflowSource).toContain(
'node tools/embedded-mpv/stage-runtime.mjs linux x64 "${RUNTIME_PREFIX}"'
);
expect(buildAndMakeWorkflowSource).toContain(
'name: linux-embedded-mpv-runtime'
);
expect(buildAndMakeWorkflowSource).toContain(
'path: vendor/embedded-mpv/linux-x64'
);
expect(buildAndMakeWorkflowSource).toContain(
'Download pinned Linux Embedded MPV runtime'
);
expect(buildAndMakeWorkflowSource).not.toContain(
'libmpv-dev mpv pkg-config libegl-dev libgl-dev libopengl-dev libgbm-dev'
);
expect(buildAndMakeWorkflowSource).toContain(
expect(buildAndMakeWorkflowSource).not.toContain(
'Stage Linux embedded MPV build inputs'
);
expect(buildAndMakeWorkflowSource).toContain(
"linuxBackend: 'process-isolated mpv --wid'"
);
expect(buildAndMakeWorkflowSource).toContain("matrix.os == 'linux'");
expect(buildAndMakeWorkflowSource).toContain(
"manifest.origin !== 'linux-frame-copy-build' || manifest.sourceRuntimeValidated !== true"
);
expect(buildAndMakeWorkflowSource).toContain(
'Linux embedded MPV addon must not link directly to libmpv'
);
@@ -721,7 +739,7 @@ describe('Embedded MPV native source recording invariants', () => {
'test -f dist/apps/electron-backend/native/iptvnator_mpv_helper'
);
expect(buildAndMakeWorkflowSource).toContain(
'Linux frame-copy helper must link libmpv'
'Linux frame-copy helper must need libmpv.so.2'
);
});
@@ -113,8 +113,9 @@ export class EmbeddedMpvNativeService {
/**
* Frame-copy engine: helper process + shm ring + renderer canvas.
* Experimental, macOS Apple Silicon (owner decision 2026-07-10), Linux
* and Windows, opted into with IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1
* on top of the regular embedded MPV experiment flag.
* x64 and Windows, opted into with
* IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 on top of the regular
* embedded MPV experiment flag.
*/
private isFrameCopyEngineRequested(): boolean {
return ['1', 'true', 'yes', 'on'].includes(
@@ -9,10 +9,10 @@ export const VLC_PLAYER_ARGUMENTS = 'VLC_PLAYER_ARGUMENTS';
export const MPV_REUSE_INSTANCE = 'MPV_REUSE_INSTANCE';
export const VLC_REUSE_INSTANCE = 'VLC_REUSE_INSTANCE';
/**
* Embedded MPV frame-copy engine opt-in (macOS arm64, Linux). Lives in the main
* process config file because it must be readable synchronously before the
* BrowserWindow is created — the engine relaxes the window sandbox for its
* preload frame pump, which cannot change after window creation.
* Embedded MPV frame-copy engine opt-in (macOS arm64, Linux x64). Lives in the
* main process config file because it must be readable synchronously before
* the BrowserWindow is created — the engine relaxes the window sandbox for
* its preload frame pump, which cannot change after window creation.
*/
export const EMBEDDED_MPV_FRAME_COPY = 'EMBEDDED_MPV_FRAME_COPY';
+2 -5
View File
@@ -36,10 +36,7 @@ import {
shouldPromotePersistedFrameCopyOptIn,
} from './app/services/embedded-mpv-frame-copy-platform.util';
import { isEmbeddedMpvFeatureEnabled } from './app/services/embedded-mpv-runtime-policy.util';
import {
EMBEDDED_MPV_FRAME_COPY,
store,
} from './app/services/store.service';
import { EMBEDDED_MPV_FRAME_COPY, store } from './app/services/store.service';
app.setName('iptvnator');
@@ -74,7 +71,7 @@ if (
shouldPromotePersistedFrameCopyOptIn(
store.get(EMBEDDED_MPV_FRAME_COPY, false),
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY,
isFrameCopyRuntimeUsable()
isFrameCopyRuntimeUsable
)
) {
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';