From 47564ab94cc4bac5de6715169939353c211af689 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Fri, 31 Jul 2026 00:17:44 -0700 Subject: [PATCH] fix(cli,core,lint,producer): terminate ffprobe options at every call site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2740 added `--` to one of nine independent ffprobe invocations, so the bug class it closed stayed open everywhere else while CI reported it fixed — the regression test asserts the argv of that single site. Reproduced on ffprobe 8.1.1: an asset named `-intro.mp4` probes fine through extractMediaMetadata but fails with "Missing argument for option 'intro.mp4'" in audio pad/trim (mid-render), `hyperframes init`, whisper duration probing and webmAlphaCheck. hevcPreviewLint catches and returns false, so a dash-prefixed HEVC preview silently passes the lint rule. Terminated at all of them: producer/services/render/audioPadTrim.ts (x2) producer/plan-parity-analysis.ts cli/commands/init.ts cli/utils/webmAlphaCheck.ts cli/whisper/transcribe.ts (x2) core/mediaGradeAnalyzer.ts lint/hevcPreviewLint.ts audioPadTrim's runFfprobeJson is a near-verbatim clone of the engine's runFfprobe and structurally cannot add the terminator itself, because callers bake the input path into `args`. It now asserts the terminator is present rather than letting a dash-prefixed path through, takes the same stdio ["ignore", ...] as the engine helper, and redacts its stderr — it was throwing raw ffprobe output, which echoes the input path, into logs and telemetry. Co-Authored-By: Claude Opus 5 (1M context) --- packages/cli/src/commands/init.ts | 2 +- packages/cli/src/utils/webmAlphaCheck.ts | 1 + packages/cli/src/whisper/transcribe.ts | 3 ++- packages/core/src/mediaGradeAnalyzer.ts | 1 + packages/lint/src/hevcPreviewLint.ts | 1 + packages/producer/src/plan-parity-analysis.ts | 1 + .../producer/src/services/render/audioPadTrim.ts | 15 +++++++++++++-- 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index b470da0f8..b8905f2d3 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -109,7 +109,7 @@ function probeVideo(filePath: string): VideoMeta | undefined { if (!ffprobePath) return undefined; const raw = execFileSync( ffprobePath, - ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath], + ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "--", filePath], { encoding: "utf-8", timeout: 15_000 }, ); diff --git a/packages/cli/src/utils/webmAlphaCheck.ts b/packages/cli/src/utils/webmAlphaCheck.ts index d52f1d72a..75b8ae159 100644 --- a/packages/cli/src/utils/webmAlphaCheck.ts +++ b/packages/cli/src/utils/webmAlphaCheck.ts @@ -86,6 +86,7 @@ function probeWebmAlpha(filePath: string): WebmAlphaProbe { "stream=codec_name:stream_tags=alpha_mode", "-of", "json", + "--", filePath, ], { encoding: "utf-8", timeout: 15_000 }, diff --git a/packages/cli/src/whisper/transcribe.ts b/packages/cli/src/whisper/transcribe.ts index 6641edb95..c511a2b53 100644 --- a/packages/cli/src/whisper/transcribe.ts +++ b/packages/cli/src/whisper/transcribe.ts @@ -171,6 +171,7 @@ function getMediaDurationSeconds(filePath: string): number | null { "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", + "--", filePath, ], { encoding: "utf-8", timeout: 10_000 }, @@ -329,7 +330,7 @@ function isWav16kMono(filePath: string): boolean { if (!ffprobePath) return false; const raw = execFileSync( ffprobePath, - ["-v", "quiet", "-print_format", "json", "-show_streams", filePath], + ["-v", "quiet", "-print_format", "json", "-show_streams", "--", filePath], { encoding: "utf-8", timeout: 10_000 }, ); const parsed: { diff --git a/packages/core/src/mediaGradeAnalyzer.ts b/packages/core/src/mediaGradeAnalyzer.ts index 7ea693435..ca6c12913 100644 --- a/packages/core/src/mediaGradeAnalyzer.ts +++ b/packages/core/src/mediaGradeAnalyzer.ts @@ -121,6 +121,7 @@ function probeMedia(mediaPath: string, ffprobePath: string): GradeMediaProbe { "stream=color_space,color_transfer,color_primaries,pix_fmt,duration:format=duration", "-of", "json", + "--", mediaPath, ], { encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"] }, diff --git a/packages/lint/src/hevcPreviewLint.ts b/packages/lint/src/hevcPreviewLint.ts index 9a6cfba85..e0cc086c0 100644 --- a/packages/lint/src/hevcPreviewLint.ts +++ b/packages/lint/src/hevcPreviewLint.ts @@ -57,6 +57,7 @@ async function probeIsHevc(ffprobePath: string, filePath: string): Promise(args: string[], signal?: AbortSignal): Promise { - const proc = spawn(getFfprobeBinary(), args); + // Callers bake the input path into `args` (terminated with "--"), so this + // helper cannot add the terminator itself — assert they did rather than + // let a dash-prefixed path silently reach ffprobe as an option. + if (!args.includes("--")) { + throw new Error('[audioPadTrim] ffprobe args must terminate options with "--".'); + } + const proc = spawn(getFfprobeBinary(), args, { stdio: ["ignore", "pipe", "pipe"] }); trackChildProcess(proc); let stdout = ""; proc.stdout.on("data", (data: Buffer) => { @@ -452,7 +461,9 @@ async function runFfprobeJson(args: string[], signal?: AbortSignal): Promise< throw outcome.error ?? new Error(outcome.stderr); } if (outcome.reason !== "exit" || outcome.exitCode !== 0) { - throw new Error(`ffprobe ${outcome.reason}: ${outcome.stderr}`); + // Redacted: raw ffprobe stderr echoes the input path, and this message + // reaches logs and telemetry. + throw new Error(`ffprobe ${outcome.reason}: ${redactTelemetryString(outcome.stderr, 2000)}`); } try { return JSON.parse(stdout) as T;