Files
iptvnator/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/validation-primitives.ts
T
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

* docs(embedded-mpv): document Linux frame-copy packaging

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

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

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00

163 lines
4.5 KiB
TypeScript

import { createHash } from 'crypto';
import type * as nodeFileSystem from 'fs';
import path from 'path';
import { isDeepStrictEqual } from 'util';
import {
SAFE_RUNTIME_NAME_PATTERN,
SHA256_PATTERN,
SHARED_LIBRARY_PATTERN,
} from './contracts';
import type {
EmbeddedMpvFrameCopyRuntimeFailureReason,
EmbeddedMpvFrameCopyRuntimeFileSystem,
EmbeddedMpvFrameCopyRuntimeResult,
RuntimeFile,
ValidationFailure,
ValidationResult,
} from './types';
export function failure(
reason: EmbeddedMpvFrameCopyRuntimeFailureReason
): EmbeddedMpvFrameCopyRuntimeResult {
return { usable: false, reason };
}
export function validationFailure(
reason: EmbeddedMpvFrameCopyRuntimeFailureReason
): ValidationFailure {
return { reason };
}
export function isValidationFailure<T>(
result: ValidationResult<T>
): result is ValidationFailure {
return 'reason' in result;
}
export function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
export function hasExactFields(
value: Record<string, unknown>,
fields: readonly string[]
): boolean {
return isDeepStrictEqual(Object.keys(value).sort(), [...fields].sort());
}
export function isSafeRuntimeName(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value !== '.' &&
value !== '..' &&
path.basename(value) === value &&
!value.includes('/') &&
!value.includes('\\') &&
SAFE_RUNTIME_NAME_PATTERN.test(value) &&
SHARED_LIBRARY_PATTERN.test(value)
);
}
export function isMissingFileError(error: unknown): boolean {
return (
isObject(error) &&
typeof error.code === 'string' &&
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
);
}
export function fileIdentity(
filePath: string,
stat: nodeFileSystem.Stats,
contents: Buffer
): string {
return [
path.resolve(filePath),
stat.dev,
stat.ino,
stat.mode,
stat.size,
stat.mtimeMs,
stat.ctimeMs,
createHash('sha256').update(contents).digest('hex'),
].join(':');
}
export function readManifest(
manifestPath: string,
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem
): ValidationResult<Record<string, unknown>> {
let contents: Buffer;
try {
contents = fileSystem.readFileSync(manifestPath);
} catch {
return validationFailure('runtime-manifest-invalid');
}
try {
const parsed: unknown = JSON.parse(contents.toString('utf8'));
return isObject(parsed)
? { value: parsed }
: validationFailure('runtime-manifest-invalid');
} catch {
return validationFailure('runtime-manifest-invalid');
}
}
export function validateTargets(
targets: unknown,
allowedTargets: ReadonlySet<string>
): boolean {
const expectedTargets = [...allowedTargets].sort();
if (
!Array.isArray(targets) ||
targets.length !== expectedTargets.length ||
targets.some(
(target) =>
typeof target !== 'string' ||
target.trim() !== target ||
target.toLowerCase() !== target ||
!allowedTargets.has(target)
)
) {
return false;
}
return isDeepStrictEqual(targets, expectedTargets);
}
export function validateRuntimeFiles(value: unknown): RuntimeFile[] | null {
if (!Array.isArray(value) || value.length === 0) {
return null;
}
const runtimeFiles: RuntimeFile[] = [];
const names = new Set<string>();
for (const candidate of value) {
if (
!isObject(candidate) ||
!hasExactFields(candidate, ['name', 'sha256', 'size']) ||
!isSafeRuntimeName(candidate.name) ||
!Number.isSafeInteger(candidate.size) ||
(candidate.size as number) <= 0 ||
typeof candidate.sha256 !== 'string' ||
!SHA256_PATTERN.test(candidate.sha256) ||
names.has(candidate.name)
) {
return null;
}
names.add(candidate.name);
runtimeFiles.push({
name: candidate.name,
size: candidate.size as number,
sha256: candidate.sha256,
});
}
return isDeepStrictEqual(
runtimeFiles.map(({ name }) => name).sort(),
runtimeFiles.map(({ name }) => name)
)
? runtimeFiles
: null;
}