Files
Recordly/electron/ffmpegDistribution.test.mjs

248 lines
9.0 KiB
JavaScript

import { createHash } from "node:crypto";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
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";
const fixtures = [];
function fixture(overrides = {}) {
const directory = mkdtempSync(path.join(os.tmpdir(), "recordly-ffmpeg-provenance-"));
fixtures.push(directory);
const binary = path.join(directory, "ffmpeg");
const bytes = Buffer.from("unapproved executable fixture");
writeFileSync(binary, bytes);
writeFileSync(
`${binary}.distribution.json`,
JSON.stringify({
schemaVersion: 1,
platform: "win32",
arch: "x64",
sha256: createHash("sha256").update(bytes).digest("hex"),
...overrides,
}),
);
return binary;
}
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
for (const directory of fixtures.splice(0)) rmSync(directory, { recursive: true, force: true });
});
describe("FFmpeg redistribution checks", () => {
it("does not accept a content-hash override for an unapproved Linux executable", () => {
const binary = fixture({
platform: "linux",
sha256: APPROVED_FFMPEG_BINARIES["linux-x64"],
contentSha256: createHash("sha256")
.update("unapproved executable fixture")
.digest("hex"),
});
expect(() => verifyFfmpegDistribution(binary, { platform: "linux", arch: "x64" })).toThrow(
"does not match",
);
});
it("accepts GPL/LGPL builds with nonfree explicitly disabled", () => {
expect(() =>
assertRedistributableFfmpegOutput(
"--disable-nonfree\nGNU General Public License version 3",
),
).not.toThrow();
expect(() =>
assertRedistributableFfmpegOutput("GNU Lesser General Public License version 2.1"),
).not.toThrow();
});
it("rejects the real nonredistributable build declaration despite GPL text", () => {
expect(() =>
assertRedistributableFfmpegOutput(
"GNU General Public License\nThis version of ffmpeg has nonfree parts compiled in. Therefore it is not legally redistributable.",
),
).toThrow("nonfree");
expect(() =>
assertRedistributableFfmpegOutput(
"configuration: --enable-gpl --enable-nonfree\nGNU General Public License",
),
).toThrow("nonfree");
});
it("rejects missing or unrecognized license output", () => {
expect(() => assertRedistributableFfmpegOutput("ffmpeg version 8.1.2")).toThrow(
"recognized",
);
});
it("rejects a changed executable even with an existing provenance record", () => {
const binary = fixture();
writeFileSync(binary, "changed after verification");
expect(() => verifyFfmpegDistribution(binary, { platform: "win32", arch: "x64" })).toThrow(
"does not match",
);
});
it("rejects an artifact from another target architecture", () => {
expect(() =>
verifyFfmpegDistribution(fixture(), { platform: "win32", arch: "arm64" }),
).toThrow("does not match");
});
it("does not trust an arbitrary binary merely because its manifest has a matching checksum", () => {
expect(() =>
verifyFfmpegDistribution(fixture(), { platform: "win32", arch: "x64" }),
).toThrow("Unapproved");
});
it("rejects a manifest pretending to be a macOS source build", () => {
expect(() =>
verifyFfmpegDistribution(
fixture({ platform: "darwin", arch: "arm64", buildId: "made-up" }),
{ platform: "darwin", arch: "arm64" },
),
).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]]);
});
});