Files
av-tools/scripts/package-reviewed-ffmpeg-core.mjs

280 lines
8.6 KiB
JavaScript

import { createHash } from 'node:crypto';
import {
lstat,
mkdir,
mkdtemp,
readFile,
realpath,
rename,
rm,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { unzipSync, zipSync } from 'fflate';
const repositoryRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const lockPath = path.join(
repositoryRoot,
'scripts',
'reviewed-ffmpeg-core-lock.json'
);
const maximumAssetBytes = 64 * 1024 * 1024;
const fixedTimestamp = new Date(1980, 0, 1, 0, 0, 0, 0);
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}`);
}
}
async function readLock() {
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' ||
!Number.isSafeInteger(lock.sourceArchive?.bytes) ||
!/^[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 ?? '') ||
!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)
) ||
typeof lock.artifact?.path !== 'string' ||
!Number.isSafeInteger(lock.artifact?.bytes) ||
lock.artifact.bytes < 1 ||
lock.artifact.bytes > 64 * 1024 * 1024 ||
!/^[a-f0-9]{64}$/.test(lock.artifact?.sha256 ?? '')
) {
throw new Error(`Invalid reviewed FFmpeg core lock: ${lockPath}`);
}
return lock;
}
async function readBuildAsset(asset) {
const relativeAssetPath = assertSafeRelativePath(asset.path);
const [mode, fileName, ...extra] = relativeAssetPath.split('/');
if (
extra.length > 0 ||
!['st', 'mt'].includes(mode) ||
!['ffmpeg-core.js', 'ffmpeg-core.wasm', 'ffmpeg-core.worker.js'].includes(
fileName
)
) {
throw new Error(`Unexpected reviewed-core asset: ${relativeAssetPath}`);
}
const outputDirectory = path.join(
repositoryRoot,
'.core-build',
'reviewed-stack5m',
mode,
'output'
);
const source = path.join(outputDirectory, fileName);
const details = await lstat(source);
if (details.isSymbolicLink() || !details.isFile()) {
throw new Error(`Reviewed-core input must be a regular file: ${source}`);
}
const canonicalOutput = await realpath(outputDirectory);
const canonicalSource = await realpath(source);
if (!canonicalSource.startsWith(`${canonicalOutput}${path.sep}`)) {
throw new Error(`Reviewed-core input resolves outside output: ${source}`);
}
const bytes = await readFile(canonicalSource);
if (
bytes.byteLength < 1 ||
bytes.byteLength > maximumAssetBytes ||
bytes.byteLength !== asset.bytes ||
sha256(bytes) !== asset.sha256
) {
throw new Error(`Reviewed-core build does not match lock: ${asset.path}`);
}
return bytes;
}
function verifyArchiveEntries(archive, lock) {
const lockedEntries = new Map(
lock.assets.map((asset) => [asset.path, asset])
);
const seen = new Set();
const uncompressed = unzipSync(archive, {
filter: (entry) => {
const relative = assertSafeRelativePath(entry.name);
const locked = lockedEntries.get(relative);
if (
!locked ||
seen.has(relative) ||
entry.originalSize !== locked.bytes ||
entry.originalSize < 1 ||
entry.originalSize > maximumAssetBytes ||
entry.size < 1 ||
entry.size > 64 * 1024 * 1024
) {
throw new Error(
`Reviewed-core archive contains an unexpected entry: ${entry.name}`
);
}
seen.add(relative);
return true;
},
});
if (seen.size !== lockedEntries.size) {
throw new Error('Reviewed-core archive is missing an expected entry.');
}
for (const asset of lock.assets) {
const bytes = uncompressed[asset.path];
if (
!bytes ||
bytes.byteLength !== asset.bytes ||
sha256(bytes) !== asset.sha256
) {
throw new Error(
`Reviewed-core archive entry does not match lock: ${asset.path}`
);
}
}
}
const lock = await readLock();
const buildDriver = await readFile(
path.join(repositoryRoot, ...lock.buildDriver.path.split('/'))
);
if (sha256(buildDriver) !== lock.buildDriver.sha256) {
throw new Error(
'Reviewed-core build driver does not match its locked identity.'
);
}
const sourceLock = await readFile(
path.join(repositoryRoot, ...lock.sourceLock.path.split('/'))
);
if (sha256(sourceLock) !== lock.sourceLock.sha256) {
throw new Error('FFmpeg source lock does not match its locked identity.');
}
const sourceArchive = await readFile(
path.join(repositoryRoot, ...lock.sourceArchive.path.split('/'))
);
if (
sourceArchive.byteLength !== lock.sourceArchive.bytes ||
sha256(sourceArchive) !== lock.sourceArchive.sha256
) {
throw new Error(
'Reviewed-core Corresponding Source archive does not match its locked identity.'
);
}
const artifactRelativePath = assertSafeRelativePath(lock.artifact.path);
const expectedArtifactRelativePath = `vendor/ffmpeg-core-${lock.buildId}.zip`;
if (artifactRelativePath !== expectedArtifactRelativePath) {
throw new Error(
`Reviewed-core artifact path must be ${expectedArtifactRelativePath}.`
);
}
const artifactPath = path.join(
repositoryRoot,
...artifactRelativePath.split('/')
);
const entries = {};
for (const asset of [...lock.assets].sort((left, right) =>
left.path.localeCompare(right.path, 'en')
)) {
entries[asset.path] = [
await readBuildAsset(asset),
{ level: 9, mtime: fixedTimestamp },
];
}
const archive = Buffer.from(zipSync(entries, { level: 9 }));
verifyArchiveEntries(archive, lock);
const archiveDigest = sha256(archive);
if (
archive.byteLength !== lock.artifact.bytes ||
archiveDigest !== lock.artifact.sha256
) {
throw new Error(
`Deterministic reviewed-core archive does not match its locked identity: received ${String(archive.byteLength)} bytes/${archiveDigest}.`
);
}
const artifactDirectory = path.dirname(artifactPath);
await requireRealDirectory(repositoryRoot);
await requireRealDirectory(artifactDirectory, true);
const stagingRoot = await mkdtemp(
path.join(artifactDirectory, '.reviewed-core-package-')
);
if (
!stagingRoot.startsWith(
`${path.join(artifactDirectory, '.reviewed-core-package-')}`
)
) {
throw new Error(`Unexpected reviewed-core staging path: ${stagingRoot}`);
}
try {
const stagedArtifact = path.join(stagingRoot, path.basename(artifactPath));
await writeFile(stagedArtifact, archive, { flag: 'wx', mode: 0o644 });
await rename(stagedArtifact, artifactPath);
} finally {
await rm(stagingRoot, { force: true, recursive: true });
}
console.log(
`Packaged ${lock.assets.length} reviewed FFmpeg assets as ${artifactRelativePath} (${archive.byteLength} bytes, ${archiveDigest})`
);