feat: publish av-tools 0.2.0

This commit is contained in:
2026-07-27 01:04:21 +02:00
parent fcc537b024
commit 0a4548851c
68 changed files with 4130 additions and 605 deletions

View File

@@ -1,138 +1,203 @@
import { createHash } from 'node:crypto';
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
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 = join(dirname(fileURLToPath(import.meta.url)), '..');
const expectedVersion = '0.12.10';
const outputRoot = join(
repositoryRoot,
'public',
'vendor',
'ffmpeg',
expectedVersion
const repositoryRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const packages = [
{
packageName: '@ffmpeg/core',
mode: 'st',
files: ['ffmpeg-core.js', 'ffmpeg-core.wasm'],
},
{
packageName: '@ffmpeg/core-mt',
mode: 'mt',
files: ['ffmpeg-core.js', 'ffmpeg-core.wasm', 'ffmpeg-core.worker.js'],
},
];
const expectedHashes = new Map([
[
'@ffmpeg/core:ffmpeg-core.js',
'67a48f11645f85439f3fde4f2119042c16b374b910206b7a7a24f342e28dcae3',
],
[
'@ffmpeg/core:ffmpeg-core.wasm',
'9f57947a5bd530d8f00c5b3f2cb2a3492faa7e5d823315342d6a8656d0a6b7b7',
],
[
'@ffmpeg/core-mt:ffmpeg-core.js',
'270a2e6ff945e173238610669a3f7132df5f9c52698a9bf708cf5c2ab6bda0de',
],
[
'@ffmpeg/core-mt:ffmpeg-core.wasm',
'be2c97605366b78f3f13e21b52e81a55a79e1f29c133b03a68ec187b1a2ec41a',
],
[
'@ffmpeg/core-mt:ffmpeg-core.worker.js',
'f77898d631dc010b45c29c23cb4379c611a7d7b131bf591d08a656bb729a4ca3',
],
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',
]);
async function loadPackageVersion(packageName) {
const packagePath = join(
repositoryRoot,
'node_modules',
...packageName.split('/'),
'package.json'
);
const parsed = JSON.parse(await readFile(packagePath, 'utf8'));
if (parsed.version !== expectedVersion) {
throw new Error(
`${packageName} ${parsed.version ?? '<unknown>'} is installed; expected exactly ${expectedVersion}`
);
}
function sha256(bytes) {
return createHash('sha256').update(bytes).digest('hex');
}
async function copyAsset(packageName, mode, fileName) {
const source = join(
repositoryRoot,
'node_modules',
...packageName.split('/'),
'dist',
'esm',
fileName
);
const destination = join(outputRoot, mode, fileName);
const sourceBytes = await readFile(source).catch((error) => {
throw new Error(`Required FFmpeg asset is missing: ${source}`, {
cause: error,
});
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;
});
const sourceDigest = createHash('sha256').update(sourceBytes).digest('hex');
const expectedDigest = expectedHashes.get(`${packageName}:${fileName}`);
if (!expectedDigest || sourceDigest !== expectedDigest) {
throw new Error(
`Pinned FFmpeg asset checksum mismatch for ${packageName}/${fileName}`
);
if (!details && create) {
await mkdir(directory);
details = await lstat(directory);
}
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, {
dereference: true,
errorOnExist: false,
force: true,
preserveTimestamps: false,
});
const destinationBytes = await readFile(destination);
if (!sourceBytes.equals(destinationBytes)) {
throw new Error(`Copied FFmpeg asset differs from its source: ${fileName}`);
}
return {
path: relative(outputRoot, destination).replaceAll('\\', '/'),
bytes: destinationBytes.byteLength,
sha256: sourceDigest,
};
}
await rm(outputRoot, { force: true, recursive: true });
const assets = [];
for (const entry of packages) {
await loadPackageVersion(entry.packageName);
for (const fileName of entry.files) {
assets.push(await copyAsset(entry.packageName, entry.mode, fileName));
if (!details || details.isSymbolicLink() || !details.isDirectory()) {
throw new Error(`Required directory is unsafe: ${directory}`);
}
}
assets.sort((left, right) => left.path.localeCompare(right.path, 'en'));
const versionManifest = {
schemaVersion: 1,
coreVersion: expectedVersion,
packages: {
'@ffmpeg/core': expectedVersion,
'@ffmpeg/core-mt': expectedVersion,
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;
},
assets,
};
});
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(
join(outputRoot, 'version.json'),
`${JSON.stringify(versionManifest, null, 2)}\n`
path.join(outputRoot, 'version.json'),
`${JSON.stringify(versionManifest, null, 2)}\n`,
{ flag: 'wx', mode: 0o644 }
);
console.log(
`Copied ${assets.length} pinned FFmpeg assets to ${relative(repositoryRoot, outputRoot)}`
`Verified and unpacked ${lock.assets.length} reviewed FFmpeg assets to ${path.relative(repositoryRoot, outputRoot)}`
);