diff --git a/internal/adapters/audio/ffprobe.go b/internal/adapters/audio/ffprobe.go index 6a7e476..90bbaa5 100644 --- a/internal/adapters/audio/ffprobe.go +++ b/internal/adapters/audio/ffprobe.go @@ -22,8 +22,14 @@ type FFprobe struct { Bin string // MaxOutput bounds how much of ffprobe's stdout is read; zero means DefaultMaxOutput. MaxOutput int + // Timeout bounds one probe; zero means DefaultProbeTimeout. A probe is milliseconds of + // work on a real file, so one that runs this long is the FILE's doing (#49). + Timeout time.Duration } +// DefaultProbeTimeout is the probe deadline when FFprobe.Timeout is unset. +const DefaultProbeTimeout = 45 * time.Second + // DefaultMaxOutput is far above the narrowed probe output of a legitimate file (a few // hundred bytes) and far below what a hostile one can make ffprobe print. const DefaultMaxOutput = 1 << 20 @@ -102,7 +108,13 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error) } args := append([]string{"-v", "error"}, inputRestrictions...) args = append(args, "-print_format", "json", "-show_entries", probeEntries, path) - cmd := exec.CommandContext(ctx, bin, args...) + timeout := f.Timeout + if timeout <= 0 { + timeout = DefaultProbeTimeout + } + probeCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + cmd := exec.CommandContext(probeCtx, bin, args...) cmd.WaitDelay = killGrace maxOut := f.MaxOutput if maxOut <= 0 { @@ -111,9 +123,12 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error) stdout, stderr := &capWriter{max: maxOut}, &headWriter{max: maxStderr} cmd.Stdout, cmd.Stderr = stdout, stderr if err := cmd.Run(); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { + if ctxErr := ctx.Err(); ctxErr != nil { // the caller gave up: not a verdict on the file return uploads.Probe{}, fmt.Errorf("audio: ffprobe: %w", ctxErr) } + if probeCtx.Err() != nil { // our own deadline passed while the caller was still waiting + return uploads.Probe{}, fmt.Errorf("%w: ffprobe did not finish within %s", uploads.ErrUnreadable, timeout) + } if errors.Is(err, errOutputCap) { return uploads.Probe{}, fmt.Errorf("%w: ffprobe printed more than %d bytes", uploads.ErrUnreadable, maxOut) }