204 lines
6.6 KiB
JavaScript
204 lines
6.6 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { unzipSync } from 'fflate';
|
|
|
|
const repositoryRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..'
|
|
);
|
|
const lockPath = path.join(
|
|
repositoryRoot,
|
|
'scripts',
|
|
'reviewed-ffmpeg-core-lock.json'
|
|
);
|
|
const maximumArchiveBytes = 64 * 1024 * 1024;
|
|
const maximumAssetBytes = 64 * 1024 * 1024;
|
|
const requiredAssetPaths = new Set([
|
|
'st/ffmpeg-core.js',
|
|
'st/ffmpeg-core.wasm',
|
|
'mt/ffmpeg-core.js',
|
|
'mt/ffmpeg-core.wasm',
|
|
'mt/ffmpeg-core.worker.js',
|
|
]);
|
|
|
|
function sha256(bytes) {
|
|
return createHash('sha256').update(bytes).digest('hex');
|
|
}
|
|
|
|
function assertSafeRelativePath(value) {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length === 0 ||
|
|
value.includes('\\') ||
|
|
value.includes('\0') ||
|
|
value.startsWith('/') ||
|
|
path.posix.normalize(value) !== value ||
|
|
value.split('/').some((part) => !part || part === '.' || part === '..')
|
|
) {
|
|
throw new Error(`Unsafe reviewed-core path: ${String(value)}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
async function requireRealDirectory(directory, create = false) {
|
|
let details = await lstat(directory).catch((error) => {
|
|
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
return undefined;
|
|
}
|
|
throw error;
|
|
});
|
|
if (!details && create) {
|
|
await mkdir(directory);
|
|
details = await lstat(directory);
|
|
}
|
|
if (!details || details.isSymbolicLink() || !details.isDirectory()) {
|
|
throw new Error(`Required directory is unsafe: ${directory}`);
|
|
}
|
|
}
|
|
|
|
const lock = JSON.parse(await readFile(lockPath, 'utf8'));
|
|
const lockedAssetPaths = new Set(
|
|
Array.isArray(lock.assets) ? lock.assets.map((asset) => asset.path) : []
|
|
);
|
|
if (
|
|
lock.schemaVersion !== 1 ||
|
|
typeof lock.buildId !== 'string' ||
|
|
!/^[a-zA-Z0-9._-]+$/.test(lock.buildId) ||
|
|
lock.coreVersion !== '0.12.10' ||
|
|
lock.profile !== 'reviewed-stack5m' ||
|
|
lock.sourceArchive?.path !==
|
|
'release/ffmpeg-core-0.12.10-reviewed-stack5m.1-corresponding-source.tar.xz' ||
|
|
!/^[a-f0-9]{64}$/.test(lock.sourceArchive?.sha256 ?? '') ||
|
|
lock.sourceLock?.path !== 'scripts/ffmpeg-source-lock.json' ||
|
|
!/^[a-f0-9]{64}$/.test(lock.sourceLock?.sha256 ?? '') ||
|
|
lock.buildDriver?.path !== 'scripts/build-reviewed-ffmpeg-core.sh' ||
|
|
!/^[a-f0-9]{64}$/.test(lock.buildDriver?.sha256 ?? '') ||
|
|
lock.patch?.upstreamCommit !== 'b409e36475bc21f0451b5b1e1d126fa82871439a' ||
|
|
lock.patch?.stackSizeBytes !== 5 * 1024 * 1024 ||
|
|
typeof lock.opusRegression?.singleThread !== 'boolean' ||
|
|
typeof lock.opusRegression?.multiThread !== 'boolean' ||
|
|
!Array.isArray(lock.assets) ||
|
|
lock.assets.length !== 5 ||
|
|
lock.assets.some(
|
|
(asset) =>
|
|
!requiredAssetPaths.has(asset.path) ||
|
|
!Number.isSafeInteger(asset.bytes) ||
|
|
asset.bytes < 1 ||
|
|
asset.bytes > maximumAssetBytes ||
|
|
!/^[a-f0-9]{64}$/.test(asset.sha256 ?? '')
|
|
) ||
|
|
lockedAssetPaths.size !== requiredAssetPaths.size ||
|
|
[...requiredAssetPaths].some((assetPath) => !lockedAssetPaths.has(assetPath))
|
|
) {
|
|
throw new Error(`Invalid reviewed FFmpeg core lock: ${lockPath}`);
|
|
}
|
|
|
|
const artifactRelativePath = assertSafeRelativePath(lock.artifact?.path);
|
|
if (artifactRelativePath !== `vendor/ffmpeg-core-${lock.buildId}.zip`) {
|
|
throw new Error('Reviewed-core artifact path and build identity disagree.');
|
|
}
|
|
const artifact = await readFile(
|
|
path.join(repositoryRoot, ...artifactRelativePath.split('/'))
|
|
);
|
|
if (
|
|
artifact.byteLength < 1 ||
|
|
artifact.byteLength > maximumArchiveBytes ||
|
|
artifact.byteLength !== lock.artifact.bytes ||
|
|
sha256(artifact) !== lock.artifact.sha256
|
|
) {
|
|
throw new Error('Reviewed FFmpeg core archive failed locked verification.');
|
|
}
|
|
|
|
const lockedArchiveEntries = new Map(
|
|
lock.assets.map((asset) => [asset.path, asset])
|
|
);
|
|
const archiveEntries = new Set();
|
|
const uncompressed = unzipSync(artifact, {
|
|
filter: (entry) => {
|
|
const relative = assertSafeRelativePath(entry.name);
|
|
const locked = lockedArchiveEntries.get(relative);
|
|
if (
|
|
!locked ||
|
|
archiveEntries.has(relative) ||
|
|
entry.originalSize !== locked.bytes ||
|
|
entry.originalSize < 1 ||
|
|
entry.originalSize > maximumAssetBytes ||
|
|
entry.size < 1 ||
|
|
entry.size > maximumArchiveBytes
|
|
) {
|
|
throw new Error(
|
|
`Reviewed FFmpeg core archive has an unexpected entry: ${entry.name}`
|
|
);
|
|
}
|
|
archiveEntries.add(relative);
|
|
return true;
|
|
},
|
|
});
|
|
if (archiveEntries.size !== lockedArchiveEntries.size) {
|
|
throw new Error('Reviewed FFmpeg core archive is missing an expected entry.');
|
|
}
|
|
|
|
const vendorRoot = path.join(repositoryRoot, 'public', 'vendor', 'ffmpeg');
|
|
const outputRoot = path.join(vendorRoot, lock.buildId);
|
|
await requireRealDirectory(repositoryRoot);
|
|
await requireRealDirectory(path.join(repositoryRoot, 'public'));
|
|
await requireRealDirectory(path.join(repositoryRoot, 'public', 'vendor'), true);
|
|
const existingVendorRoot = await lstat(vendorRoot).catch((error) => {
|
|
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
return undefined;
|
|
}
|
|
throw error;
|
|
});
|
|
if (
|
|
existingVendorRoot &&
|
|
(existingVendorRoot.isSymbolicLink() || !existingVendorRoot.isDirectory())
|
|
) {
|
|
throw new Error(`Generated FFmpeg vendor root is unsafe: ${vendorRoot}`);
|
|
}
|
|
await rm(vendorRoot, { force: true, recursive: true });
|
|
|
|
for (const asset of lock.assets) {
|
|
const relativeAssetPath = assertSafeRelativePath(asset.path);
|
|
const bytes = uncompressed[relativeAssetPath];
|
|
if (
|
|
!bytes ||
|
|
bytes.byteLength < 1 ||
|
|
bytes.byteLength > maximumAssetBytes ||
|
|
bytes.byteLength !== asset.bytes ||
|
|
sha256(bytes) !== asset.sha256
|
|
) {
|
|
throw new Error(
|
|
`Reviewed FFmpeg core entry failed locked verification: ${asset.path}`
|
|
);
|
|
}
|
|
const destination = path.join(outputRoot, ...relativeAssetPath.split('/'));
|
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
await writeFile(destination, bytes, { flag: 'wx', mode: 0o644 });
|
|
}
|
|
|
|
const versionManifest = {
|
|
schemaVersion: 2,
|
|
buildId: lock.buildId,
|
|
profile: lock.profile,
|
|
coreVersion: lock.coreVersion,
|
|
ffmpegVersion: lock.ffmpegVersion,
|
|
sourceArchive: lock.sourceArchive,
|
|
sourceLock: lock.sourceLock,
|
|
buildDriver: lock.buildDriver,
|
|
toolchain: lock.toolchain,
|
|
patch: lock.patch,
|
|
opusRegression: lock.opusRegression,
|
|
assets: lock.assets,
|
|
};
|
|
await writeFile(
|
|
path.join(outputRoot, 'version.json'),
|
|
`${JSON.stringify(versionManifest, null, 2)}\n`,
|
|
{ flag: 'wx', mode: 0o644 }
|
|
);
|
|
|
|
console.log(
|
|
`Verified and unpacked ${lock.assets.length} reviewed FFmpeg assets to ${path.relative(repositoryRoot, outputRoot)}`
|
|
);
|