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

@@ -0,0 +1,420 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/build-reviewed-ffmpeg-core.sh st|mt SOURCE_ARCHIVE EMSDK_ROOT [JOBS]
Builds one private, staged FFmpeg WebAssembly core from the exact
0.12.10 Corresponding Source archive. The only upstream source change is
ffmpeg.wasm commit b409e36475bc21f0451b5b1e1d126fa82871439a:
`-sSTACK_SIZE=5MB`, which fixes upstream issues #591 and #775.
Output remains below .core-build/reviewed-stack5m and is never copied into the
application automatically. Re-running resumes at verified component stamps.
EOF
}
if [[ $# -lt 3 || $# -gt 4 ]]; then
usage >&2
exit 2
fi
MODE=$1
SOURCE_ARCHIVE=$2
EMSDK_ROOT=$3
JOBS=${4:-2}
if [[ "$MODE" != "st" && "$MODE" != "mt" ]]; then
echo "Mode must be st or mt." >&2
exit 2
fi
if [[ ! "$JOBS" =~ ^[1-8]$ ]]; then
echo "JOBS must be an integer from 1 to 8." >&2
exit 2
fi
SCRIPT_DIRECTORY=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
REPOSITORY_ROOT=$(cd -- "$SCRIPT_DIRECTORY/.." && pwd -P)
SOURCE_ARCHIVE=$(realpath -- "$SOURCE_ARCHIVE")
EMSDK_ROOT=$(realpath -- "$EMSDK_ROOT")
EXPECTED_ARCHIVE_SHA256=2ab505f72be883d1257bc71cbb93880d1ade5bc029792621cb87b4ecc068399a
EXPECTED_EMCC_SHA256=b0c9551f9155fa9c028efa24591acdfd30f02f32fb247cf9266f5f0f2ede7589
EXPECTED_CMAKE_SHA256=ade9326f83f4996b17e2cb1bbfe4848df30a237ef5769cd3c00239b9f0d3ec58
EXPECTED_SOURCE_ROOT=ffmpeg-core-0.12.10-reviewed-stack5m.1-corresponding-source
SOURCE_DATE_EPOCH=1736208000
if [[ ! -f "$SOURCE_ARCHIVE" || -L "$SOURCE_ARCHIVE" ]]; then
echo "SOURCE_ARCHIVE must be a regular, non-symlink file." >&2
exit 2
fi
if [[ ! -d "$EMSDK_ROOT" || -L "$EMSDK_ROOT" ]]; then
echo "EMSDK_ROOT must be a real directory." >&2
exit 2
fi
actual_archive_sha256=$(sha256sum -- "$SOURCE_ARCHIVE" | cut -d' ' -f1)
if [[ "$actual_archive_sha256" != "$EXPECTED_ARCHIVE_SHA256" ]]; then
echo "Corresponding Source archive SHA-256 mismatch." >&2
exit 1
fi
# shellcheck disable=SC1091
source "$EMSDK_ROOT/emsdk_env.sh" >/dev/null
export PATH="$REPOSITORY_ROOT/scripts/build-tools:$PATH"
if [[ -z "${AV_REVIEWED_CMAKE_BIN:-}" ]]; then
echo "AV_REVIEWED_CMAKE_BIN must identify the exact CMake 3.27.9 binary directory." >&2
exit 2
fi
AV_REVIEWED_CMAKE_BIN=$(realpath -- "$AV_REVIEWED_CMAKE_BIN")
if [[ ! -d "$AV_REVIEWED_CMAKE_BIN" || -L "$AV_REVIEWED_CMAKE_BIN" ]]; then
echo "AV_REVIEWED_CMAKE_BIN must be a real directory." >&2
exit 2
fi
if [[ $(sha256sum -- "$AV_REVIEWED_CMAKE_BIN/cmake" | cut -d' ' -f1) != "$EXPECTED_CMAKE_SHA256" ]]; then
echo "CMake binary SHA-256 mismatch; exact PyPI CMake 3.27.9 is required." >&2
exit 1
fi
export PATH="$AV_REVIEWED_CMAKE_BIN:$PATH"
EMCC=$(realpath -- "$EMSDK_ROOT/upstream/emscripten/emcc")
actual_emcc_sha256=$(sha256sum -- "$EMCC" | cut -d' ' -f1)
if [[ "$actual_emcc_sha256" != "$EXPECTED_EMCC_SHA256" ]]; then
echo "Emscripten compiler SHA-256 mismatch; activate exact version 3.1.40." >&2
exit 1
fi
if ! "$EMCC" --version | head -1 | grep -Fq \
"3.1.40 (5c27e79dd0a9c4e27ef2326841698cdd4f6b5784)"; then
echo "Emscripten identity mismatch; exact version 3.1.40 is required." >&2
exit 1
fi
if [[ $(cmake --version | head -1) != "cmake version 3.27.9" ]]; then
echo "CMake identity mismatch; exact version 3.27.9 is required." >&2
exit 1
fi
BUILD_PARENT="$REPOSITORY_ROOT/.core-build/reviewed-stack5m"
MODE_ROOT="$BUILD_PARENT/$MODE"
EXTRACT_ROOT="$MODE_ROOT/source"
SOURCE_ROOT="$EXTRACT_ROOT/$EXPECTED_SOURCE_ROOT/sources"
INSTALL_DIR="$MODE_ROOT/install"
OUTPUT_DIR="$MODE_ROOT/output"
STAMP_DIR="$MODE_ROOT/stamps"
case "$MODE_ROOT" in
"$REPOSITORY_ROOT"/.core-build/reviewed-stack5m/st | \
"$REPOSITORY_ROOT"/.core-build/reviewed-stack5m/mt) ;;
*)
echo "Refusing an unexpected build directory: $MODE_ROOT" >&2
exit 1
;;
esac
mkdir -p -- "$MODE_ROOT" "$STAMP_DIR"
if [[ ! -f "$STAMP_DIR/source-prepared" ]]; then
if [[ -e "$EXTRACT_ROOT" ]]; then
echo "Incomplete source staging already exists at $EXTRACT_ROOT." >&2
echo "Remove only that private mode directory to restart it." >&2
exit 1
fi
mkdir -p -- "$EXTRACT_ROOT"
tar -xf "$SOURCE_ARCHIVE" -C "$EXTRACT_ROOT"
lock="$EXTRACT_ROOT/$EXPECTED_SOURCE_ROOT/SOURCE_LOCK.json"
node -e '
const fs = require("node:fs");
const lock = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
const expected = new Map([
["ffmpeg.wasm", "71aa99d37c02a7b4c435275ca9ef50e612f6efa1"],
["FFmpeg", "4729204c17f756e186d622060088371d10b34f7e"],
["Opus", "e85ed7726db5d677c9c0677298ea0cb9c65bdd23"],
]);
for (const [name, revision] of expected) {
const entry = lock.sources.find((candidate) => candidate.name === name);
if (!entry || entry.revision !== revision) {
throw new Error(`Unexpected ${name} source identity.`);
}
}
' "$lock"
wasm_build_script="$SOURCE_ROOT/ffmpeg.wasm/build/ffmpeg-wasm.sh"
if grep -Fq -- "-sSTACK_SIZE=" "$wasm_build_script"; then
echo "Base archive unexpectedly already selects a stack size." >&2
exit 1
fi
sed -i \
'/-sUSE_SDL=2/a\ -sSTACK_SIZE=5MB # upstream b409e364: support libopus' \
"$wasm_build_script"
if [[ $(grep -Fc -- "-sSTACK_SIZE=5MB" "$wasm_build_script") -ne 1 ]]; then
echo "Could not apply the reviewed one-line stack patch exactly once." >&2
exit 1
fi
touch "$STAMP_DIR/source-prepared"
fi
# The upstream container uses unbounded `make -j`. Keep local builds bounded
# without changing compiled source, and allow a resumed build to select a new
# bounded job count.
while IFS= read -r -d '' build_script; do
sed -E -i \
-e "s/ -j([1-8])?$/ -j$JOBS/" \
-e "s/ -j([1-8])? / -j$JOBS /g" \
"$build_script"
done < <(find "$SOURCE_ROOT/ffmpeg.wasm/build" -type f -name '*.sh' -print0)
# The upstream container does not provide native NASM. Some development hosts
# do; allowing CMake to discover it would produce native x86 objects that
# cannot be linked into WebAssembly. Make the container's effective choice
# explicit and deterministic.
X265_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/x265.sh"
if ! grep -Fq -- "-DENABLE_ASSEMBLY=OFF" "$X265_BUILD_SCRIPT"; then
sed -i \
'/-DENABLE_LIBNUMA=OFF/a\ -DENABLE_ASSEMBLY=OFF' \
"$X265_BUILD_SCRIPT"
fi
if [[ $(grep -Fc -- "-DENABLE_ASSEMBLY=OFF" "$X265_BUILD_SCRIPT") -ne 1 ]]; then
echo "Could not disable native x265 assembly exactly once." >&2
exit 1
fi
# Upstream builds each dependency in an isolated container stage. A cumulative
# local prefix would otherwise let FreeType discover the already-built zlib and
# let HarfBuzz discover FreeType. Their resulting private dependencies are not
# present in FFmpeg's pkg-config probes and, on a developer workstation, optional
# libraries such as libpng may even resolve to host-native /usr objects. Make the
# isolated-stage dependency choices explicit.
FREETYPE_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/freetype2.sh"
if ! grep -Fq -- "--with-zlib=no" "$FREETYPE_BUILD_SCRIPT"; then
sed -i \
'/--without-harfbuzz/a\ --with-zlib=no\n --with-bzip2=no\n --with-png=no\n --with-brotli=no' \
"$FREETYPE_BUILD_SCRIPT"
fi
for freetype_flag in \
"--without-harfbuzz" \
"--with-zlib=no" \
"--with-bzip2=no" \
"--with-png=no" \
"--with-brotli=no"; do
if [[ $(grep -Fc -- "$freetype_flag" "$FREETYPE_BUILD_SCRIPT") -ne 1 ]]; then
echo "Could not select isolated FreeType dependency flags exactly once." >&2
exit 1
fi
done
HARFBUZZ_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/harfbuzz.sh"
for harfbuzz_flag in \
"--with-freetype=no" \
"--with-glib=no" \
"--with-gobject=no" \
"--with-cairo=no" \
"--with-icu=no" \
"--with-graphite2=no" \
"--with-uniscribe=no" \
"--with-directwrite=no" \
"--with-coretext=no"; do
if ! grep -Fq -- "$harfbuzz_flag" "$HARFBUZZ_BUILD_SCRIPT"; then
sed -i \
"/--enable-static/a\\ $harfbuzz_flag" \
"$HARFBUZZ_BUILD_SCRIPT"
fi
if [[ $(grep -Fc -- "$harfbuzz_flag" "$HARFBUZZ_BUILD_SCRIPT") -ne 1 ]]; then
echo "Could not select isolated HarfBuzz dependency flags exactly once." >&2
exit 1
fi
done
mkdir -p -- "$INSTALL_DIR" "$OUTPUT_DIR"
export INSTALL_DIR
export EM_PKG_CONFIG_PATH="$EMSDK/upstream/emscripten/system/lib/pkgconfig:$INSTALL_DIR/lib/pkgconfig"
export EM_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
export PKG_CONFIG_PATH="$INSTALL_DIR/lib/pkgconfig:$EM_PKG_CONFIG_PATH"
export LANG=C
export LC_ALL=C
export TZ=UTC
export SOURCE_DATE_EPOCH
export ZERO_AR_DATE=1
MODE_FLAGS="-O3 -msimd128"
if [[ "$MODE" == "st" ]]; then
export FFMPEG_ST=yes
unset FFMPEG_MT || true
else
MODE_FLAGS="$MODE_FLAGS -sUSE_PTHREADS -pthread"
# HarfBuzz's pinned upstream build script reads this value directly under
# `set -u`, while the other scripts use its presence to disable threading.
# An exported empty value keeps both behaviours correct for the MT build.
export FFMPEG_ST=
export FFMPEG_MT=yes
fi
export CFLAGS="-I$INSTALL_DIR/include $MODE_FLAGS"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="-L$INSTALL_DIR/lib $CFLAGS"
FFMPEG_WASM="$SOURCE_ROOT/ffmpeg.wasm"
build_component() {
local stamp=$1
local directory=$2
local script=$3
if [[ -f "$STAMP_DIR/$stamp" ]]; then
echo "[reviewed-core:$MODE] reuse $stamp"
return
fi
echo "[reviewed-core:$MODE] build $stamp"
(
cd -- "$SOURCE_ROOT/$directory"
if [[ "$stamp" == "libvpx" ]]; then
# libvpx embeds its configure command in the linked library. A relative
# prefix keeps that shipped identity independent of the checkout path
# while still installing into this mode's private prefix.
INSTALL_DIR=../../../../install \
CFLAGS="-I../../../../install/include $MODE_FLAGS" \
CXXFLAGS="-I../../../../install/include $MODE_FLAGS" \
LDFLAGS="-L../../../../install/lib -I../../../../install/include $MODE_FLAGS" \
bash "$FFMPEG_WASM/build/$script"
else
bash "$FFMPEG_WASM/build/$script"
fi
)
touch "$STAMP_DIR/$stamp"
}
if [[ ! -f "$STAMP_DIR/sdl2" ]]; then
if [[ "$MODE" == "st" ]]; then
embuilder build sdl2
else
embuilder build sdl2-mt
fi
touch "$STAMP_DIR/sdl2"
fi
build_component x264 x264 x264.sh
build_component x265 x265 x265.sh
build_component libvpx libvpx libvpx.sh
build_component lame lame lame.sh
build_component ogg ogg ogg.sh
build_component theora theora theora.sh
build_component opus opus opus.sh
build_component vorbis vorbis vorbis.sh
build_component zlib zlib zlib.sh
build_component libwebp libwebp libwebp.sh
build_component freetype2 freetype2 freetype2.sh
build_component fribidi fribidi fribidi.sh
build_component harfbuzz harfbuzz harfbuzz.sh
build_component libass libass libass.sh
build_component zimg zimg zimg.sh
# Configuration probes from Autotools projects must never smuggle host-native
# include/library paths into the reviewed WebAssembly link.
if grep -R -E -n -- \
'/usr/(lib|include)|/lib/(x86_64|i[3-6]86)-linux-gnu' \
"$INSTALL_DIR/lib/pkgconfig" "$INSTALL_DIR/lib"/*.la; then
echo "A staged dependency recorded a host-native build path." >&2
exit 1
fi
if emnm "$INSTALL_DIR/lib/libfreetype.a" |
grep -F -- " U png_" >/dev/null; then
echo "FreeType unexpectedly retained an unbundled libpng dependency." >&2
exit 1
fi
if emnm "$INSTALL_DIR/lib/libfreetype.a" |
grep -E -- ' U (inflate|BZ2_|Brotli)' >/dev/null; then
echo "FreeType unexpectedly retained a cross-stage compression dependency." >&2
exit 1
fi
FFMPEG_SOURCE="$SOURCE_ROOT/FFmpeg"
# FFmpeg records --extra-cflags in `ffmpeg -buildconf`. Use a path relative to
# its source root so the shipped runtime identity is independent of the
# maintainer's checkout location. Dependency builds above retain their
# absolute private staging prefix; only the final FFmpeg configuration is
# embedded in the distributed core.
FFMPEG_INSTALL_RELATIVE=../../../../install
export CFLAGS="-I$FFMPEG_INSTALL_RELATIVE/include $MODE_FLAGS"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="-L$INSTALL_DIR/lib $CFLAGS"
if [[ ! -f "$STAMP_DIR/ffmpeg" ]]; then
echo "[reviewed-core:$MODE] build FFmpeg"
(
cd -- "$FFMPEG_SOURCE"
bash "$FFMPEG_WASM/build/ffmpeg.sh" \
--enable-gpl \
--enable-libx264 \
--enable-libx265 \
--enable-libvpx \
--enable-libmp3lame \
--enable-libtheora \
--enable-libvorbis \
--enable-libopus \
--enable-zlib \
--enable-libwebp \
--enable-libfreetype \
--enable-libfribidi \
--enable-libass \
--enable-libzimg
)
touch "$STAMP_DIR/ffmpeg"
fi
if [[ ! -f "$STAMP_DIR/core" ]]; then
echo "[reviewed-core:$MODE] link ffmpeg-core with 5 MiB stack"
mkdir -p -- "$FFMPEG_SOURCE/src"
cp -a -- "$FFMPEG_WASM/src/bind" "$FFMPEG_SOURCE/src/"
cp -a -- "$FFMPEG_WASM/src/fftools" "$FFMPEG_SOURCE/src/"
cp -- "$FFMPEG_WASM/build/ffmpeg-wasm.sh" "$FFMPEG_SOURCE/build-reviewed-core.sh"
(
cd -- "$FFMPEG_SOURCE"
bash ./build-reviewed-core.sh \
-lx264 \
-lx265 \
-lvpx \
-lmp3lame \
-logg \
-ltheora \
-lvorbis \
-lvorbisenc \
-lvorbisfile \
-lopus \
-lz \
-lwebpmux \
-lwebp \
-lsharpyuv \
-lfreetype \
-lfribidi \
-lharfbuzz \
-lass \
-lzimg \
-sEXPORT_ES6 \
-o "$OUTPUT_DIR/ffmpeg-core.js"
)
touch "$STAMP_DIR/core"
fi
expected_files=(ffmpeg-core.js ffmpeg-core.wasm)
if [[ "$MODE" == "mt" ]]; then
expected_files+=(ffmpeg-core.worker.js)
fi
for expected_file in "${expected_files[@]}"; do
if [[ ! -s "$OUTPUT_DIR/$expected_file" || -L "$OUTPUT_DIR/$expected_file" ]]; then
echo "Missing staged output: $OUTPUT_DIR/$expected_file" >&2
exit 1
fi
if grep -aFq -- "$REPOSITORY_ROOT" "$OUTPUT_DIR/$expected_file" ||
grep -aFq -- "$EMSDK_ROOT" "$OUTPUT_DIR/$expected_file"; then
echo "Staged output leaks a maintainer-local build path: $expected_file" >&2
exit 1
fi
done
(
cd -- "$OUTPUT_DIR"
sha256sum -- "${expected_files[@]}" >SHA256SUMS
)
echo "[reviewed-core:$MODE] staged outputs"
cat "$OUTPUT_DIR/SHA256SUMS"

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
#
# A deliberately small `install(1)` compatibility wrapper for build trees on
# filesystems that map every created file to a shared owner and reject chmod.
# It is used only inside the private reviewed-core build. The final asset pack
# is hash-verified, and executable permission bits are irrelevant to browser
# JavaScript/WebAssembly files.
set -euo pipefail
directory_mode=false
create_leading=false
target_directory=
operands=()
while [[ $# -gt 0 ]]; do
case "$1" in
-d)
directory_mode=true
shift
;;
-D)
create_leading=true
shift
;;
-c | -p | -s | -v)
shift
;;
-m | -o | -g)
if [[ $# -lt 2 ]]; then
echo "install wrapper: $1 requires a value" >&2
exit 2
fi
shift 2
;;
-m* | -o* | -g*)
shift
;;
-t)
if [[ $# -lt 2 ]]; then
echo "install wrapper: -t requires a directory" >&2
exit 2
fi
target_directory=$2
shift 2
;;
--)
shift
operands+=("$@")
break
;;
-*)
echo "install wrapper: unsupported option $1" >&2
exit 2
;;
*)
operands+=("$1")
shift
;;
esac
done
if $directory_mode; then
if [[ ${#operands[@]} -eq 0 ]]; then
echo "install wrapper: -d requires at least one directory" >&2
exit 2
fi
mkdir -p -- "${operands[@]}"
exit 0
fi
if [[ -n "$target_directory" ]]; then
if [[ ${#operands[@]} -eq 0 ]]; then
echo "install wrapper: -t requires at least one source" >&2
exit 2
fi
mkdir -p -- "$target_directory"
cp -f -- "${operands[@]}" "$target_directory/"
exit 0
fi
if [[ ${#operands[@]} -lt 2 ]]; then
echo "install wrapper: expected source and destination" >&2
exit 2
fi
destination=${operands[-1]}
unset 'operands[-1]'
if $create_leading; then
mkdir -p -- "$(dirname -- "$destination")"
fi
if [[ ${#operands[@]} -gt 1 || -d "$destination" ]]; then
mkdir -p -- "$destination"
cp -f -- "${operands[@]}" "$destination/"
else
cp -f -- "${operands[0]}" "$destination"
fi

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)}`
);

View File

@@ -1,7 +1,9 @@
# FFmpeg WebAssembly Corresponding Source
This archive accompanies the exact `@ffmpeg/core` and `@ffmpeg/core-mt`
0.12.10 object code distributed by av-tools 0.1.0.
This archive accompanies the reviewed single-thread and multithread FFmpeg
WebAssembly core profile distributed by av-tools. The profile retains the
upstream core version 0.12.10 and FFmpeg n5.1.4 identities, but it is rebuilt
from source rather than copied from the published `@ffmpeg/core` packages.
`SOURCE_LOCK.json` records the immutable Git commit for ffmpeg.wasm, FFmpeg and
every external library selected by the upstream v12.15 build. The source trees
@@ -18,10 +20,15 @@ The original build-control files are in `sources/ffmpeg.wasm/`, including:
- `build/ffmpeg-wasm.sh`;
- the other dependency scripts under `build/`.
The upstream build selects Emscripten SDK 3.1.40 and FFmpeg n5.1.4. av-tools
does not modify the five published JavaScript/WebAssembly core assets. Their
exact sizes, hashes and embedded configure arguments are recorded in the
application's `SOURCE.md` and `docs/FFMPEG_BUILD.md`.
The build uses Emscripten SDK 3.1.40 and FFmpeg n5.1.4. Its one source patch
applies upstream ffmpeg.wasm commit
`b409e36475bc21f0451b5b1e1d126fa82871439a`, which raises the Emscripten stack
to 5 MiB and fixes the libopus encode failure reported upstream. The tracked
`scripts/build-reviewed-ffmpeg-core.sh` driver in the matching av-tools source
release also isolates optional host dependencies and records the exact
toolchain identities. The generated assets' exact sizes, hashes and embedded
configure arguments are recorded in the application's `SOURCE.md`,
`scripts/reviewed-ffmpeg-core-lock.json` and `docs/FFMPEG_BUILD.md`.
The source archive preserves each component's own licence and copyright
notices. av-tools itself is distributed separately under
@@ -37,14 +44,11 @@ test "$(git -C source-check rev-parse FETCH_HEAD)" = "<revision>"
```
The included ffmpeg.wasm Dockerfile documents the historical upstream build,
but still names remote tags and branches. It is not a turnkey offline or
byte-for-byte reproducible build recipe. To reconstruct a pinned build,
adapt each remote `ADD` to use the corresponding bundled source tree. Replace
the zimg `git clone -b ... --recursive` step with the bundled `sources/zimg/`
tree, which already contains its pinned GoogleTest submodule at
`test/extra/googletest/`. Keep the included build scripts and Emscripten
version unchanged. The upstream targets are `make prd` for single-thread and
`make prd-mt` for multithread.
but still names remote tags and branches. For the distributed reviewed
profile, use this archive together with the matching av-tools source release
and invoke its `scripts/build-reviewed-ffmpeg-core.sh` driver. The bundled
`sources/zimg/` tree already contains its pinned GoogleTest submodule at
`test/extra/googletest/`.
General-purpose build tooling such as Git, Docker/BuildKit and the Emscripten
3.1.40 container image is not copied into this archive.

522
scripts/opus-regression.mjs Normal file
View File

@@ -0,0 +1,522 @@
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const SCRIPT_PATH = fileURLToPath(import.meta.url);
const REPOSITORY_ROOT = path.resolve(path.dirname(SCRIPT_PATH), '..');
const FIXTURE_RELATIVE_PATH = 'tests/fixtures/generated/tone.wav';
const FIXTURE_PATH = path.join(REPOSITORY_ROOT, FIXTURE_RELATIVE_PATH);
const EXPECTED_FIXTURE_BYTES = 32_102;
const EXPECTED_FIXTURE_SHA256 =
'0fd1f54d6be2f1cc7fd778775dd29cf184770d3716cf3394bf03ed54ac264c3c';
const DEFAULT_MODE_TIMEOUT_MILLISECONDS = 60_000;
const RESULT_PREFIX = 'OPUS_RESULT ';
const EVENT_PREFIX = 'OPUS_EVENT ';
const MODES = Object.freeze(['single-thread', 'multithread']);
const argumentsSet = new Set(process.argv.slice(2));
const option = (name) =>
process.argv
.slice(2)
.find((argument) => argument.startsWith(`--${name}=`))
?.slice(name.length + 3);
function boundedInteger(value, fallback, minimum, maximum) {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum
? parsed
: fallback;
}
function serializeError(error) {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
...(error.stack ? { stack: error.stack.split('\n').slice(0, 8) } : {}),
};
}
return { name: 'Error', message: String(error) };
}
function emit(prefix, value) {
process.stdout.write(`${prefix}${JSON.stringify(value)}\n`);
}
function parsePrefixedJsonLines(text, prefix) {
return text
.split(/\r?\n/u)
.filter((line) => line.startsWith(prefix))
.flatMap((line) => {
try {
return [JSON.parse(line.slice(prefix.length))];
} catch {
return [];
}
});
}
function appendBounded(current, chunk, maximumLength = 32_768) {
return `${current}${chunk}`.slice(-maximumLength);
}
function terminateProcessGroup(child) {
if (!child.pid) return;
try {
if (process.platform === 'win32') {
child.kill('SIGKILL');
} else {
process.kill(-child.pid, 'SIGKILL');
}
} catch {
child.kill('SIGKILL');
}
}
async function runIsolatedMode(mode, origin, timeoutMilliseconds) {
const startedAt = performance.now();
const child = spawn(
process.execPath,
[SCRIPT_PATH, '--child', `--mode=${mode}`, `--origin=${origin}`],
{
cwd: REPOSITORY_ROOT,
detached: process.platform !== 'win32',
stdio: ['ignore', 'pipe', 'pipe'],
}
);
let stdout = '';
let stderr = '';
let timedOut = false;
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout = appendBounded(stdout, chunk);
for (const event of parsePrefixedJsonLines(chunk, EVENT_PREFIX)) {
if (event.stage && event.stage !== 'core-log') {
process.stdout.write(`[opus:${mode}] ${event.stage}\n`);
}
}
});
child.stderr.on('data', (chunk) => {
stderr = appendBounded(stderr, chunk);
});
const timeout = setTimeout(() => {
timedOut = true;
terminateProcessGroup(child);
}, timeoutMilliseconds);
const { code, signal } = await new Promise((resolve) => {
child.once('close', (exitCode, exitSignal) => {
resolve({ code: exitCode, signal: exitSignal });
});
});
clearTimeout(timeout);
const elapsedMilliseconds = Math.round(performance.now() - startedAt);
const events = parsePrefixedJsonLines(stdout, EVENT_PREFIX);
const reported = parsePrefixedJsonLines(stdout, RESULT_PREFIX).at(-1);
if (reported) {
return {
...reported,
elapsedMilliseconds,
process: { exitCode: code, signal },
};
}
const coreReady = events.find((event) => event.stage === 'core-ready');
const lastStage = [...events]
.reverse()
.find((event) => event.stage && event.stage !== 'core-log');
const relevantLogs = events
.filter((event) => event.stage === 'core-log')
.slice(-16)
.map((event) => event.message);
return {
requestedMode: mode,
status: 'fail',
failure: timedOut ? 'timeout' : 'child-exited-without-result',
message: timedOut
? `The isolated browser did not finish within ${String(timeoutMilliseconds)} ms.`
: 'The isolated browser exited without a structured result.',
...(coreReady?.actualMode ? { actualMode: coreReady.actualMode } : {}),
...(coreReady?.ffmpegVersion
? { ffmpegVersion: coreReady.ffmpegVersion }
: {}),
...(coreReady?.capabilities
? { capabilities: coreReady.capabilities }
: {}),
...(lastStage?.stage ? { lastStage: lastStage.stage } : {}),
...(relevantLogs.length > 0 ? { relevantLogs } : {}),
...(stderr.trim() ? { stderr: stderr.trim().split('\n').slice(-12) } : {}),
elapsedMilliseconds,
process: { exitCode: code, signal },
};
}
async function fixtureIdentity() {
const [bytes, fixtureStat] = await Promise.all([
readFile(FIXTURE_PATH),
stat(FIXTURE_PATH),
]);
const sha256 = createHash('sha256').update(bytes).digest('hex');
if (
fixtureStat.size !== EXPECTED_FIXTURE_BYTES ||
sha256 !== EXPECTED_FIXTURE_SHA256
) {
throw new Error(
`Opus fixture identity mismatch: expected ${String(EXPECTED_FIXTURE_BYTES)} bytes/${EXPECTED_FIXTURE_SHA256}, received ${String(fixtureStat.size)} bytes/${sha256}.`
);
}
return {
path: FIXTURE_RELATIVE_PATH,
bytes: fixtureStat.size,
sha256,
};
}
async function runMain() {
const timeoutMilliseconds = boundedInteger(
option('timeout-ms') ?? process.env.AV_OPUS_REGRESSION_TIMEOUT_MS,
DEFAULT_MODE_TIMEOUT_MILLISECONDS,
10_000,
10 * 60_000
);
const [fixture, packageText, coreLockText] = await Promise.all([
fixtureIdentity(),
readFile(path.join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
readFile(
path.join(REPOSITORY_ROOT, 'scripts', 'reviewed-ffmpeg-core-lock.json'),
'utf8'
),
]);
const packageManifest = JSON.parse(packageText);
const coreLock = JSON.parse(coreLockText);
const { createServer } = await import('vite');
const server = await createServer({
configFile: path.join(REPOSITORY_ROOT, 'vite.config.ts'),
logLevel: 'warn',
server: {
host: '127.0.0.1',
port: 0,
strictPort: false,
},
});
try {
await server.listen();
const address = server.httpServer?.address();
if (!address || typeof address === 'string') {
throw new Error('Vite did not expose a local TCP address.');
}
const origin = `http://127.0.0.1:${String(address.port)}`;
const modes = [];
for (const mode of MODES) {
modes.push(await runIsolatedMode(mode, origin, timeoutMilliseconds));
}
const report = {
schemaVersion: 2,
recordedAt: new Date().toISOString(),
browser: 'chromium',
wrapperVersion: packageManifest.dependencies['@ffmpeg/ffmpeg'],
coreVersion: coreLock.coreVersion,
coreBuildId: coreLock.buildId,
coreProfile: coreLock.profile,
fixture,
perModeTimeoutMilliseconds: timeoutMilliseconds,
modes,
safe: modes.every((result) => result.status === 'pass'),
};
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
if (argumentsSet.has('--require-pass') && !report.safe) {
process.exitCode = 1;
}
} finally {
await server.close();
}
}
async function runBrowserMode(mode, origin) {
const { chromium } = await import('@playwright/test');
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.exposeFunction('__reportOpusEvent', (event) => {
emit(EVENT_PREFIX, event);
});
await page.goto(origin, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
return await page.evaluate(
async ({ fixtureUrl, requestedMode }) => {
const report = async (event) => {
await globalThis.__reportOpusEvent(event);
};
const replacePaths = (plan, context) => {
const replacements = new Map();
plan.inputs.forEach((input, index) => {
const actual = context.inputPaths[index];
if (actual) replacements.set(input.path, actual);
});
plan.outputs.forEach((output, index) => {
const actual = context.outputPaths[index];
if (actual) replacements.set(output.path, actual);
});
plan.temporaryFiles.forEach((temporary, index) => {
const actual = context.temporaryPaths[index];
if (actual) replacements.set(temporary.path, actual);
});
return plan.args.map((argument) =>
[...replacements].reduce(
(resolved, [virtualPath, actualPath]) =>
resolved.replaceAll(virtualPath, actualPath),
argument
)
);
};
const errorShape = (error) =>
error instanceof Error
? {
name: error.name,
message: error.message,
...(error.stack
? { stack: error.stack.split('\n').slice(0, 8) }
: {}),
}
: { name: 'Error', message: String(error) };
let manager;
const relevantLogs = [];
try {
await report({ stage: 'module-import' });
const [{ EngineManager }, { buildConvertPlan }, { AUDIO_PRESETS }] =
await Promise.all([
import('/src/ffmpeg/EngineManager.ts'),
import('/src/commands/convert.ts'),
import('/src/presets/audio-presets.ts'),
]);
manager = new EngineManager();
manager.setPreference(
requestedMode === 'single-thread'
? 'force-single-thread'
: 'prefer-multithread'
);
manager.onLog((event) => {
for (const line of event.message.split(/\r?\n/u)) {
if (
relevantLogs.length < 32 &&
/libopus|Output #|Stream mapping|Aborted\(\)|(?:^|\s)(?:size|progress)=|error|failed/iu.test(
line
)
) {
const message = line.slice(0, 600);
relevantLogs.push(message);
void report({ stage: 'core-log', message });
}
}
});
await report({ stage: 'fixture-fetch' });
const fixtureResponse = await fetch(fixtureUrl);
if (!fixtureResponse.ok) {
throw new Error(
`Fixture request failed with HTTP ${String(fixtureResponse.status)}.`
);
}
const fixtureBytes = new Uint8Array(
await fixtureResponse.arrayBuffer()
);
const sourceFile = new File([fixtureBytes], 'tone.wav', {
type: 'audio/wav',
});
await report({ stage: 'core-initializing' });
await manager.initialize();
const ready = manager.state;
if (ready.status !== 'ready') {
throw new Error('The FFmpeg engine did not reach ready state.');
}
const capabilities = {
opusMuxer: ready.capabilities.muxers.has('opus'),
libopusEncoder: ready.capabilities.encoders.has('libopus'),
};
await report({
stage: 'core-ready',
requestedMode,
actualMode: ready.mode,
ffmpegVersion: ready.capabilities.versionText,
capabilities,
});
if (ready.mode !== requestedMode) {
throw new Error(
`Requested ${requestedMode}, but the engine selected ${ready.mode}.`
);
}
if (!capabilities.opusMuxer || !capabilities.libopusEncoder) {
throw new Error(
'The pinned core does not advertise the Opus muxer and libopus encoder.'
);
}
const preset = AUDIO_PRESETS.find(
(candidate) => candidate.id === 'audio-opus'
);
if (!preset) {
throw new Error('The built-in audio-opus preset is missing.');
}
const plan = buildConvertPlan({
jobId: `opus-regression-${requestedMode}`,
source: {
id: 'tone',
sourceIndex: 0,
fileName: 'tone.wav',
extension: 'wav',
},
preset,
streamSelection: { audio: [0] },
expectedDurationSeconds: 1,
timeoutMs: 30_000,
});
await report({
stage: 'encode-started',
presetId: preset.id,
requirements: plan.requiredCapabilities,
});
let commandArguments = [];
const completed = await manager.runJob({
id: `opus-regression-${requestedMode}`,
operation: 'Opus regression',
coreWorkload: plan.operation,
inputs: [sourceFile],
outputs: plan.outputs.map((output) => ({
name: output.fileName,
mimeType: output.mediaType,
})),
expectedDurationSeconds: plan.expectedDurationSeconds,
timeoutMilliseconds: plan.timeoutMs,
buildArguments: (context) => {
commandArguments = replacePaths(plan, context);
void report({
stage: 'encode-command',
arguments: commandArguments,
});
return commandArguments;
},
});
const output = completed.outputs[0];
if (!output || output.bytes.byteLength === 0) {
throw new Error(
'The Opus encode did not produce non-empty output.'
);
}
await report({
stage: 'encode-finished',
outputBytes: output.bytes.byteLength,
});
const outputFile = new File([output.bytes], output.name, {
type: output.mimeType,
});
await report({ stage: 'reprobe-started' });
const probe = await manager.probe(outputFile, 15_000);
const probeJson = probe.json;
const streams = Array.isArray(probeJson?.streams)
? probeJson.streams
: [];
const opusStream = streams.find(
(stream) =>
stream?.codec_type === 'audio' && stream?.codec_name === 'opus'
);
const durationSeconds = Number(
probeJson?.format?.duration ?? opusStream?.duration
);
if (!opusStream || !(durationSeconds > 0)) {
throw new Error(
'The generated file did not re-probe as positive-duration Opus audio.'
);
}
const digest = new Uint8Array(
await crypto.subtle.digest('SHA-256', output.bytes)
);
const outputSha256 = [...digest]
.map((value) => value.toString(16).padStart(2, '0'))
.join('');
await report({ stage: 'reprobe-finished', durationSeconds });
return {
requestedMode,
actualMode: ready.mode,
status: 'pass',
ffmpegVersion: ready.capabilities.versionText,
capabilities,
presetId: preset.id,
commandArguments,
output: {
name: output.name,
mimeType: output.mimeType,
bytes: output.bytes.byteLength,
sha256: outputSha256,
codecName: opusStream.codec_name,
codecType: opusStream.codec_type,
durationSeconds,
},
relevantLogs,
};
} catch (error) {
const state = manager?.state;
return {
requestedMode,
...(state?.mode ? { actualMode: state.mode } : {}),
status: 'fail',
failure: 'browser-operation-failed',
error: errorShape(error),
relevantLogs,
};
} finally {
manager?.dispose();
}
},
{
fixtureUrl: `${origin}/${FIXTURE_RELATIVE_PATH}`,
requestedMode: mode,
}
);
} finally {
await browser.close();
}
}
async function runChild() {
const mode = option('mode');
const origin = option('origin');
if (!MODES.includes(mode) || !origin) {
throw new Error('Child mode requires valid --mode and --origin options.');
}
emit(EVENT_PREFIX, { stage: 'browser-launching', requestedMode: mode });
try {
const result = await runBrowserMode(mode, origin);
emit(RESULT_PREFIX, result);
} catch (error) {
emit(RESULT_PREFIX, {
requestedMode: mode,
status: 'fail',
failure: 'browser-process-failed',
error: serializeError(error),
});
}
}
try {
if (argumentsSet.has('--child')) {
await runChild();
} else {
await runMain();
}
} catch (error) {
process.stderr.write(
`Opus regression harness failed: ${JSON.stringify(serializeError(error))}\n`
);
process.exitCode = 2;
}

View File

@@ -18,6 +18,11 @@ import { createReadStream } from 'node:fs';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const lockPath = join(repositoryRoot, 'scripts', 'ffmpeg-source-lock.json');
const reviewedLockPath = join(
repositoryRoot,
'scripts',
'reviewed-ffmpeg-core-lock.json'
);
const readmePath = join(repositoryRoot, 'scripts', 'ffmpeg-source-README.md');
const releaseDirectory = join(repositoryRoot, 'release');
const force = process.argv.includes('--force');
@@ -204,7 +209,19 @@ async function publishSourcePair({
const lock = JSON.parse(await readFile(lockPath, 'utf8'));
assertLock(lock);
const archiveName = `ffmpeg-core-${lock.coreVersion}-corresponding-source.tar.xz`;
const reviewedLock = JSON.parse(await readFile(reviewedLockPath, 'utf8'));
if (
reviewedLock?.schemaVersion !== 1 ||
reviewedLock.coreVersion !== lock.coreVersion ||
typeof reviewedLock.buildId !== 'string' ||
!/^[a-zA-Z0-9._-]+$/u.test(reviewedLock.buildId) ||
!reviewedLock.buildId.startsWith(`${lock.coreVersion}-`)
) {
throw new Error(
'The reviewed-core lock does not identify a build-qualified source bundle.'
);
}
const archiveName = `ffmpeg-core-${reviewedLock.buildId}-corresponding-source.tar.xz`;
const archivePath = join(releaseDirectory, archiveName);
const checksumPath = `${archivePath}.sha256`;
await ensureReleaseDirectory();

View File

@@ -52,8 +52,6 @@ const ffmpegLicenseCoverage = new Map([
['@ffmpeg/ffmpeg', 'LICENSES/ffmpeg.wasm-MIT.txt'],
['@ffmpeg/util', 'LICENSES/ffmpeg.wasm-MIT.txt'],
['@ffmpeg/types', 'LICENSES/ffmpeg.wasm-MIT.txt'],
['@ffmpeg/core', 'LICENSES/GPL-2.0-or-later.txt'],
['@ffmpeg/core-mt', 'LICENSES/GPL-2.0-or-later.txt'],
]);
function compareText(left, right) {
@@ -479,7 +477,8 @@ function validateReleaseLayout(entries, packageJson, applicationLicense) {
}
}
const vendorManifestPath = 'vendor/ffmpeg/0.12.10/version.json';
const vendorBuildId = '0.12.10-reviewed-stack5m.1';
const vendorManifestPath = `vendor/ffmpeg/${vendorBuildId}/version.json`;
const vendorManifestEntry = entriesByName.get(vendorManifestPath);
if (!vendorManifestEntry) {
throw new Error(
@@ -488,10 +487,12 @@ function validateReleaseLayout(entries, packageJson, applicationLicense) {
}
const vendorManifest = JSON.parse(vendorManifestEntry.data.toString('utf8'));
if (
vendorManifest.schemaVersion !== 1 ||
vendorManifest.schemaVersion !== 2 ||
vendorManifest.buildId !== vendorBuildId ||
vendorManifest.profile !== 'reviewed-stack5m' ||
vendorManifest.coreVersion !== '0.12.10' ||
vendorManifest.packages?.['@ffmpeg/core'] !== '0.12.10' ||
vendorManifest.packages?.['@ffmpeg/core-mt'] !== '0.12.10' ||
vendorManifest.opusRegression?.singleThread !== true ||
vendorManifest.opusRegression?.multiThread !== true ||
!Array.isArray(vendorManifest.assets) ||
vendorManifest.assets.length !== 5
) {
@@ -511,7 +512,7 @@ function validateReleaseLayout(entries, packageJson, applicationLicense) {
);
}
const assetPath = safeArchivePath(
path.posix.join('vendor/ffmpeg/0.12.10', asset.path)
path.posix.join(`vendor/ffmpeg/${vendorBuildId}`, asset.path)
);
if (verifiedVendorPaths.has(assetPath)) {
throw new Error(`Duplicate FFmpeg verification path: ${assetPath}`);

View 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})`
);

View File

@@ -16,6 +16,7 @@ const repositoryRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const reviewedCoreBuildId = '0.12.10-reviewed-stack5m.1';
function consumeValue(argumentsList, index, option) {
const value = argumentsList[index + 1];
@@ -179,12 +180,21 @@ export function inspectPortalHeaders(configuration) {
'The config does not explicitly map `.wasm` to application/wasm; verify the pinned image mime.types response.'
);
}
if (
!configuration.includes('vendor/ffmpeg/0\\.12\\.10') &&
!configuration.includes('vendor/ffmpeg/0.12.10')
) {
const immutableCoreRule = configuration
.split(/\r?\n/u)
.find(
(line) =>
/vendor\/ffmpeg\//iu.test(line) &&
/ffmpeg-core/iu.test(line) &&
/max-age=31536000/iu.test(line) &&
/immutable/iu.test(line)
);
const supportsReviewedCore =
immutableCoreRule !== undefined &&
immutableCoreRule.replaceAll('\\', '').includes(reviewedCoreBuildId);
if (!supportsReviewedCore) {
warnings.push(
'The config does not give versioned FFmpeg core assets an explicit immutable cache rule.'
`The config does not give reviewed FFmpeg build ${reviewedCoreBuildId} an explicit immutable cache rule.`
);
}
return { errors, warnings };
@@ -229,11 +239,11 @@ async function verifyAssembly(output, appVersion, digest) {
}
for (const reference of [
'./index.html',
'./vendor/ffmpeg/0.12.10/st/ffmpeg-core.js',
'./vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm',
'./vendor/ffmpeg/0.12.10/mt/ffmpeg-core.js',
'./vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm',
'./vendor/ffmpeg/0.12.10/mt/ffmpeg-core.worker.js',
'./vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.js',
'./vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm',
'./vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.js',
'./vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.wasm',
'./vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.worker.js',
]) {
const relative = reference.replace(/^\.\//, '');
await requireRegularFile(

View File

@@ -0,0 +1,73 @@
{
"schemaVersion": 1,
"buildId": "0.12.10-reviewed-stack5m.1",
"profile": "reviewed-stack5m",
"coreVersion": "0.12.10",
"ffmpegVersion": "n5.1.4",
"sourceArchive": {
"path": "release/ffmpeg-core-0.12.10-reviewed-stack5m.1-corresponding-source.tar.xz",
"bytes": 43050052,
"sha256": "2ab505f72be883d1257bc71cbb93880d1ade5bc029792621cb87b4ecc068399a"
},
"sourceLock": {
"path": "scripts/ffmpeg-source-lock.json",
"sha256": "f521ecab59942f899b117e4570bf3a71710fcab6281fe6d796c9bdd22a1fc986"
},
"buildDriver": {
"path": "scripts/build-reviewed-ffmpeg-core.sh",
"sha256": "d3551fe12e7883191c41b922850d9bcf7ea093e21ceaa3dec99570db7ea25e2e"
},
"toolchain": {
"emscriptenVersion": "3.1.40",
"emscriptenCommit": "5c27e79dd0a9c4e27ef2326841698cdd4f6b5784",
"emccSha256": "b0c9551f9155fa9c028efa24591acdfd30f02f32fb247cf9266f5f0f2ede7589",
"cmakeVersion": "3.27.9",
"cmakeBinarySha256": "ade9326f83f4996b17e2cb1bbfe4848df30a237ef5769cd3c00239b9f0d3ec58"
},
"patch": {
"upstreamCommit": "b409e36475bc21f0451b5b1e1d126fa82871439a",
"stackSizeBytes": 5242880,
"change": "build/ffmpeg-wasm.sh: add -sSTACK_SIZE=5MB"
},
"opusRegression": {
"fixturePath": "tests/fixtures/generated/tone.wav",
"fixtureBytes": 32102,
"fixtureSha256": "0fd1f54d6be2f1cc7fd778775dd29cf184770d3716cf3394bf03ed54ac264c3c",
"presetId": "audio-opus",
"expectedCodec": "opus",
"singleThread": true,
"multiThread": true
},
"assets": [
{
"path": "st/ffmpeg-core.js",
"bytes": 111804,
"sha256": "80c05d79d0e4e9434977b76cb851d10ddf2bfd378570178700d2c43b8afdcb24"
},
{
"path": "st/ffmpeg-core.wasm",
"bytes": 32232580,
"sha256": "fe41ddc77220cef6c04f5d48eeffcaaaef179ad270642e098f9945013aa0c9dc"
},
{
"path": "mt/ffmpeg-core.js",
"bytes": 128953,
"sha256": "4f2650099ab70cb2583951c0421147c62bea6c18dfbba5cfae4d7698b5d0ab62"
},
{
"path": "mt/ffmpeg-core.wasm",
"bytes": 32718455,
"sha256": "abbead010cb0448b26f01a36a3c8c03a6b04aca07812ddd4719a7f4b6bf4b645"
},
{
"path": "mt/ffmpeg-core.worker.js",
"bytes": 2115,
"sha256": "f77898d631dc010b45c29c23cb4379c611a7d7b131bf591d08a656bb729a4ca3"
}
],
"artifact": {
"path": "vendor/ffmpeg-core-0.12.10-reviewed-stack5m.1.zip",
"bytes": 21338974,
"sha256": "ebc068c6d096de55ae3335b04b7121130665f5b0ffa7a828067122733f55185f"
}
}

View File

@@ -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'}`
);