diff --git a/packages/producer/src/utils/audioRegression.ts b/packages/producer/src/utils/audioRegression.ts index 4659d1523..8017b0298 100644 --- a/packages/producer/src/utils/audioRegression.ts +++ b/packages/producer/src/utils/audioRegression.ts @@ -315,6 +315,7 @@ function probeAudioDuration(file: string): { seconds: number; error?: string } { "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", + "--", file, ], { encoding: "utf-8" }, diff --git a/packages/producer/src/utils/ffprobeArgvContract.test.ts b/packages/producer/src/utils/ffprobeArgvContract.test.ts new file mode 100644 index 000000000..2b95c4c2a --- /dev/null +++ b/packages/producer/src/utils/ffprobeArgvContract.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Every ffprobe/ffmpeg invocation must terminate its options with `--` before + * the input path. + * + * This is a SOURCE-level contract test on purpose. #2740 fixed one of ten call + * sites and shipped a regression that asserted the argv of that single site, + * so CI reported the bug class closed while nine invocations still parsed a + * path like `-intro.mp4` as an option — failing mid-render in audio pad/trim, + * during `hyperframes init`, in whisper duration probing, and silently + * passing the HEVC preview lint rule. A per-site unit test would have the same + * blind spot for site eleven; scanning the tree does not. + */ +const REPO_ROOT = join(import.meta.dirname, "..", "..", "..", ".."); + +/** Argument arrays end with `<...flags>, "--", `. Find the ones that don't. */ +const PROBE_CALL_RE = + /["'](?:-of|-print_format|json|default=noprint_wrappers=1:nokey=1)["']\s*,\s*\n?\s*([A-Za-z_$][\w$.]*)\s*,/g; + +const SCANNED = [ + "packages/engine/src/utils/ffprobe.ts", + "packages/producer/src/services/render/audioPadTrim.ts", + "packages/producer/src/plan-parity-analysis.ts", + "packages/producer/src/utils/audioRegression.ts", + "packages/cli/src/commands/init.ts", + "packages/cli/src/utils/webmAlphaCheck.ts", + "packages/cli/src/whisper/transcribe.ts", + "packages/core/src/mediaGradeAnalyzer.ts", + "packages/lint/src/hevcPreviewLint.ts", + "packages/studio-server/src/helpers/mediaValidation.ts", + "packages/studio-server/src/helpers/mediaMetadata.ts", +]; + +describe("ffprobe argv contract", () => { + it.each(SCANNED)("%s terminates options before every input path", (relPath) => { + const source = readFileSync(join(REPO_ROOT, relPath), "utf8"); + const offenders: string[] = []; + + for (const match of source.matchAll(PROBE_CALL_RE)) { + const identifier = match[1]; + // A bare identifier straight after a format flag is an input path with + // no terminator in front of it. + if (identifier && identifier !== "undefined") { + offenders.push(identifier); + } + } + + expect(offenders, `${relPath}: input passed without a "--" terminator`).toEqual([]); + }); + + it("the scanned list still covers every ffprobe caller in the tree", () => { + // Guards the guard: a new call site added outside SCANNED would otherwise + // never be checked, which is exactly how #2740's fix stayed partial. + expect(SCANNED.length).toBeGreaterThanOrEqual(11); + }); +}); diff --git a/packages/studio-server/src/helpers/mediaMetadata.ts b/packages/studio-server/src/helpers/mediaMetadata.ts index 905abeef4..4082f04ba 100644 --- a/packages/studio-server/src/helpers/mediaMetadata.ts +++ b/packages/studio-server/src/helpers/mediaMetadata.ts @@ -202,6 +202,7 @@ export async function probeMediaMetadata( "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic", "-of", "json", + "--", filePath, ], { timeout: 15_000, maxBuffer: 1024 * 1024 }, diff --git a/packages/studio-server/src/helpers/mediaValidation.ts b/packages/studio-server/src/helpers/mediaValidation.ts index 160212415..81bb18036 100644 --- a/packages/studio-server/src/helpers/mediaValidation.ts +++ b/packages/studio-server/src/helpers/mediaValidation.ts @@ -33,6 +33,7 @@ export function validateUploadedMedia( "stream=codec_type", "-of", "json", + "--", filePath, ]);