From c6495016fc1bf12b4e23229a7fe7099638651438 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:31:48 +1100 Subject: [PATCH] Keep verified FFmpeg downloads available in Recordly 2.0 part 6 --- electron/ffmpegDistribution.test.mjs | 149 ++++++++++++++++++++++++++- scripts/ffmpeg-distribution.mjs | 48 ++++++++- 2 files changed, 193 insertions(+), 4 deletions(-) diff --git a/electron/ffmpegDistribution.test.mjs b/electron/ffmpegDistribution.test.mjs index 60015f9b..fd18efe0 100644 --- a/electron/ffmpegDistribution.test.mjs +++ b/electron/ffmpegDistribution.test.mjs @@ -2,10 +2,12 @@ import { createHash } from "node:crypto"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { gzipSync } from "node:zlib"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { APPROVED_FFMPEG_BINARIES, assertRedistributableFfmpegOutput, + ensureRedistributableFfmpeg, verifyFfmpegDistribution, } from "../scripts/ffmpeg-distribution.mjs"; @@ -29,6 +31,8 @@ function fixture(overrides = {}) { return binary; } afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); for (const directory of fixtures.splice(0)) rmSync(directory, { recursive: true, force: true }); }); @@ -98,3 +102,146 @@ describe("FFmpeg redistribution checks", () => { ).toThrow("pinned redistributable sources"); }); }); + +const directRelease = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1"; +const invalidArtifact = gzipSync(Buffer.from("unapproved executable fixture")); +function installTarget(platform = "linux", arch = "x64") { + const root = mkdtempSync(path.join(os.tmpdir(), "recordly-ffmpeg-download-")); + fixtures.push(root); + return ensureRedistributableFfmpeg({ root, platform, arch }); +} +function artifactResponse() { + return new Response(invalidArtifact); +} + +describe("FFmpeg release transport", () => { + it("keeps a successful direct download and still rejects unapproved bytes", async () => { + const network = vi.fn().mockResolvedValue(artifactResponse()); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toThrow("pinned SHA-256 check"); + expect(network).toHaveBeenCalledOnce(); + expect(network.mock.calls[0][0]).toBe(`${directRelease}/ffmpeg-linux-x64.gz`); + expect(network.mock.calls[0][1]).toEqual({ + signal: expect.any(AbortSignal), + credentials: "omit", + }); + }); + + it.each([ + 500, 502, 503, 504, + ])("falls back once for retryable HTTP %i without accepting changed bytes", async (status) => { + const failed = new Response("temporarily unavailable", { status }); + const cancel = vi.spyOn(failed.body, "cancel"); + const network = vi + .fn() + .mockResolvedValueOnce(failed) + .mockResolvedValueOnce(artifactResponse()); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toThrow("pinned SHA-256 check"); + expect(cancel).toHaveBeenCalledOnce(); + expect(network).toHaveBeenCalledTimes(2); + expect(network.mock.calls[1]).toEqual([ + "https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/316528626", + { + signal: expect.any(AbortSignal), + credentials: "omit", + headers: { Accept: "application/octet-stream" }, + }, + ]); + }); + + it.each([ + ["linux", "arm64", 316528458], + ["win32", "x64", 316528798], + ])("uses the same pinned release asset for %s/%s", async (platform, arch, assetId) => { + const network = vi + .fn() + .mockResolvedValueOnce(new Response(null, { status: 504 })) + .mockResolvedValueOnce(artifactResponse()); + vi.stubGlobal("fetch", network); + await expect(installTarget(platform, arch)).rejects.toThrow("pinned SHA-256 check"); + expect(network.mock.calls.map(([url]) => url)).toEqual([ + `${directRelease}/ffmpeg-${platform}-${arch}.gz`, + `https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/${assetId}`, + ]); + }); + + it.each([ + 400, 401, 403, 404, 429, + ])("fails closed without fallback for permanent HTTP %i", async (status) => { + const network = vi.fn().mockResolvedValue(new Response(null, { status })); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toThrow(`download failed (${status})`); + expect(network).toHaveBeenCalledOnce(); + }); + + it("recovers a network exception through the exact same-provider asset API", async () => { + const network = vi + .fn() + .mockRejectedValueOnce(new TypeError("fetch failed")) + .mockResolvedValueOnce(artifactResponse()); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toThrow("pinned SHA-256 check"); + expect(network).toHaveBeenCalledTimes(2); + expect(network.mock.calls[1][0]).toBe( + "https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/316528626", + ); + }); + + it("does not retry programming failures as network outages", async () => { + const network = vi.fn().mockRejectedValue(new Error("unexpected failure")); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toThrow("unexpected failure"); + expect(network).toHaveBeenCalledOnce(); + }); + + it("stops after a failed API fallback rather than looping", async () => { + const network = vi + .fn() + .mockResolvedValueOnce(new Response(null, { status: 504 })) + .mockResolvedValueOnce(new Response(null, { status: 503 })); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toThrow("download failed (503)"); + expect(network).toHaveBeenCalledTimes(2); + }); + + it("bounds both stalled requests with real abort signals", async () => { + const realTimeout = AbortSignal.timeout.bind(AbortSignal); + const deadlines = vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(1)); + const network = vi.fn( + (_url, { signal }) => + new Promise((_resolve, reject) => { + if (signal.aborted) reject(signal.reason); + else + signal.addEventListener("abort", () => reject(signal.reason), { + once: true, + }); + }), + ); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toMatchObject({ name: "TimeoutError" }); + expect(network).toHaveBeenCalledTimes(2); + expect(deadlines.mock.calls).toEqual([[180000], [180000]]); + expect(network.mock.calls[0][1].signal).not.toBe(network.mock.calls[1][1].signal); + }); + + it("includes response-body stalls in the same two-request bound", async () => { + const realTimeout = AbortSignal.timeout.bind(AbortSignal); + const deadlines = vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(1)); + const network = vi.fn(async (_url, { signal }) => ({ + ok: true, + arrayBuffer: () => + new Promise((_resolve, reject) => { + if (signal.aborted) reject(signal.reason); + else + signal.addEventListener("abort", () => reject(signal.reason), { + once: true, + }); + }), + })); + vi.stubGlobal("fetch", network); + await expect(installTarget()).rejects.toMatchObject({ name: "TimeoutError" }); + expect(network).toHaveBeenCalledTimes(2); + expect(deadlines.mock.calls).toEqual([[180000], [180000]]); + }); +}); diff --git a/scripts/ffmpeg-distribution.mjs b/scripts/ffmpeg-distribution.mjs index 85408dec..373e9584 100644 --- a/scripts/ffmpeg-distribution.mjs +++ b/scripts/ffmpeg-distribution.mjs @@ -13,6 +13,16 @@ import { machoContentHash } from "./macho-content-hash.mjs"; import { peContentHash } from "./pe-content-hash.mjs"; const upstreamRelease = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1"; +// Exact assets from that same pinned release. The API route avoids release-page outages. +const upstreamAssetIds = Object.freeze({ + "ffmpeg-linux-arm64.gz": 316528458, + "ffmpeg-linux-x64.gz": 316528626, + "ffmpeg-win32-x64.gz": 316528798, + "ffprobe-win32-x64.gz": 316529830, + "linux-arm64.README": 316529906, + "linux-x64.README": 316529927, + "win32-x64.README": 316529965, +}); // Verified raw release digests. The upstream darwin-arm64 artifact enables nonfree. export const APPROVED_FFMPEG_BINARIES = Object.freeze({ "win32-x64": "04e1307997530f9cf2fe35cba2ca7e8875ca91da02f89d6c7243df819c94ad00", @@ -151,9 +161,41 @@ export function verifyFfprobeDistribution( } async function download(url) { - const response = await fetch(url, { signal: AbortSignal.timeout(180000) }); - if (!response.ok) throw new Error(`FFmpeg download failed (${response.status}): ${url}`); - return Buffer.from(await response.arrayBuffer()); + const assetName = url.startsWith(`${upstreamRelease}/`) + ? url.slice(upstreamRelease.length + 1) + : undefined; + const assetId = upstreamAssetIds[assetName]; + if (!Number.isSafeInteger(assetId)) throw new Error("Unapproved FFmpeg asset URL."); + for (let attempt = 0; attempt < 2; attempt += 1) { + let response; + try { + response = await fetch( + attempt === 0 + ? url + : `https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/${assetId}`, + { + signal: AbortSignal.timeout(180000), + credentials: "omit", + ...(attempt === 0 ? {} : { headers: { Accept: "application/octet-stream" } }), + }, + ); + if (!response.ok) { + await response.body?.cancel().catch(() => { + // Preserve the HTTP failure if releasing its body also fails. + }); + throw new Error(`FFmpeg download failed (${response.status}): ${url}`); + } + return Buffer.from(await response.arrayBuffer()); + } catch (error) { + const retryable = + response && !response.ok + ? response.status >= 500 && response.status <= 599 + : error instanceof TypeError || + error?.name === "TimeoutError" || + error?.name === "AbortError"; + if (attempt !== 0 || !retryable) throw error; + } + } } async function ensureWindowsFfprobe(root) {