mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
* docs: design Linux frame-copy packaging * docs: plan Linux frame-copy packaging * feat(packaging): define Linux frame-copy profiles * fix(packaging): reject inherited profile names * feat(embedded-mpv): validate staged Linux runtime * fix(embedded-mpv): require Linux source packages * fix(embedded-mpv): harden Linux runtime staging * feat(embedded-mpv): build LGPL Linux runtime * fix(embedded-mpv): pin Linux runtime inputs * feat(embedded-mpv): build relocatable Linux helper * fix(embedded-mpv): require bundled Linux runtime * fix(embedded-mpv): make Linux runtime portable * feat(packaging): ship Linux frame-copy artifacts * fix(embedded-mpv): verify Linux helper linkage * fix(packaging): enforce Linux frame-copy isolation * fix(embedded-mpv): pin Linux display data * docs(embedded-mpv): document Linux frame-copy packaging * feat(embedded-mpv): probe Linux frame-copy runtime * test(embedded-mpv): smoke packaged Linux frame-copy * docs(embedded-mpv): clarify Linux system runtime baseline * fix(embedded-mpv): harden Linux runtime capability gate * ci: verify Linux frame-copy packages * test(embedded-mpv): harden packaged Linux smoke * test(embedded-mpv): preserve packaged GL mode * test(packaging): harden Linux package probes * fix(embedded-mpv): enable private Snap shared memory * fix(embedded-mpv): sanitize Linux helper environment * fix(packaging): enforce private Snap memory semantics * fix(packaging): reject ambiguous Snap memory metadata * fix(embedded-mpv): prioritize trusted Snap GL * fix(packaging): reject advanced Snap YAML semantics * fix(packaging): reject arbitrary Snap YAML aliases * feat(packaging): ship Linux runtime license notices * docs(embedded-mpv): document Linux runtime distribution * fix(packaging): parse Snap trailing comments safely * fix(release): gate Snap publish on public source release * fix(packaging): strip VCS metadata from source bundle * docs(packaging): clarify Linux source release gate * test(embedded-mpv): smoke missing bundled libmpv * style(embedded-mpv): format final validation inputs * fix(e2e): satisfy fixture index signature typing * fix(ci): declare fontconfig gperf generator * fix(embedded-mpv): hash runtime cache identities * fix(packaging): harden Linux frame-copy delivery * fix(packaging): tighten runtime delivery gates * fix(ci): decouple Linux runtime matrix * fix(packaging): harden Linux frame-copy delivery * fix(packaging): validate Linux frame-copy runtimes * fix(packaging): scope Snap Electron library checks * feat(packaging): ship Linux frame-copy runtimes * fix(packaging): improve Linux runtime smoke diagnostics * fix(packaging): expose bounded helper probe details * test(packaging): trace Snap EGL probe failures * fix(packaging): prefer core22 ABI in Snap helper * fix(packaging): bound helper probe capture * fix(packaging): harden Linux frame-copy releases * fix(packaging): canonicalize libplacebo submodule identity * fix(packaging): make source archive inspection portable * fix(packaging): harden Snap release verification
289 lines
9.2 KiB
TypeScript
289 lines
9.2 KiB
TypeScript
import assert = require('node:assert/strict');
|
|
import {
|
|
chmodSync,
|
|
existsSync,
|
|
lstatSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readlinkSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { afterEach, describe, it } from 'node:test';
|
|
import {
|
|
createDisposablePackagedLinuxApp,
|
|
createPackagedEntryGuard,
|
|
readPackagedRuntimeIdentity,
|
|
} from './embedded-mpv-frame-copy-packaged-filesystem';
|
|
|
|
const temporaryDirectories = new Set<string>();
|
|
|
|
type PackageFixture = {
|
|
executablePath: string;
|
|
libmpvAliasPath: string;
|
|
libmpvSonamePath: string;
|
|
nativeDir: string;
|
|
packageRoot: string;
|
|
payloadPath: string;
|
|
runtimeManifestPath: string;
|
|
};
|
|
|
|
function createPackageFixture(): PackageFixture {
|
|
const packageRoot = mkdtempSync(
|
|
join(tmpdir(), 'iptvnator-packaged-fixture-source-')
|
|
);
|
|
temporaryDirectories.add(packageRoot);
|
|
const executablePath = join(packageRoot, 'IPTVnator');
|
|
const nativeDir = join(
|
|
packageRoot,
|
|
'resources',
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'native'
|
|
);
|
|
const payloadPath = join(packageRoot, 'resources', 'payload.bin');
|
|
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
|
|
const runtimeLibraryDir = join(nativeDir, 'lib');
|
|
const libmpvSonamePath = join(runtimeLibraryDir, 'libmpv.so.2');
|
|
const libmpvAliasPath = join(runtimeLibraryDir, 'libmpv.so');
|
|
|
|
mkdirSync(runtimeLibraryDir, { recursive: true });
|
|
writeFileSync(executablePath, '#!/bin/sh\nexit 0\n');
|
|
chmodSync(executablePath, 0o755);
|
|
writeFileSync(payloadPath, 'packaged payload');
|
|
chmodSync(payloadPath, 0o640);
|
|
writeFileSync(libmpvSonamePath, 'packaged libmpv');
|
|
symlinkSync('libmpv.so.2', libmpvAliasPath);
|
|
writeFileSync(
|
|
runtimeManifestPath,
|
|
JSON.stringify({
|
|
arch: 'x64',
|
|
libmpvSoname: 'libmpv.so.2',
|
|
platform: 'linux',
|
|
profile: 'portable',
|
|
runtimeMode: 'bundled',
|
|
})
|
|
);
|
|
|
|
if (process.platform !== 'win32') {
|
|
symlinkSync(
|
|
'payload.bin',
|
|
join(packageRoot, 'resources', 'payload-link')
|
|
);
|
|
}
|
|
|
|
return {
|
|
executablePath,
|
|
libmpvAliasPath,
|
|
libmpvSonamePath,
|
|
nativeDir,
|
|
packageRoot,
|
|
payloadPath,
|
|
runtimeManifestPath,
|
|
};
|
|
}
|
|
|
|
function entryExists(entryPath: string): boolean {
|
|
try {
|
|
lstatSync(entryPath);
|
|
return true;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const directory of temporaryDirectories) {
|
|
rmSync(directory, { force: true, recursive: true });
|
|
}
|
|
temporaryDirectories.clear();
|
|
});
|
|
|
|
describe('disposable unpacked package clone', () => {
|
|
it('requires a safe versioned libmpv target in the packaged manifest', () => {
|
|
const source = createPackageFixture();
|
|
|
|
assert.equal(
|
|
readPackagedRuntimeIdentity(source.nativeDir).libmpvSoname,
|
|
'libmpv.so.2'
|
|
);
|
|
|
|
writeFileSync(
|
|
source.runtimeManifestPath,
|
|
JSON.stringify({
|
|
arch: 'x64',
|
|
libmpvSoname: '../libmpv.so.2',
|
|
platform: 'linux',
|
|
profile: 'portable',
|
|
runtimeMode: 'bundled',
|
|
})
|
|
);
|
|
assert.throws(
|
|
() => readPackagedRuntimeIdentity(source.nativeDir),
|
|
/libmpvSoname/
|
|
);
|
|
});
|
|
|
|
it('hides and restores a cloned regular dependency without changing the source package', () => {
|
|
const source = createPackageFixture();
|
|
const clone = createDisposablePackagedLinuxApp(source.executablePath);
|
|
temporaryDirectories.add(clone.temporaryRoot);
|
|
|
|
const clonedLibmpvPath = join(clone.nativeDir, 'lib', 'libmpv.so.2');
|
|
const guard = createPackagedEntryGuard(clonedLibmpvPath, {
|
|
expectedKind: 'regular-file',
|
|
hiddenDirectory: clone.temporaryRoot,
|
|
});
|
|
|
|
try {
|
|
assert.equal(lstatSync(clonedLibmpvPath).isFile(), true);
|
|
assert.equal(
|
|
statSync(clonedLibmpvPath).ino,
|
|
statSync(source.libmpvSonamePath).ino
|
|
);
|
|
|
|
guard.hide();
|
|
|
|
assert.equal(entryExists(clonedLibmpvPath), false);
|
|
assert.equal(lstatSync(source.libmpvSonamePath).isFile(), true);
|
|
assert.equal(
|
|
readFileSync(source.libmpvSonamePath, 'utf8'),
|
|
'packaged libmpv'
|
|
);
|
|
|
|
guard.restore();
|
|
|
|
assert.equal(lstatSync(clonedLibmpvPath).isFile(), true);
|
|
assert.equal(
|
|
statSync(clonedLibmpvPath).ino,
|
|
statSync(source.libmpvSonamePath).ino
|
|
);
|
|
} finally {
|
|
guard.restore();
|
|
clone.cleanup();
|
|
temporaryDirectories.delete(clone.temporaryRoot);
|
|
}
|
|
|
|
assert.equal(entryExists(source.libmpvSonamePath), true);
|
|
});
|
|
|
|
it('hides and restores a cloned symbolic link without dereferencing it', () => {
|
|
const source = createPackageFixture();
|
|
const clone = createDisposablePackagedLinuxApp(source.executablePath);
|
|
temporaryDirectories.add(clone.temporaryRoot);
|
|
|
|
const clonedAliasPath = join(clone.nativeDir, 'lib', 'libmpv.so');
|
|
const guard = createPackagedEntryGuard(clonedAliasPath, {
|
|
expectedKind: 'symbolic-link',
|
|
hiddenDirectory: clone.temporaryRoot,
|
|
});
|
|
|
|
try {
|
|
guard.hide();
|
|
assert.equal(entryExists(clonedAliasPath), false);
|
|
assert.equal(
|
|
lstatSync(source.libmpvAliasPath).isSymbolicLink(),
|
|
true
|
|
);
|
|
assert.equal(readlinkSync(source.libmpvAliasPath), 'libmpv.so.2');
|
|
|
|
guard.restore();
|
|
assert.equal(lstatSync(clonedAliasPath).isSymbolicLink(), true);
|
|
assert.equal(readlinkSync(clonedAliasPath), 'libmpv.so.2');
|
|
} finally {
|
|
guard.restore();
|
|
clone.cleanup();
|
|
temporaryDirectories.delete(clone.temporaryRoot);
|
|
}
|
|
});
|
|
|
|
it('hardlinks regular files and preserves modes, symlinks, and the source manifest', () => {
|
|
const source = createPackageFixture();
|
|
const clone = createDisposablePackagedLinuxApp(source.executablePath);
|
|
temporaryDirectories.add(clone.temporaryRoot);
|
|
|
|
try {
|
|
assert.notEqual(clone.packageRoot, source.packageRoot);
|
|
assert.equal(
|
|
statSync(clone.executablePath).mode & 0o777,
|
|
statSync(source.executablePath).mode & 0o777
|
|
);
|
|
|
|
const clonedPayloadPath = join(
|
|
clone.packageRoot,
|
|
'resources',
|
|
'payload.bin'
|
|
);
|
|
assert.equal(
|
|
statSync(clonedPayloadPath).ino,
|
|
statSync(source.payloadPath).ino
|
|
);
|
|
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
|
|
|
|
if (process.platform !== 'win32') {
|
|
const clonedLinkPath = join(
|
|
clone.packageRoot,
|
|
'resources',
|
|
'payload-link'
|
|
);
|
|
assert.equal(lstatSync(clonedLinkPath).isSymbolicLink(), true);
|
|
assert.equal(readlinkSync(clonedLinkPath), 'payload.bin');
|
|
}
|
|
|
|
const clonedRuntimeManifestPath = join(
|
|
clone.nativeDir,
|
|
'embedded-mpv-runtime.json'
|
|
);
|
|
assert.equal(existsSync(clonedRuntimeManifestPath), true);
|
|
assert.equal(existsSync(source.runtimeManifestPath), true);
|
|
} finally {
|
|
clone.cleanup();
|
|
temporaryDirectories.delete(clone.temporaryRoot);
|
|
}
|
|
|
|
assert.equal(existsSync(clone.temporaryRoot), false);
|
|
assert.equal(existsSync(source.runtimeManifestPath), true);
|
|
});
|
|
|
|
it('copies a regular file when hardlinking is unavailable', () => {
|
|
const source = createPackageFixture();
|
|
const clone = createDisposablePackagedLinuxApp(source.executablePath, {
|
|
linkFile() {
|
|
throw Object.assign(new Error('cross-device hardlink'), {
|
|
code: 'EXDEV',
|
|
});
|
|
},
|
|
});
|
|
temporaryDirectories.add(clone.temporaryRoot);
|
|
|
|
try {
|
|
assert.equal(statSync(clone.executablePath).mode & 0o777, 0o755);
|
|
const clonedPayloadPath = join(
|
|
clone.packageRoot,
|
|
'resources',
|
|
'payload.bin'
|
|
);
|
|
assert.equal(
|
|
readFileSync(clonedPayloadPath, 'utf8'),
|
|
readFileSync(source.payloadPath, 'utf8')
|
|
);
|
|
assert.notEqual(
|
|
statSync(clonedPayloadPath).ino,
|
|
statSync(source.payloadPath).ino
|
|
);
|
|
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
|
|
} finally {
|
|
clone.cleanup();
|
|
temporaryDirectories.delete(clone.temporaryRoot);
|
|
}
|
|
});
|
|
});
|