feat: publish av-tools 0.2.0
This commit is contained in:
@@ -1,80 +1,146 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { lstat, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const expectedVersion = '0.12.10';
|
||||
const outputRoot = join(
|
||||
const repositoryRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..'
|
||||
);
|
||||
const argumentsList = process.argv.slice(2);
|
||||
const runtimeOnly = argumentsList.includes('--runtime-only');
|
||||
if (
|
||||
argumentsList.some((argument) => argument !== '--runtime-only') ||
|
||||
(runtimeOnly && argumentsList.length !== 1)
|
||||
) {
|
||||
throw new Error(
|
||||
'Usage: node scripts/verify-ffmpeg-assets.mjs [--runtime-only]'
|
||||
);
|
||||
}
|
||||
const lock = JSON.parse(
|
||||
await readFile(
|
||||
path.join(repositoryRoot, 'scripts', 'reviewed-ffmpeg-core-lock.json'),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
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) ||
|
||||
lock.sourceArchive.bytes < 1 ||
|
||||
lock.sourceArchive.bytes > 512 * 1024 * 1024 ||
|
||||
!/^[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.artifact?.path !== `vendor/ffmpeg-core-${lock.buildId}.zip` ||
|
||||
!Number.isSafeInteger(lock.artifact?.bytes) ||
|
||||
lock.artifact.bytes < 1 ||
|
||||
lock.artifact.bytes > 64 * 1024 * 1024 ||
|
||||
!/^[a-f0-9]{64}$/.test(lock.artifact?.sha256 ?? '') ||
|
||||
!Array.isArray(lock.assets) ||
|
||||
lock.assets.length !== 5
|
||||
) {
|
||||
throw new Error('Invalid reviewed FFmpeg core lock.');
|
||||
}
|
||||
const outputRoot = path.join(
|
||||
repositoryRoot,
|
||||
'public',
|
||||
'vendor',
|
||||
'ffmpeg',
|
||||
expectedVersion
|
||||
lock.buildId
|
||||
);
|
||||
const manifestPath = join(outputRoot, 'version.json');
|
||||
const manifestPath = path.join(outputRoot, 'version.json');
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
||||
const safeAssetPath = (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 FFmpeg asset path: ${String(value)}`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const verifyLockedFile = async (record, label) => {
|
||||
const relative = safeAssetPath(record.path);
|
||||
const absolute = path.join(repositoryRoot, ...relative.split('/'));
|
||||
const details = await lstat(absolute).catch((error) => {
|
||||
throw new Error(`${label} is missing: ${relative}`, { cause: error });
|
||||
});
|
||||
if (details.isSymbolicLink() || !details.isFile()) {
|
||||
throw new Error(`${label} must be a regular file: ${relative}`);
|
||||
}
|
||||
const bytes = await readFile(absolute);
|
||||
if (
|
||||
(record.bytes !== undefined && bytes.byteLength !== record.bytes) ||
|
||||
sha256(bytes) !== record.sha256
|
||||
) {
|
||||
throw new Error(`${label} does not match its locked identity: ${relative}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
manifest.schemaVersion !== 1 ||
|
||||
manifest.coreVersion !== expectedVersion ||
|
||||
manifest.schemaVersion !== 2 ||
|
||||
manifest.buildId !== lock.buildId ||
|
||||
manifest.profile !== lock.profile ||
|
||||
manifest.coreVersion !== lock.coreVersion ||
|
||||
manifest.ffmpegVersion !== lock.ffmpegVersion ||
|
||||
!isDeepStrictEqual(manifest.sourceArchive, lock.sourceArchive) ||
|
||||
!isDeepStrictEqual(manifest.sourceLock, lock.sourceLock) ||
|
||||
!isDeepStrictEqual(manifest.buildDriver, lock.buildDriver) ||
|
||||
!isDeepStrictEqual(manifest.toolchain, lock.toolchain) ||
|
||||
!isDeepStrictEqual(manifest.patch, lock.patch) ||
|
||||
!isDeepStrictEqual(manifest.opusRegression, lock.opusRegression) ||
|
||||
!Array.isArray(manifest.assets) ||
|
||||
manifest.assets.length !== 5
|
||||
manifest.assets.length !== lock.assets.length
|
||||
) {
|
||||
throw new Error(`Invalid FFmpeg asset manifest: ${manifestPath}`);
|
||||
throw new Error(`Invalid reviewed FFmpeg asset manifest: ${manifestPath}`);
|
||||
}
|
||||
|
||||
const expectedPaths = new Set([
|
||||
'mt/ffmpeg-core.js',
|
||||
'mt/ffmpeg-core.wasm',
|
||||
'mt/ffmpeg-core.worker.js',
|
||||
'st/ffmpeg-core.js',
|
||||
'st/ffmpeg-core.wasm',
|
||||
]);
|
||||
const expectedHashes = new Map([
|
||||
[
|
||||
'st/ffmpeg-core.js',
|
||||
'67a48f11645f85439f3fde4f2119042c16b374b910206b7a7a24f342e28dcae3',
|
||||
],
|
||||
[
|
||||
'st/ffmpeg-core.wasm',
|
||||
'9f57947a5bd530d8f00c5b3f2cb2a3492faa7e5d823315342d6a8656d0a6b7b7',
|
||||
],
|
||||
[
|
||||
'mt/ffmpeg-core.js',
|
||||
'270a2e6ff945e173238610669a3f7132df5f9c52698a9bf708cf5c2ab6bda0de',
|
||||
],
|
||||
[
|
||||
'mt/ffmpeg-core.wasm',
|
||||
'be2c97605366b78f3f13e21b52e81a55a79e1f29c133b03a68ec187b1a2ec41a',
|
||||
],
|
||||
[
|
||||
'mt/ffmpeg-core.worker.js',
|
||||
'f77898d631dc010b45c29c23cb4379c611a7d7b131bf591d08a656bb729a4ca3',
|
||||
],
|
||||
]);
|
||||
if (!runtimeOnly) {
|
||||
await verifyLockedFile(lock.sourceArchive, 'FFmpeg Corresponding Source');
|
||||
}
|
||||
await verifyLockedFile(lock.sourceLock, 'FFmpeg source lock');
|
||||
await verifyLockedFile(lock.buildDriver, 'Reviewed-core build driver');
|
||||
await verifyLockedFile(lock.artifact, 'Reviewed-core deterministic artifact');
|
||||
|
||||
const lockedAssets = new Map(lock.assets.map((asset) => [asset.path, asset]));
|
||||
const seen = new Set();
|
||||
for (const asset of manifest.assets) {
|
||||
if (!expectedPaths.delete(asset.path)) {
|
||||
throw new Error(`Unexpected or duplicate FFmpeg asset: ${asset.path}`);
|
||||
}
|
||||
const bytes = await readFile(join(outputRoot, asset.path));
|
||||
const digest = createHash('sha256').update(bytes).digest('hex');
|
||||
safeAssetPath(asset.path);
|
||||
const locked = lockedAssets.get(asset.path);
|
||||
if (
|
||||
asset.bytes !== bytes.byteLength ||
|
||||
asset.sha256 !== digest ||
|
||||
expectedHashes.get(asset.path) !== digest
|
||||
!locked ||
|
||||
seen.has(asset.path) ||
|
||||
asset.bytes !== locked.bytes ||
|
||||
asset.sha256 !== locked.sha256
|
||||
) {
|
||||
throw new Error(`Unexpected reviewed FFmpeg asset: ${asset.path}`);
|
||||
}
|
||||
seen.add(asset.path);
|
||||
const bytes = await readFile(path.join(outputRoot, ...asset.path.split('/')));
|
||||
if (bytes.byteLength !== locked.bytes || sha256(bytes) !== locked.sha256) {
|
||||
throw new Error(`FFmpeg asset verification failed: ${asset.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedPaths.size > 0) {
|
||||
throw new Error(
|
||||
`FFmpeg assets are missing: ${[...expectedPaths].join(', ')}`
|
||||
);
|
||||
if (seen.size !== lockedAssets.size) {
|
||||
throw new Error('One or more reviewed FFmpeg assets are missing.');
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Verified 5 FFmpeg ${expectedVersion} assets in ${relative(repositoryRoot, outputRoot)}`
|
||||
`Verified ${seen.size} FFmpeg ${lock.buildId} assets in ${path.relative(repositoryRoot, outputRoot)}${runtimeOnly ? ' (runtime inputs only)' : ' with Corresponding Source'}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user