mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 09:26:14 -08:00
* fix(packaging): restore Snap desktop runtime * docs(packaging): publish Snap launch repair note * fix(packaging): declare Node 22.12 floor * docs(architecture): update SQLite pin rationale * fix(tooling): align Node engine floor * fix(tooling): constrain supported Node releases * docs(architecture): correct node-abi consumer
2178 lines
74 KiB
JavaScript
2178 lines
74 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
createRuntimeProbeEnvironment,
|
|
detectLinuxArtifactFormat,
|
|
extractLinuxArtifact,
|
|
findAppImageSquashfsOffsets,
|
|
findExtractedResourceDir,
|
|
parseVerifierArguments,
|
|
readLinuxArtifactMetadata,
|
|
readElfArchitecture,
|
|
runBoundedRuntimeProbe,
|
|
runVerifierCommand,
|
|
validateSystemPackageDependencies,
|
|
validateExtractedSnapMetadata,
|
|
verifyExtractedLinuxFrameCopyRuntime,
|
|
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 backendRuntimeProbeUrl = new URL(
|
|
'../../apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe.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,
|
|
origin: 'system-libmpv-frame-copy',
|
|
generatedAt: '2026-07-17T00:00:00.000Z',
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
profile: 'system',
|
|
runtimeMode: 'system',
|
|
targets: SYSTEM_TARGETS,
|
|
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: {
|
|
deb: ['libmpv2', 'libegl1', 'libgl1', 'libgbm1'],
|
|
rpm: ['mpv-libs', 'libglvnd-egl', 'libglvnd-glx', 'mesa-libgbm'],
|
|
pacman: ['mpv', 'libglvnd', 'mesa'],
|
|
},
|
|
runtimeFiles: [],
|
|
runtimeTotalBytes: 0,
|
|
};
|
|
const DEB_SYSTEM_PACKAGE_DEPENDENCIES = [
|
|
'libmpv2',
|
|
'libegl1',
|
|
'libgl1',
|
|
'libgbm1',
|
|
];
|
|
const SNAP_DESKTOP_RUNTIME_SCRIPTS = [
|
|
'desktop-init.sh',
|
|
'desktop-common.sh',
|
|
'desktop-gnome-specific.sh',
|
|
];
|
|
|
|
function writeSnapDesktopRuntimeScripts(root) {
|
|
for (const script of SNAP_DESKTOP_RUNTIME_SCRIPTS) {
|
|
const scriptPath = path.join(root, script);
|
|
fs.writeFileSync(scriptPath, '#!/bin/sh\n');
|
|
fs.chmodSync(scriptPath, 0o755);
|
|
}
|
|
}
|
|
|
|
test('uses the shared frozen runtime probe resource 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.equal(
|
|
runtimeProbeContract.RUNTIME_PROBE_MAX_BUFFER_BYTES,
|
|
16 * 1024 * 1024
|
|
);
|
|
// The packaging check may wait far longer than the app's blocking startup
|
|
// decision, and repeats only the outcome that says nothing about the
|
|
// payload.
|
|
assert.equal(
|
|
runtimeProbeContract.PACKAGE_VERIFICATION_PROBE_TIMEOUT_MS,
|
|
15000
|
|
);
|
|
assert.equal(
|
|
runtimeProbeContract.PACKAGE_VERIFICATION_PROBE_MAX_ATTEMPTS,
|
|
2
|
|
);
|
|
const runtimeProbeContractTypes = fs.readFileSync(
|
|
runtimeProbeContractTypesUrl,
|
|
'utf8'
|
|
);
|
|
assert.match(
|
|
runtimeProbeContractTypes,
|
|
/readonly RUNTIME_PROBE_TIMEOUT_MS: 3000/
|
|
);
|
|
assert.match(
|
|
runtimeProbeContractTypes,
|
|
/readonly RUNTIME_PROBE_MAX_BUFFER_BYTES: 16777216/
|
|
);
|
|
assert.match(
|
|
runtimeProbeContractTypes,
|
|
/readonly PACKAGE_VERIFICATION_PROBE_TIMEOUT_MS: 15000/
|
|
);
|
|
assert.match(
|
|
runtimeProbeContractTypes,
|
|
/readonly PACKAGE_VERIFICATION_PROBE_MAX_ATTEMPTS: 2/
|
|
);
|
|
|
|
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/
|
|
);
|
|
assert.match(
|
|
verifierSource,
|
|
/timeout:\s*PACKAGE_VERIFICATION_PROBE_TIMEOUT_MS/
|
|
);
|
|
assert.doesNotMatch(
|
|
verifierSource,
|
|
/PACKAGE_VERIFICATION_PROBE_TIMEOUT_MS\s*=\s*15000/
|
|
);
|
|
|
|
const backendContractsSource = fs.readFileSync(
|
|
backendRuntimeContractsUrl,
|
|
'utf8'
|
|
);
|
|
assert.match(
|
|
backendContractsSource,
|
|
/import runtimeProbeContract = require\(['"][^'"]*\/runtime-probe-contract\.cjs['"]\)/
|
|
);
|
|
assert.match(
|
|
backendContractsSource,
|
|
/export const \{\s*RUNTIME_PROBE_MAX_BUFFER_BYTES,\s*RUNTIME_PROBE_TIMEOUT_MS,?\s*\}\s*=\s*runtimeProbeContract/
|
|
);
|
|
assert.doesNotMatch(
|
|
backendContractsSource,
|
|
/RUNTIME_PROBE_TIMEOUT_MS\s*=\s*3000/
|
|
);
|
|
for (const sourceUrl of [verifierUrl, backendRuntimeProbeUrl]) {
|
|
assert.match(
|
|
fs.readFileSync(sourceUrl, 'utf8'),
|
|
/maxBuffer:\s*RUNTIME_PROBE_MAX_BUFFER_BYTES/
|
|
);
|
|
}
|
|
});
|
|
|
|
test('preserves the child-process default unless an explicit capture bound is requested', () => {
|
|
const outputScript = 'process.stdout.write("x".repeat(2 * 1024 * 1024))';
|
|
const defaultCapture = runVerifierCommand(process.execPath, [
|
|
'-e',
|
|
outputScript,
|
|
]);
|
|
assert.equal(defaultCapture.error?.code, 'ENOBUFS');
|
|
|
|
const explicitCapture = runVerifierCommand(
|
|
process.execPath,
|
|
['-e', outputScript],
|
|
{ maxBuffer: 3 * 1024 * 1024 }
|
|
);
|
|
assert.equal(explicitCapture.error, undefined);
|
|
assert.equal(explicitCapture.status, 0);
|
|
assert.equal(explicitCapture.stdout.length, 2 * 1024 * 1024);
|
|
});
|
|
|
|
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,
|
|
arm64: 183,
|
|
armv7l: 40,
|
|
};
|
|
const image = Buffer.alloc(64);
|
|
image.set([0x7f, 0x45, 0x4c, 0x46, 2, 1], 0);
|
|
image.writeUInt16LE(machines[architecture], 18);
|
|
return image;
|
|
}
|
|
|
|
function createSystemPayload({
|
|
architecture = 'x64',
|
|
electronBinaryName = 'iptvnator.bin',
|
|
} = {}) {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-layout-')
|
|
);
|
|
const appDir = path.join(root, 'opt', 'IPTVnator');
|
|
const resourceDir = path.join(appDir, 'resources');
|
|
const nativeDir = path.join(
|
|
resourceDir,
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'native'
|
|
);
|
|
fs.mkdirSync(nativeDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(appDir, electronBinaryName),
|
|
elfHeader(architecture)
|
|
);
|
|
|
|
if (architecture === 'x64') {
|
|
fs.writeFileSync(path.join(nativeDir, 'embedded_mpv.node'), 'addon', {
|
|
mode: 0o644,
|
|
});
|
|
fs.writeFileSync(
|
|
path.join(nativeDir, 'embedded_mpv_frame_reader.node'),
|
|
'reader',
|
|
{ mode: 0o644 }
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(nativeDir, 'iptvnator_mpv_helper'),
|
|
'helper',
|
|
{ mode: 0o755 }
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(nativeDir, 'embedded-mpv-runtime.json'),
|
|
`${JSON.stringify(SYSTEM_MANIFEST, null, 2)}\n`,
|
|
{ mode: 0o644 }
|
|
);
|
|
} else {
|
|
fs.writeFileSync(
|
|
path.join(nativeDir, 'embedded-mpv-unavailable.txt'),
|
|
`Unavailable for ${architecture}\n`
|
|
);
|
|
}
|
|
|
|
return { root, appDir, resourceDir, nativeDir };
|
|
}
|
|
|
|
function validElfInspector(binaryPath) {
|
|
const name = path.basename(binaryPath);
|
|
if (name === 'iptvnator_mpv_helper') {
|
|
return {
|
|
soname: null,
|
|
needed: ['libc.so.6', 'libmpv.so.2'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN/lib'],
|
|
};
|
|
}
|
|
return {
|
|
soname: null,
|
|
needed: ['libc.so.6'],
|
|
rpath: [],
|
|
runpath: [],
|
|
};
|
|
}
|
|
|
|
function successfulProbeRunner() {
|
|
return {
|
|
status: 0,
|
|
signal: null,
|
|
stdout: '{"protocol":1,"usable":true,"libmpv":"0.41.0","renderApi":"egl"}\n',
|
|
stderr: '',
|
|
};
|
|
}
|
|
|
|
test('detects every supported Linux package payload format', () => {
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.AppImage'), 'appimage');
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.deb'), 'deb');
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.rpm'), 'rpm');
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.pacman'), 'pacman');
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.pkg.tar.zst'), 'pacman');
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.snap'), 'snap');
|
|
assert.equal(detectLinuxArtifactFormat('IPTVnator.flatpak'), 'flatpak');
|
|
assert.throws(
|
|
() => detectLinuxArtifactFormat('IPTVnator.tar.gz'),
|
|
/Unsupported Linux package artifact/
|
|
);
|
|
});
|
|
|
|
test('parses the required artifact and profile arguments without evaluating paths', () => {
|
|
const directory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-args-')
|
|
);
|
|
const artifactPath = path.join(directory, 'package $(touch owned).deb');
|
|
fs.writeFileSync(artifactPath, 'fixture');
|
|
|
|
try {
|
|
assert.deepEqual(
|
|
parseVerifierArguments([
|
|
'--artifact',
|
|
artifactPath,
|
|
'--profile',
|
|
'system',
|
|
]),
|
|
{
|
|
artifactPath: path.resolve(artifactPath),
|
|
profileName: 'system',
|
|
}
|
|
);
|
|
assert.throws(
|
|
() => parseVerifierArguments(['--artifact', artifactPath]),
|
|
/--profile/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseVerifierArguments([
|
|
'--artifact',
|
|
artifactPath,
|
|
'--profile',
|
|
'standard',
|
|
]),
|
|
/Unsupported Linux frame-copy profile/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseVerifierArguments([
|
|
'--artifact',
|
|
artifactPath,
|
|
'--artifact',
|
|
artifactPath,
|
|
'--profile',
|
|
'system',
|
|
]),
|
|
/duplicate --artifact/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseVerifierArguments([
|
|
'--artifact',
|
|
artifactPath,
|
|
'--profile',
|
|
'system',
|
|
'--profile',
|
|
'system',
|
|
]),
|
|
/duplicate --profile/
|
|
);
|
|
} finally {
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('extracts every payload format with argument arrays and no shell', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-extract-')
|
|
);
|
|
const invocations = [];
|
|
const runCommand = (command, args, options = {}) => {
|
|
invocations.push({ command, args: [...args], cwd: options.cwd });
|
|
if (command === 'ostree' && args[0] === 'refs') {
|
|
return {
|
|
status: 0,
|
|
stdout: 'app/com.fourgray.iptvnator/x86_64/stable\n',
|
|
stderr: '',
|
|
};
|
|
}
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
};
|
|
|
|
try {
|
|
for (const [format, fileName] of [
|
|
['appimage', 'IPTVnator.AppImage'],
|
|
['deb', 'IPTVnator.deb'],
|
|
['rpm', 'IPTVnator.rpm'],
|
|
['pacman', 'IPTVnator.pacman'],
|
|
['snap', 'IPTVnator.snap'],
|
|
['flatpak', 'IPTVnator.flatpak'],
|
|
]) {
|
|
const artifactPath = path.join(root, fileName);
|
|
const destination = path.join(root, `${format}-payload`);
|
|
fs.writeFileSync(
|
|
artifactPath,
|
|
format === 'appimage'
|
|
? Buffer.concat([Buffer.alloc(4096), Buffer.from('hsqs')])
|
|
: 'fixture'
|
|
);
|
|
fs.mkdirSync(destination);
|
|
extractLinuxArtifact({
|
|
artifactPath,
|
|
format,
|
|
destination,
|
|
runCommand,
|
|
});
|
|
}
|
|
|
|
assert.deepEqual(
|
|
invocations.map(({ command }) => command),
|
|
[
|
|
'unsquashfs',
|
|
'dpkg-deb',
|
|
'bsdtar',
|
|
'bsdtar',
|
|
'unsquashfs',
|
|
'ostree',
|
|
'flatpak',
|
|
'ostree',
|
|
'ostree',
|
|
]
|
|
);
|
|
assert.equal(
|
|
invocations.some(
|
|
({ command, args }) =>
|
|
['sh', 'bash'].includes(command) || args.includes('-c')
|
|
),
|
|
false
|
|
);
|
|
assert.deepEqual(invocations[1].args.slice(0, 2), [
|
|
'--extract',
|
|
path.join(root, 'IPTVnator.deb'),
|
|
]);
|
|
assert.deepEqual(invocations[2].args.slice(0, 2), [
|
|
'--extract',
|
|
'--file',
|
|
]);
|
|
assert.deepEqual(invocations[4].args.slice(0, 2), [
|
|
'-no-progress',
|
|
'-dest',
|
|
]);
|
|
assert.equal(invocations[5].args[1], 'init');
|
|
assert.ok(invocations[5].args.includes('--mode=archive-z2'));
|
|
assert.equal(invocations[8].args[0], 'checkout');
|
|
assert.ok(invocations[8].args.includes('-U'));
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('gives unsquashfs a fresh destination for every AppImage and Snap extraction', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-unsquashfs-')
|
|
);
|
|
const appImagePath = path.join(root, 'IPTVnator.AppImage');
|
|
const snapPath = path.join(root, 'IPTVnator.snap');
|
|
const appImageDestination = path.join(root, 'appimage-payload');
|
|
const snapDestination = path.join(root, 'snap-payload');
|
|
fs.writeFileSync(
|
|
appImagePath,
|
|
Buffer.concat([
|
|
Buffer.alloc(128),
|
|
Buffer.from('hsqs'),
|
|
Buffer.alloc(64),
|
|
Buffer.from('hsqs'),
|
|
])
|
|
);
|
|
fs.writeFileSync(snapPath, 'snap fixture');
|
|
fs.mkdirSync(appImageDestination);
|
|
fs.mkdirSync(snapDestination);
|
|
let appImageAttempt = 0;
|
|
|
|
try {
|
|
extractLinuxArtifact({
|
|
artifactPath: appImagePath,
|
|
format: 'appimage',
|
|
destination: appImageDestination,
|
|
runCommand: (command) => {
|
|
assert.equal(command, 'unsquashfs');
|
|
assert.equal(fs.existsSync(appImageDestination), false);
|
|
appImageAttempt += 1;
|
|
fs.mkdirSync(appImageDestination);
|
|
return {
|
|
status: appImageAttempt === 1 ? 1 : 0,
|
|
stdout: '',
|
|
stderr: '',
|
|
};
|
|
},
|
|
});
|
|
assert.equal(appImageAttempt, 2);
|
|
|
|
extractLinuxArtifact({
|
|
artifactPath: snapPath,
|
|
format: 'snap',
|
|
destination: snapDestination,
|
|
runCommand: (command) => {
|
|
assert.equal(command, 'unsquashfs');
|
|
assert.equal(fs.existsSync(snapDestination), false);
|
|
fs.mkdirSync(snapDestination);
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
},
|
|
});
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('initializes a user-checkout OSTree repository before importing Flatpak bundles', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-flatpak-repo-')
|
|
);
|
|
const artifactPath = path.join(root, 'IPTVnator.flatpak');
|
|
const destination = path.join(root, 'flatpak-payload');
|
|
const invocations = [];
|
|
let initializedRepository = null;
|
|
fs.writeFileSync(artifactPath, 'flatpak fixture');
|
|
|
|
try {
|
|
extractLinuxArtifact({
|
|
artifactPath,
|
|
format: 'flatpak',
|
|
destination,
|
|
runCommand: (command, args) => {
|
|
invocations.push([command, ...args]);
|
|
if (command === 'ostree' && args.includes('init')) {
|
|
initializedRepository = args
|
|
.find((argument) => argument.startsWith('--repo='))
|
|
?.slice('--repo='.length);
|
|
assert.ok(args.includes('--mode=archive-z2'));
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
}
|
|
if (command === 'flatpak') {
|
|
assert.equal(args[0], 'build-import-bundle');
|
|
assert.equal(args[1], initializedRepository);
|
|
if (!initializedRepository) {
|
|
return {
|
|
status: 1,
|
|
stdout: '',
|
|
stderr: 'error: opening repo: opendir(objects): No such file or directory',
|
|
};
|
|
}
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
}
|
|
if (command === 'ostree' && args[0] === 'refs') {
|
|
return {
|
|
status: 0,
|
|
stdout: 'app/com.fourgray.iptvnator/x86_64/stable\n',
|
|
stderr: '',
|
|
};
|
|
}
|
|
if (command === 'ostree' && args[0] === 'checkout') {
|
|
assert.ok(args.includes('-U'));
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
}
|
|
throw new Error(`Unexpected command: ${command}`);
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(
|
|
invocations.map(([command, ...args]) => [
|
|
command,
|
|
args.find((argument) =>
|
|
[
|
|
'init',
|
|
'build-import-bundle',
|
|
'refs',
|
|
'checkout',
|
|
].includes(argument)
|
|
),
|
|
]),
|
|
[
|
|
['ostree', 'init'],
|
|
['flatpak', 'build-import-bundle'],
|
|
['ostree', 'refs'],
|
|
['ostree', 'checkout'],
|
|
]
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('locates AppImage SquashFS payloads without executing a foreign-arch runtime', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-appimage-')
|
|
);
|
|
const artifactPath = path.join(root, 'arm64.AppImage');
|
|
fs.writeFileSync(
|
|
artifactPath,
|
|
Buffer.concat([
|
|
Buffer.alloc(128, 0x41),
|
|
Buffer.from('hsqs'),
|
|
Buffer.alloc(64),
|
|
Buffer.from('hsqs'),
|
|
])
|
|
);
|
|
try {
|
|
assert.deepEqual(findAppImageSquashfsOffsets(artifactPath), [128, 196]);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('finds one packaged native payload under arbitrary format roots', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-payload-')
|
|
);
|
|
const resourceDir = path.join(root, 'opt', 'IPTVnator', 'resources');
|
|
const nativeDir = path.join(
|
|
resourceDir,
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'native'
|
|
);
|
|
fs.mkdirSync(nativeDir, { recursive: true });
|
|
|
|
try {
|
|
assert.equal(findExtractedResourceDir(root), resourceDir);
|
|
const duplicateNativeDir = path.join(
|
|
root,
|
|
'duplicate',
|
|
'resources',
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'native'
|
|
);
|
|
fs.mkdirSync(duplicateNativeDir, { recursive: true });
|
|
assert.throws(
|
|
() => findExtractedResourceDir(root),
|
|
/exactly one embedded MPV native payload/
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('reads x64, arm64, and armv7 ELF architectures without host execution', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-elf-')
|
|
);
|
|
try {
|
|
for (const architecture of ['x64', 'arm64', 'armv7l']) {
|
|
const binaryPath = path.join(root, architecture);
|
|
fs.writeFileSync(binaryPath, elfHeader(architecture));
|
|
assert.equal(readElfArchitecture(binaryPath), architecture);
|
|
}
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('requires every direct helper runtime dependency for DEB, RPM, and Pacman', () => {
|
|
assert.deepEqual(
|
|
validateSystemPackageDependencies('deb', [
|
|
'libmpv2 (>= 0.35)',
|
|
'libegl1',
|
|
'libgl1',
|
|
'libgbm1',
|
|
'libc6',
|
|
]),
|
|
[]
|
|
);
|
|
assert.deepEqual(
|
|
validateSystemPackageDependencies('rpm', [
|
|
'mpv-libs',
|
|
'libglvnd-egl',
|
|
'libglvnd-glx',
|
|
'mesa-libgbm',
|
|
'glibc',
|
|
]),
|
|
[]
|
|
);
|
|
assert.deepEqual(
|
|
validateSystemPackageDependencies('pacman', [
|
|
'mpv>=0.35',
|
|
'libglvnd',
|
|
'mesa',
|
|
'glibc',
|
|
]),
|
|
[]
|
|
);
|
|
assert.match(
|
|
validateSystemPackageDependencies('deb', [
|
|
'libmpv2',
|
|
'libegl1',
|
|
'libgbm1',
|
|
])[0],
|
|
/libgl1/
|
|
);
|
|
});
|
|
|
|
test('reads DEB and RPM metadata without shell pipelines', () => {
|
|
const invocations = [];
|
|
const runCommand = (command, args) => {
|
|
invocations.push({ command, args: [...args] });
|
|
if (command === 'dpkg-deb' && args.at(-1) === 'Architecture') {
|
|
return { status: 0, stdout: 'amd64\n', stderr: '' };
|
|
}
|
|
if (command === 'dpkg-deb') {
|
|
return {
|
|
status: 0,
|
|
stdout: 'libmpv2 (>= 0.35), libc6\n',
|
|
stderr: '',
|
|
};
|
|
}
|
|
if (command === 'rpm' && args.includes('%{ARCH}\\n')) {
|
|
return { status: 0, stdout: 'x86_64\n', stderr: '' };
|
|
}
|
|
return {
|
|
status: 0,
|
|
stdout: 'mpv-libs\nglibc\n',
|
|
stderr: '',
|
|
};
|
|
};
|
|
|
|
assert.deepEqual(
|
|
readLinuxArtifactMetadata({
|
|
artifactPath: '/tmp/package name.deb',
|
|
format: 'deb',
|
|
extractionRoot: '/tmp/payload',
|
|
runCommand,
|
|
}),
|
|
{
|
|
declaredArch: 'x64',
|
|
dependencies: ['libmpv2 (>= 0.35)', 'libc6'],
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
readLinuxArtifactMetadata({
|
|
artifactPath: '/tmp/package name.rpm',
|
|
format: 'rpm',
|
|
extractionRoot: '/tmp/payload',
|
|
runCommand,
|
|
}),
|
|
{
|
|
declaredArch: 'x64',
|
|
dependencies: ['mpv-libs', 'glibc'],
|
|
}
|
|
);
|
|
assert.equal(
|
|
invocations.some(
|
|
({ command, args }) =>
|
|
['sh', 'bash'].includes(command) || args.includes('-c')
|
|
),
|
|
false
|
|
);
|
|
});
|
|
|
|
test('reads Pacman architecture and dependencies from the extracted .PKGINFO', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-pacman-')
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, '.PKGINFO'),
|
|
[
|
|
'pkgname = iptvnator',
|
|
'arch = x86_64',
|
|
'depend = mpv>=0.35',
|
|
'depend = glibc',
|
|
'',
|
|
].join('\n')
|
|
);
|
|
try {
|
|
assert.deepEqual(
|
|
readLinuxArtifactMetadata({
|
|
artifactPath: '/tmp/package.pacman',
|
|
format: 'pacman',
|
|
extractionRoot: root,
|
|
}),
|
|
{
|
|
declaredArch: 'x64',
|
|
dependencies: ['mpv>=0.35', 'glibc'],
|
|
}
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('validates an x64 system payload and executes one bounded helper probe', () => {
|
|
const fixture = createSystemPayload();
|
|
const probeCalls = [];
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: [
|
|
'libmpv2 (>= 0.35)',
|
|
'libegl1',
|
|
'libgl1',
|
|
'libgbm1',
|
|
],
|
|
elfInspector: validElfInspector,
|
|
probeRunner(command, args, options) {
|
|
probeCalls.push({ command, args, options });
|
|
return successfulProbeRunner();
|
|
},
|
|
environment: {
|
|
PATH: '/usr/bin',
|
|
LD_AUDIT: '/host/can-inject-audit.so',
|
|
LD_LIBRARY_PATH: '/host/can-mask-missing-dependencies',
|
|
LD_PRELOAD: '/host/can-inject.so',
|
|
},
|
|
});
|
|
assert.deepEqual(errors, []);
|
|
assert.equal(probeCalls.length, 1);
|
|
assert.equal(
|
|
probeCalls[0].command,
|
|
path.join(fixture.nativeDir, 'iptvnator_mpv_helper')
|
|
);
|
|
assert.deepEqual(probeCalls[0].args, ['--runtime-probe']);
|
|
assert.deepEqual(probeCalls[0].options, {
|
|
encoding: 'utf8',
|
|
env: { PATH: '/usr/bin' },
|
|
killSignal: 'SIGKILL',
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
timeout: 15000,
|
|
windowsHide: true,
|
|
});
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('rejects any stale embedded MPV native payload hidden inside app.asar', () => {
|
|
const fixture = createSystemPayload();
|
|
fs.writeFileSync(path.join(fixture.resourceDir, 'app.asar'), 'fixture');
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: validElfInspector,
|
|
probeRunner: successfulProbeRunner,
|
|
asarListPackage: () => [
|
|
'/electron-backend/main.js',
|
|
'/electron-backend/native/iptvnator_mpv_helper',
|
|
'/electron-backend/native/lib/libmpv.so.2',
|
|
],
|
|
});
|
|
assert.match(
|
|
errors.join('\n'),
|
|
/app\.asar must not contain embedded MPV native payloads.*iptvnator_mpv_helper.*libmpv\.so\.2/s
|
|
);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('excludes only Snap template library roots from Electron isolation checks', () => {
|
|
const fixture = createSystemPayload({ architecture: 'arm64' });
|
|
const packageLibraryDirs = [
|
|
path.join(fixture.appDir, 'lib', 'x86_64-linux-gnu'),
|
|
path.join(fixture.appDir, 'usr', 'lib', 'x86_64-linux-gnu'),
|
|
];
|
|
const electronLibrary = path.join(fixture.appDir, 'libelectron-extra.so');
|
|
for (const [index, packageLibraryDir] of packageLibraryDirs.entries()) {
|
|
const packageLibraryTarget = path.join(
|
|
packageLibraryDir,
|
|
`libpackage-${index}.so.0.12.2`
|
|
);
|
|
fs.mkdirSync(packageLibraryDir, { recursive: true });
|
|
fs.writeFileSync(packageLibraryTarget, 'package-managed library');
|
|
fs.symlinkSync(
|
|
path.basename(packageLibraryTarget),
|
|
path.join(packageLibraryDir, `libpackage-${index}.so.0`)
|
|
);
|
|
}
|
|
fs.writeFileSync(electronLibrary, 'electron library');
|
|
|
|
const inspectedPaths = [];
|
|
const isPackageLibraryPath = (binaryPath) =>
|
|
packageLibraryDirs.some((packageLibraryDir) =>
|
|
path
|
|
.resolve(binaryPath)
|
|
.startsWith(`${path.resolve(packageLibraryDir)}${path.sep}`)
|
|
);
|
|
const ownershipScopedElfInspector = (binaryPath) => {
|
|
inspectedPaths.push(binaryPath);
|
|
if (isPackageLibraryPath(binaryPath)) {
|
|
throw new Error(
|
|
'package-manager library tree crossed the Electron ownership boundary'
|
|
);
|
|
}
|
|
return validElfInspector(binaryPath);
|
|
};
|
|
|
|
try {
|
|
assert.deepEqual(
|
|
verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'snap',
|
|
profileName: 'portable',
|
|
packageDependencies: [],
|
|
elfInspector: ownershipScopedElfInspector,
|
|
probeRunner() {
|
|
assert.fail('foreign-architecture Snap must not probe');
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
assert.ok(inspectedPaths.includes(electronLibrary));
|
|
assert.equal(inspectedPaths.some(isPackageLibraryPath), false);
|
|
|
|
const isolationErrors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'snap',
|
|
profileName: 'portable',
|
|
packageDependencies: [],
|
|
elfInspector(binaryPath) {
|
|
if (binaryPath === electronLibrary) {
|
|
return {
|
|
soname: null,
|
|
needed: ['libmpv.so.2'],
|
|
rpath: [],
|
|
runpath: [],
|
|
};
|
|
}
|
|
return ownershipScopedElfInspector(binaryPath);
|
|
},
|
|
probeRunner() {
|
|
assert.fail('foreign-architecture Snap must not probe');
|
|
},
|
|
});
|
|
assert.match(
|
|
isolationErrors.join('\n'),
|
|
/Electron library must not link libmpv.*libelectron-extra\.so/
|
|
);
|
|
|
|
const nestedElectronLibrary = path.join(
|
|
fixture.appDir,
|
|
'future-electron-runtime',
|
|
'libfuture-electron.so'
|
|
);
|
|
fs.mkdirSync(path.dirname(nestedElectronLibrary), {
|
|
recursive: true,
|
|
});
|
|
fs.writeFileSync(nestedElectronLibrary, 'nested electron library');
|
|
const nestedIsolationErrors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'snap',
|
|
profileName: 'portable',
|
|
packageDependencies: [],
|
|
elfInspector(binaryPath) {
|
|
if (binaryPath === nestedElectronLibrary) {
|
|
return {
|
|
soname: null,
|
|
needed: ['libmpv.so.2'],
|
|
rpath: [],
|
|
runpath: [],
|
|
};
|
|
}
|
|
return ownershipScopedElfInspector(binaryPath);
|
|
},
|
|
probeRunner() {
|
|
assert.fail('foreign-architecture Snap must not probe');
|
|
},
|
|
});
|
|
assert.match(
|
|
nestedIsolationErrors.join('\n'),
|
|
/Electron library must not link libmpv.*libfuture-electron\.so/
|
|
);
|
|
|
|
fs.symlinkSync(
|
|
path.basename(electronLibrary),
|
|
path.join(fixture.appDir, 'libEGL.so')
|
|
);
|
|
const symlinkErrors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'snap',
|
|
profileName: 'portable',
|
|
packageDependencies: [],
|
|
elfInspector: ownershipScopedElfInspector,
|
|
probeRunner() {
|
|
assert.fail('foreign-architecture Snap must not probe');
|
|
},
|
|
});
|
|
assert.match(
|
|
symlinkErrors.join('\n'),
|
|
/Electron library must be a regular file: .*libEGL\.so/
|
|
);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('rejects helper probes terminated by a signal or exhausted hard timeouts', () => {
|
|
for (const { probeResult, expectedAttempts } of [
|
|
{
|
|
// An external kill is a real capability verdict, not a flake.
|
|
probeResult: {
|
|
status: null,
|
|
signal: 'SIGKILL',
|
|
stdout: '',
|
|
stderr: '',
|
|
},
|
|
expectedAttempts: 1,
|
|
},
|
|
{
|
|
// spawnSync reports a hard timeout as ETIMEDOUT alongside the
|
|
// SIGKILL it sent; the timeout wins, and a helper that keeps
|
|
// hanging still fails once the bounded attempts are spent.
|
|
probeResult: {
|
|
error: Object.assign(new Error('spawnSync helper ETIMEDOUT'), {
|
|
code: 'ETIMEDOUT',
|
|
}),
|
|
status: null,
|
|
signal: 'SIGKILL',
|
|
stdout: '',
|
|
stderr: '',
|
|
},
|
|
expectedAttempts: 2,
|
|
},
|
|
]) {
|
|
const fixture = createSystemPayload();
|
|
const warnings = [];
|
|
let probeCalls = 0;
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: validElfInspector,
|
|
probeRunner() {
|
|
probeCalls += 1;
|
|
return probeResult;
|
|
},
|
|
warn(message) {
|
|
warnings.push(message);
|
|
},
|
|
});
|
|
assert.match(
|
|
errors.join('\n'),
|
|
/(?:runtime probe terminated by signal SIGKILL|Unable to execute .*ETIMEDOUT)/
|
|
);
|
|
assert.equal(probeCalls, expectedAttempts);
|
|
assert.equal(warnings.length, expectedAttempts - 1);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('retries one timed-out helper probe and accepts a healthy second attempt', () => {
|
|
const fixture = createSystemPayload();
|
|
const probeCalls = [];
|
|
const warnings = [];
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: validElfInspector,
|
|
probeRunner(command, args, options) {
|
|
probeCalls.push({ command, args, options });
|
|
return probeCalls.length === 1
|
|
? {
|
|
error: Object.assign(
|
|
new Error('spawnSync helper ETIMEDOUT'),
|
|
{ code: 'ETIMEDOUT' }
|
|
),
|
|
status: null,
|
|
signal: 'SIGKILL',
|
|
stdout: '',
|
|
stderr: '',
|
|
}
|
|
: successfulProbeRunner();
|
|
},
|
|
warn(message) {
|
|
warnings.push(message);
|
|
},
|
|
});
|
|
assert.deepEqual(errors, []);
|
|
assert.equal(probeCalls.length, 2);
|
|
// The retry must repeat the identical bounded launch, never a weaker
|
|
// one.
|
|
assert.equal(probeCalls[1].command, probeCalls[0].command);
|
|
assert.deepEqual(probeCalls[1].args, ['--runtime-probe']);
|
|
assert.deepEqual(probeCalls[1].options, probeCalls[0].options);
|
|
assert.equal(probeCalls[1].options.timeout, 15000);
|
|
assert.equal(warnings.length, 1);
|
|
assert.match(
|
|
warnings[0],
|
|
/timed out after 15000 ms; retrying \(attempt 2 of 2\)/
|
|
);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('never retries helper probe outcomes that judge the payload', () => {
|
|
for (const { probeResult, expected } of [
|
|
{
|
|
probeResult: {
|
|
error: Object.assign(new Error('spawnSync helper ENOENT'), {
|
|
code: 'ENOENT',
|
|
}),
|
|
status: null,
|
|
signal: null,
|
|
stdout: '',
|
|
stderr: '',
|
|
},
|
|
expected: /Unable to execute .*ENOENT/,
|
|
},
|
|
{
|
|
probeResult: {
|
|
status: 127,
|
|
signal: null,
|
|
stdout: '',
|
|
stderr: 'libmpv.so.2: cannot open shared object file',
|
|
},
|
|
expected: /runtime probe failed with status 127/,
|
|
},
|
|
{
|
|
probeResult: {
|
|
status: 0,
|
|
signal: null,
|
|
stdout: '{"protocol":1,"usable":true,"libmpv":"0.41.0","renderApi":"egl"}\n{"protocol":1}\n',
|
|
stderr: '',
|
|
},
|
|
expected: /must emit exactly one newline-terminated JSON line/,
|
|
},
|
|
{
|
|
probeResult: {
|
|
status: 0,
|
|
signal: null,
|
|
stdout: '{"protocol":2,"usable":true,"libmpv":"0.41.0","renderApi":"egl"}\n',
|
|
stderr: '',
|
|
},
|
|
expected: /did not return protocol 1 usable EGL capability/,
|
|
},
|
|
]) {
|
|
const fixture = createSystemPayload();
|
|
const warnings = [];
|
|
let probeCalls = 0;
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: validElfInspector,
|
|
probeRunner() {
|
|
probeCalls += 1;
|
|
return probeResult;
|
|
},
|
|
warn(message) {
|
|
warnings.push(message);
|
|
},
|
|
});
|
|
assert.match(errors.join('\n'), expected);
|
|
assert.equal(probeCalls, 1);
|
|
assert.deepEqual(warnings, []);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('bounds timed-out helper probes to the shared attempt contract', () => {
|
|
const timeoutResult = {
|
|
error: Object.assign(new Error('spawnSync helper ETIMEDOUT'), {
|
|
code: 'ETIMEDOUT',
|
|
}),
|
|
status: null,
|
|
signal: 'SIGKILL',
|
|
stdout: '',
|
|
stderr: '',
|
|
};
|
|
const warnings = [];
|
|
let probeCalls = 0;
|
|
const result = runBoundedRuntimeProbe({
|
|
probeRunner() {
|
|
probeCalls += 1;
|
|
return timeoutResult;
|
|
},
|
|
helperPath: '/packaged/native/iptvnator_mpv_helper',
|
|
probeEnvironment: { PATH: '/usr/bin' },
|
|
warn(message) {
|
|
warnings.push(message);
|
|
},
|
|
});
|
|
assert.equal(probeCalls, 2);
|
|
assert.equal(warnings.length, 1);
|
|
// The exhausted timeout is reported verbatim so the caller still fails.
|
|
assert.equal(result, timeoutResult);
|
|
});
|
|
|
|
test('requires exact Snap graphics layouts and plugs used by the app', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-snap-metadata-')
|
|
);
|
|
const snapYamlPath = path.join(root, 'meta', 'snap.yaml');
|
|
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
|
|
fs.mkdirSync(path.join(root, 'graphics'));
|
|
writeSnapDesktopRuntimeScripts(root);
|
|
|
|
const validSnapYaml = [
|
|
'name: iptvnator',
|
|
'base: core22',
|
|
'confinement: strict',
|
|
'summary: "*literal &anchor !tag <<: is quoted"',
|
|
'# *commented-alias &commented-anchor !commented-tag',
|
|
'apps:',
|
|
' iptvnator:',
|
|
' command: iptvnator',
|
|
' environment:',
|
|
' SNAP_DESKTOP_RUNTIME: $SNAP/gnome-platform',
|
|
' plugs:',
|
|
' - desktop',
|
|
' - shared-memory',
|
|
' - graphics-core22',
|
|
'plugs:',
|
|
' shared-memory:',
|
|
' interface: shared-memory',
|
|
' private: true',
|
|
' graphics-core22:',
|
|
' interface: content',
|
|
' target: $SNAP/graphics',
|
|
' default-provider: mesa-core22',
|
|
'layout:',
|
|
' /usr/share/libdrm:',
|
|
' bind: $SNAP/graphics/libdrm',
|
|
' /usr/share/drirc.d:',
|
|
' symlink: $SNAP/graphics/drirc.d',
|
|
'',
|
|
].join('\n');
|
|
|
|
try {
|
|
fs.writeFileSync(snapYamlPath, validSnapYaml);
|
|
assert.deepEqual(validateExtractedSnapMetadata(root), []);
|
|
|
|
for (const script of SNAP_DESKTOP_RUNTIME_SCRIPTS) {
|
|
const scriptPath = path.join(root, script);
|
|
fs.rmSync(scriptPath);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
new RegExp(`required desktop runtime script.*${script}`, 'i')
|
|
);
|
|
fs.writeFileSync(scriptPath, '#!/bin/sh\n');
|
|
fs.chmodSync(scriptPath, 0o755);
|
|
}
|
|
|
|
fs.chmodSync(path.join(root, 'desktop-init.sh'), 0o644);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/desktop-init\.sh.*executable/i
|
|
);
|
|
fs.chmodSync(path.join(root, 'desktop-init.sh'), 0o755);
|
|
|
|
fs.truncateSync(snapYamlPath, 256 * 1024 + 1);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/Snap metadata exceeds.*size limit/i
|
|
);
|
|
fs.writeFileSync(snapYamlPath, validSnapYaml);
|
|
|
|
fs.rmSync(path.join(root, 'graphics'), { recursive: true });
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/empty graphics content mount directory/i
|
|
);
|
|
fs.mkdirSync(path.join(root, 'graphics'));
|
|
fs.writeFileSync(path.join(root, 'graphics', 'unexpected'), 'data');
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/graphics content mount directory must be empty/i
|
|
);
|
|
fs.rmSync(path.join(root, 'graphics'), { recursive: true });
|
|
const outsideGraphics = path.join(root, 'outside-graphics');
|
|
fs.mkdirSync(outsideGraphics);
|
|
fs.symlinkSync(outsideGraphics, path.join(root, 'graphics'), 'dir');
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/graphics content mount.*real directory/i
|
|
);
|
|
fs.rmSync(path.join(root, 'graphics'));
|
|
fs.mkdirSync(path.join(root, 'graphics'));
|
|
fs.chmodSync(path.join(root, 'graphics'), 0o700);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/graphics content mount directory must have mode 0755/i
|
|
);
|
|
fs.chmodSync(path.join(root, 'graphics'), 0o755);
|
|
|
|
for (const [mutate, expected] of [
|
|
[
|
|
(contents) => contents.replace('base: core22', 'base: core20'),
|
|
/base: core22/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'confinement: strict',
|
|
'confinement: classic'
|
|
),
|
|
/confinement: strict/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'layout:\n /usr/share/libdrm:\n bind: $SNAP/graphics/libdrm\n /usr/share/drirc.d:\n symlink: $SNAP/graphics/drirc.d\n',
|
|
''
|
|
),
|
|
/exact Snap graphics layout contract/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' /usr/share/drirc.d:\n',
|
|
' /usr/share/extra:\n bind: $SNAP/graphics/extra\n /usr/share/drirc.d:\n'
|
|
),
|
|
/exactly the libdrm and drirc.d entries/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' /usr/share/drirc.d:\n',
|
|
' /usr/share/libdrm:\n bind: $SNAP/graphics/libdrm\n /usr/share/drirc.d:\n'
|
|
),
|
|
/layout paths must be unique/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' bind: $SNAP/graphics/libdrm',
|
|
' bind: $SNAP/graphics/wrong-libdrm'
|
|
),
|
|
/libdrm.*bind: \$SNAP\/graphics\/libdrm/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' symlink: $SNAP/graphics/drirc.d',
|
|
' symlink: $SNAP/graphics/wrong-drirc.d'
|
|
),
|
|
/drirc.d.*symlink: \$SNAP\/graphics\/drirc.d/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' bind: $SNAP/graphics/libdrm\n',
|
|
' bind: $SNAP/graphics/libdrm\n type: directory\n'
|
|
),
|
|
/libdrm layout.*exactly bind/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' SNAP_DESKTOP_RUNTIME: $SNAP/gnome-platform\n',
|
|
''
|
|
),
|
|
/SNAP_DESKTOP_RUNTIME.*\$SNAP\/gnome-platform/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'$SNAP/gnome-platform',
|
|
'$SNAP/hostile-platform'
|
|
),
|
|
/SNAP_DESKTOP_RUNTIME.*\$SNAP\/gnome-platform/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(' private: true', ' private: false'),
|
|
/private: true/,
|
|
],
|
|
[
|
|
(contents) => contents.replace(' - shared-memory\n', ''),
|
|
/app.*shared-memory plug/i,
|
|
],
|
|
[
|
|
(contents) => contents.replace(' - graphics-core22\n', ''),
|
|
/app.*graphics-core22 plug/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' shared-memory:\n interface: shared-memory\n private: true\n',
|
|
''
|
|
),
|
|
/top-level shared-memory plug/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' graphics-core22:\n interface: content\n target: $SNAP/graphics\n default-provider: mesa-core22\n',
|
|
''
|
|
),
|
|
/top-level graphics-core22 plug/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' interface: content',
|
|
' interface: opengl'
|
|
),
|
|
/graphics-core22.*interface: content/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' target: $SNAP/graphics',
|
|
' target: $SNAP/wrong-graphics'
|
|
),
|
|
/graphics-core22.*target: \$SNAP\/graphics/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' default-provider: mesa-core22',
|
|
' default-provider: wrong-provider'
|
|
),
|
|
/graphics-core22.*default-provider: mesa-core22/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' default-provider: mesa-core22\n',
|
|
' default-provider: mesa-core22\n source: graphics-core22\n'
|
|
),
|
|
/graphics-core22 plug.*exactly interface, target, and default-provider/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
[
|
|
'',
|
|
'plugs:',
|
|
' duplicate-graphics:',
|
|
' interface: content',
|
|
' target: $SNAP/graphics',
|
|
' default-provider: mesa-core22',
|
|
'',
|
|
].join('\n')
|
|
),
|
|
/exactly one plug.*graphics-core22 contract/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
[
|
|
'',
|
|
'plugs:',
|
|
' competing-graphics:',
|
|
' interface: content',
|
|
' target: $SNAP/graphics',
|
|
' default-provider: hostile-provider',
|
|
' content: alternate-graphics',
|
|
'',
|
|
].join('\n')
|
|
),
|
|
/exactly one plug.*target \$SNAP\/graphics/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
' private: true\n',
|
|
' private: true\n shared-memory: named-area\n'
|
|
),
|
|
/private shared-memory plug.*exactly interface and private/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
[
|
|
'',
|
|
'plugs:',
|
|
' second-shared-memory:',
|
|
' interface: shared-memory',
|
|
' private: true',
|
|
'',
|
|
].join('\n')
|
|
),
|
|
/exactly one plug.*shared-memory interface/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
`${contents}slots:\n shared-memory:\n interface: shared-memory\n`,
|
|
/must not declare.*shared-memory slot/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
'\nplugs:\n other: { interface: shared-memory }\n'
|
|
),
|
|
/plug declarations.*block mappings/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
`${contents}slots: { leak: { interface: shared-memory } }\n`,
|
|
/slots.*block mappings/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
'\nplugs:\n network:\n interface: network\n network:\n interface: network\n'
|
|
),
|
|
/plug keys.*unique/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
[
|
|
'shared-interface: &shared.interface shared-memory',
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
'\nplugs:\n alias-plug:\n interface: *shared.interface\n'
|
|
),
|
|
].join('\n'),
|
|
/anchors, aliases, merge keys, or custom tags/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
[
|
|
'shared-interface: &sharedInterface shared-memory',
|
|
`${contents}slots:\n alias-slot:\n interface: *sharedInterface\n`,
|
|
].join('\n'),
|
|
/anchors, aliases, merge keys, or custom tags/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
'\nplugs:\n tagged:\n interface: !shared-memory shared-memory\n'
|
|
),
|
|
/anchors, aliases, merge keys, or custom tags/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
[
|
|
'shared-plug: &sharedPlug',
|
|
' interface: network',
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
'\nplugs:\n merged:\n <<: *sharedPlug\n'
|
|
),
|
|
].join('\n'),
|
|
/anchors, aliases, merge keys, or custom tags/i,
|
|
],
|
|
[
|
|
(contents) =>
|
|
contents.replace(
|
|
[
|
|
' plugs:',
|
|
' - desktop',
|
|
' - shared-memory',
|
|
].join('\n'),
|
|
[
|
|
' plugs:',
|
|
' nested:',
|
|
' - shared-memory',
|
|
].join('\n')
|
|
),
|
|
/scalar sequence.*shared-memory/i,
|
|
],
|
|
]) {
|
|
fs.writeFileSync(snapYamlPath, mutate(validSnapYaml));
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
expected
|
|
);
|
|
}
|
|
|
|
fs.rmSync(snapYamlPath);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/Missing extracted Snap metadata/
|
|
);
|
|
|
|
fs.writeFileSync(path.join(root, 'outside.yaml'), validSnapYaml);
|
|
fs.symlinkSync(
|
|
path.join(root, 'outside.yaml'),
|
|
snapYamlPath,
|
|
process.platform === 'win32' ? 'file' : undefined
|
|
);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
/regular file/
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
const SNAP_METADATA_WITH_LITERAL_HASHES = [
|
|
'name: iptvnator',
|
|
'base: core22',
|
|
'confinement: strict',
|
|
'summary: "quoted # is scalar data"',
|
|
'description: | # block scalar header comment',
|
|
' Block scalar # stays literal.',
|
|
'apps:',
|
|
' iptvnator:',
|
|
' command: iptvnator',
|
|
' environment:',
|
|
' SNAP_DESKTOP_RUNTIME: $SNAP/gnome-platform',
|
|
' plugs:',
|
|
' - shared-memory',
|
|
' - graphics-core22',
|
|
'plugs:',
|
|
' shared-memory:',
|
|
' interface: shared-memory',
|
|
' private: true',
|
|
' graphics-core22:',
|
|
' interface: content',
|
|
' target: $SNAP/graphics',
|
|
' default-provider: mesa-core22',
|
|
'layout:',
|
|
' /usr/share/libdrm:',
|
|
' bind: $SNAP/graphics/libdrm',
|
|
' /usr/share/drirc.d:',
|
|
' symlink: $SNAP/graphics/drirc.d',
|
|
'',
|
|
].join('\n');
|
|
|
|
for (const [kind, mutate, expected] of [
|
|
[
|
|
'plug',
|
|
(contents) =>
|
|
contents.replace(
|
|
'\nplugs:\n',
|
|
[
|
|
'',
|
|
'plugs:',
|
|
' hidden-extra-plug:',
|
|
' interface: shared-memory # trailing comment',
|
|
'',
|
|
].join('\n')
|
|
),
|
|
/exactly one plug.*shared-memory interface/i,
|
|
],
|
|
[
|
|
'slot',
|
|
(contents) =>
|
|
`${contents}slots:\n hidden-slot:\n interface: shared-memory # trailing comment\n`,
|
|
/must not declare.*shared-memory slot/i,
|
|
],
|
|
]) {
|
|
test(`rejects an extra Snap shared-memory ${kind} with a trailing comment`, () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-snap-comments-')
|
|
);
|
|
const snapYamlPath = path.join(root, 'meta', 'snap.yaml');
|
|
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
|
|
fs.mkdirSync(path.join(root, 'graphics'));
|
|
writeSnapDesktopRuntimeScripts(root);
|
|
|
|
try {
|
|
fs.writeFileSync(snapYamlPath, SNAP_METADATA_WITH_LITERAL_HASHES);
|
|
assert.deepEqual(validateExtractedSnapMetadata(root), []);
|
|
|
|
fs.writeFileSync(
|
|
snapYamlPath,
|
|
mutate(SNAP_METADATA_WITH_LITERAL_HASHES)
|
|
);
|
|
assert.match(
|
|
validateExtractedSnapMetadata(root).join('\n'),
|
|
expected
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
test('artifact verification enforces Snap metadata for x64 and ARM payloads', () => {
|
|
for (const architecture of ['x64', 'arm64', 'armv7l']) {
|
|
const fixture = createSystemPayload({ architecture });
|
|
const artifactPath = path.join(fixture.root, 'package.snap');
|
|
const snapYamlPath = path.join(fixture.root, 'meta', 'snap.yaml');
|
|
fs.writeFileSync(artifactPath, 'fixture');
|
|
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
|
|
fs.mkdirSync(path.join(fixture.root, 'graphics'));
|
|
writeSnapDesktopRuntimeScripts(fixture.root);
|
|
fs.writeFileSync(
|
|
snapYamlPath,
|
|
[
|
|
'name: iptvnator',
|
|
'base: core22',
|
|
'confinement: strict',
|
|
'apps:',
|
|
' iptvnator:',
|
|
' command: iptvnator',
|
|
' environment:',
|
|
' SNAP_DESKTOP_RUNTIME: $SNAP/gnome-platform',
|
|
' plugs:',
|
|
' - desktop',
|
|
' - graphics-core22',
|
|
'plugs:',
|
|
' shared-memory:',
|
|
' interface: shared-memory',
|
|
' private: true',
|
|
' graphics-core22:',
|
|
' interface: content',
|
|
' target: $SNAP/graphics',
|
|
' default-provider: mesa-core22',
|
|
'layout:',
|
|
' /usr/share/libdrm:',
|
|
' bind: $SNAP/graphics/libdrm',
|
|
' /usr/share/drirc.d:',
|
|
' symlink: $SNAP/graphics/drirc.d',
|
|
'',
|
|
].join('\n')
|
|
);
|
|
let payloadVerifierCalls = 0;
|
|
|
|
const verify = () =>
|
|
verifyLinuxFrameCopyArtifact({
|
|
artifactPath,
|
|
profileName: 'portable',
|
|
extractArtifact() {
|
|
return fixture.root;
|
|
},
|
|
metadataReader() {
|
|
return { declaredArch: architecture, dependencies: [] };
|
|
},
|
|
payloadVerifier() {
|
|
payloadVerifierCalls += 1;
|
|
return [];
|
|
},
|
|
});
|
|
|
|
try {
|
|
assert.throws(verify, /app.*shared-memory plug/i);
|
|
assert.equal(payloadVerifierCalls, 0);
|
|
|
|
fs.writeFileSync(
|
|
snapYamlPath,
|
|
fs
|
|
.readFileSync(snapYamlPath, 'utf8')
|
|
.replace(' - desktop\n', ' - shared-memory\n')
|
|
);
|
|
assert.deepEqual(verify(), {
|
|
artifactPath,
|
|
format: 'snap',
|
|
profileName: 'portable',
|
|
architecture,
|
|
});
|
|
assert.equal(payloadVerifierCalls, 1);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('verifies an outer Flatpak artifact with an unwrapped Electron ELF', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-flatpak-artifact-')
|
|
);
|
|
const artifactPath = path.join(root, 'IPTVnator.flatpak');
|
|
fs.writeFileSync(artifactPath, 'flatpak fixture');
|
|
|
|
try {
|
|
assert.deepEqual(
|
|
verifyLinuxFrameCopyArtifact({
|
|
artifactPath,
|
|
profileName: 'flatpak',
|
|
extractArtifact({ destination }) {
|
|
const appDir = path.join(
|
|
destination,
|
|
'files',
|
|
'lib',
|
|
'com.fourgray.iptvnator'
|
|
);
|
|
const resourceDir = path.join(appDir, 'resources');
|
|
const nativeDir = path.join(
|
|
resourceDir,
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'native'
|
|
);
|
|
fs.mkdirSync(nativeDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(appDir, 'iptvnator'),
|
|
elfHeader('arm64')
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(nativeDir, 'embedded-mpv-unavailable.txt'),
|
|
'Unavailable for arm64\n'
|
|
);
|
|
return destination;
|
|
},
|
|
metadataReader: () => ({
|
|
declaredArch: 'arm64',
|
|
dependencies: [],
|
|
}),
|
|
elfInspector: validElfInspector,
|
|
probeRunner() {
|
|
assert.fail('foreign Flatpak must not probe');
|
|
},
|
|
}),
|
|
{
|
|
artifactPath: path.resolve(artifactPath),
|
|
format: 'flatpak',
|
|
profileName: 'flatpak',
|
|
architecture: 'arm64',
|
|
}
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('bundled probes remove ambient loader paths and use only packaged libraries', () => {
|
|
assert.deepEqual(
|
|
createRuntimeProbeEnvironment({
|
|
environment: {
|
|
PATH: '/usr/bin',
|
|
BASH_ENV: '/host/hostile-bash-env',
|
|
ENV: '/host/hostile-shell-env',
|
|
BASHOPTS: 'extdebug',
|
|
SHELLOPTS: 'xtrace',
|
|
PS4: '$(/host/hostile-trace-hook)',
|
|
BASH_XTRACEFD: '9',
|
|
CDPATH: '/host/hostile-cdpath',
|
|
'BASH_FUNC_dirname%%':
|
|
'() { printf /host/hostile-provider-root; }',
|
|
LD_AUDIT: '/host/can-inject-audit.so',
|
|
LD_LIBRARY_PATH: '/host/can-mask-missing-dependencies',
|
|
LD_ORIGIN_PATH: '/host/can-change-origin',
|
|
LD_PRELOAD: '/host/can-inject.so',
|
|
__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:
|
|
'/host/egl/external-platform',
|
|
__EGL_EXTERNAL_PLATFORM_CONFIG_FILENAMES:
|
|
'/host/egl/external-platform.json',
|
|
__EGL_VENDOR_LIBRARY_DIRS: '/host/egl/vendor',
|
|
__EGL_VENDOR_LIBRARY_FILENAMES: '/host/egl/vendor/host.json',
|
|
GBM_BACKEND: 'host-gbm',
|
|
GBM_BACKENDS_PATH: '/host/gbm',
|
|
LIBGL_DRIVERS_PATH: '/host/dri',
|
|
MESA_LOADER_DRIVER_OVERRIDE: 'host-dri',
|
|
LIBVA_DRIVER_NAME: 'host-va',
|
|
LIBVA_DRIVERS_PATH: '/host/va',
|
|
VDPAU_DRIVER_PATH: '/host/vdpau',
|
|
VK_DRIVER_FILES: '/host/vulkan/driver.json',
|
|
VK_ICD_FILENAMES: '/host/vulkan/icd.json',
|
|
VK_ADD_DRIVER_FILES: '/host/vulkan/add-driver.json',
|
|
VK_ADD_LAYER_PATH: '/host/vulkan/add-layer',
|
|
VK_IMPLICIT_LAYER_PATH: '/host/vulkan/implicit-layer',
|
|
VK_ADD_IMPLICIT_LAYER_PATH: '/host/vulkan/add-implicit-layer',
|
|
VK_LAYER_PATH: '/host/vulkan/layer',
|
|
LIBGL_ALWAYS_SOFTWARE: '1',
|
|
},
|
|
nativeDir: '/package/resources/native',
|
|
runtimeMode: 'bundled',
|
|
}),
|
|
{
|
|
PATH: '/usr/bin',
|
|
LIBGL_ALWAYS_SOFTWARE: '1',
|
|
LD_LIBRARY_PATH: '/package/resources/native/lib',
|
|
}
|
|
);
|
|
});
|
|
|
|
test('rejects helper probe framing and fields that runtime capability rejects', () => {
|
|
const validPayload =
|
|
'{"protocol":1,"usable":true,"libmpv":"0.41.0","renderApi":"egl"}';
|
|
for (const stdout of [
|
|
validPayload,
|
|
`${validPayload}\n\n`,
|
|
`${validPayload.slice(0, -1)},"extra":true}\n`,
|
|
]) {
|
|
const fixture = createSystemPayload();
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: validElfInspector,
|
|
probeRunner() {
|
|
return {
|
|
status: 0,
|
|
signal: null,
|
|
stdout,
|
|
stderr: '',
|
|
};
|
|
},
|
|
});
|
|
assert.match(
|
|
errors.join('\n'),
|
|
/runtime probe (?:must emit|did not return)/
|
|
);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('reports missing helper, wrong mode, wrong profile, isolation, and loader failures', () => {
|
|
const cases = [
|
|
{
|
|
mutate({ nativeDir }) {
|
|
fs.rmSync(path.join(nativeDir, 'iptvnator_mpv_helper'));
|
|
},
|
|
expected: /Missing embedded MPV frame-copy helper/,
|
|
},
|
|
{
|
|
mutate({ nativeDir }) {
|
|
fs.chmodSync(
|
|
path.join(nativeDir, 'iptvnator_mpv_helper'),
|
|
0o644
|
|
);
|
|
},
|
|
expected: /must have mode 0755/,
|
|
},
|
|
{
|
|
profileName: 'portable',
|
|
expected: /does not include target "deb"/,
|
|
},
|
|
{
|
|
elfInspector(binaryPath) {
|
|
if (path.basename(binaryPath) === 'embedded_mpv.node') {
|
|
return {
|
|
soname: null,
|
|
needed: ['libmpv.so.2'],
|
|
rpath: [],
|
|
runpath: [],
|
|
};
|
|
}
|
|
return validElfInspector(binaryPath);
|
|
},
|
|
expected: /addon must not link libmpv/,
|
|
},
|
|
{
|
|
probeRunner() {
|
|
return {
|
|
status: 127,
|
|
signal: null,
|
|
stdout: '',
|
|
stderr: 'error while loading shared libraries: libmpv.so.2',
|
|
};
|
|
},
|
|
expected: /runtime probe failed with status 127.*libmpv\.so\.2/s,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = createSystemPayload();
|
|
try {
|
|
testCase.mutate?.(fixture);
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: testCase.profileName ?? 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: testCase.elfInspector ?? validElfInspector,
|
|
probeRunner: testCase.probeRunner ?? successfulProbeRunner,
|
|
});
|
|
assert.match(errors.join('\n'), testCase.expected);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('rejects an x64 package manifest produced from a target subset', () => {
|
|
const fixture = createSystemPayload();
|
|
const manifestPath = path.join(
|
|
fixture.nativeDir,
|
|
'embedded-mpv-runtime.json'
|
|
);
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
manifest.targets = ['deb'];
|
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
try {
|
|
const errors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
|
|
elfInspector: validElfInspector,
|
|
probeRunner: successfulProbeRunner,
|
|
});
|
|
assert.match(
|
|
errors.join('\n'),
|
|
/manifest targets.*must equal \["deb","pacman","rpm"\]/
|
|
);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('requires marker-only foreign packages, scans Electron, and never probes', () => {
|
|
const fixture = createSystemPayload({ architecture: 'arm64' });
|
|
let probeCalls = 0;
|
|
try {
|
|
assert.deepEqual(
|
|
verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'appimage',
|
|
profileName: 'portable',
|
|
packageDependencies: [],
|
|
elfInspector: validElfInspector,
|
|
probeRunner() {
|
|
probeCalls += 1;
|
|
return successfulProbeRunner();
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
assert.equal(probeCalls, 0);
|
|
|
|
const isolationErrors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'appimage',
|
|
profileName: 'portable',
|
|
packageDependencies: [],
|
|
elfInspector(binaryPath) {
|
|
if (path.basename(binaryPath) === 'iptvnator.bin') {
|
|
return {
|
|
soname: null,
|
|
needed: ['/tmp/libmpv.so.2'],
|
|
rpath: [],
|
|
runpath: [],
|
|
};
|
|
}
|
|
return validElfInspector(binaryPath);
|
|
},
|
|
probeRunner() {
|
|
probeCalls += 1;
|
|
return successfulProbeRunner();
|
|
},
|
|
});
|
|
assert.match(
|
|
isolationErrors.join('\n'),
|
|
/Electron binary must not link libmpv/
|
|
);
|
|
assert.equal(probeCalls, 0);
|
|
|
|
for (const forbiddenDependency of DEB_SYSTEM_PACKAGE_DEPENDENCIES) {
|
|
const dependencyErrors = verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'deb',
|
|
profileName: 'system',
|
|
packageDependencies: [forbiddenDependency, 'libc6'],
|
|
declaredArch: 'arm64',
|
|
elfInspector: validElfInspector,
|
|
probeRunner: successfulProbeRunner,
|
|
});
|
|
assert.match(
|
|
dependencyErrors.join('\n'),
|
|
new RegExp(
|
|
`must not declare frame-copy dependency ${forbiddenDependency}`
|
|
)
|
|
);
|
|
}
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('validates a marker-only Flatpak with an unwrapped Electron ELF', () => {
|
|
const fixture = createSystemPayload({
|
|
architecture: 'arm64',
|
|
electronBinaryName: 'iptvnator',
|
|
});
|
|
try {
|
|
assert.deepEqual(
|
|
verifyExtractedLinuxFrameCopyRuntime({
|
|
resourceDir: fixture.resourceDir,
|
|
artifactFormat: 'flatpak',
|
|
profileName: 'flatpak',
|
|
packageDependencies: [],
|
|
elfInspector: validElfInspector,
|
|
probeRunner() {
|
|
assert.fail(
|
|
'foreign Flatpak must not run the helper probe'
|
|
);
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
} finally {
|
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('always removes its temporary extraction root after a verifier failure', () => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-verifier-cleanup-parent-')
|
|
);
|
|
const artifactPath = path.join(root, 'package.deb');
|
|
fs.writeFileSync(artifactPath, 'fixture');
|
|
let extractionDestination;
|
|
|
|
try {
|
|
assert.throws(
|
|
() =>
|
|
verifyLinuxFrameCopyArtifact({
|
|
artifactPath,
|
|
profileName: 'system',
|
|
extractArtifact({ destination }) {
|
|
extractionDestination = destination;
|
|
fs.writeFileSync(
|
|
path.join(path.dirname(destination), 'sentinel'),
|
|
'temporary'
|
|
);
|
|
throw new Error('intentional extraction failure');
|
|
},
|
|
}),
|
|
/intentional extraction failure/
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(path.dirname(extractionDestination)),
|
|
false,
|
|
'temporary verifier root must be removed'
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|