feat: publish av-tools 0.2.0
This commit is contained in:
279
scripts/package-reviewed-ffmpeg-core.mjs
Normal file
279
scripts/package-reviewed-ffmpeg-core.mjs
Normal file
@@ -0,0 +1,279 @@
|
||||
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})`
|
||||
);
|
||||
Reference in New Issue
Block a user