@@ -4,6 +4,7 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -19,6 +20,45 @@ import (
|
||||
type FFprobe struct {
|
||||
// Bin is the ffprobe executable ("ffprobe" resolves via PATH).
|
||||
Bin string
|
||||
// MaxOutput bounds how much of ffprobe's stdout is read; zero means DefaultMaxOutput.
|
||||
MaxOutput int
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// maxStderr is how much of ffprobe's stderr is kept for the error message.
|
||||
const maxStderr = 4096
|
||||
|
||||
// errOutputCap is the write error that stops ffprobe once it prints past the cap.
|
||||
var errOutputCap = errors.New("audio: ffprobe output exceeds the cap")
|
||||
|
||||
// capWriter collects up to max bytes and then refuses further writes, which closes the
|
||||
// pipe and ends the process instead of buffering whatever it prints.
|
||||
type capWriter struct {
|
||||
buf bytes.Buffer
|
||||
max int
|
||||
}
|
||||
|
||||
func (w *capWriter) Write(p []byte) (int, error) {
|
||||
if w.buf.Len()+len(p) > w.max {
|
||||
return 0, errOutputCap
|
||||
}
|
||||
return w.buf.Write(p)
|
||||
}
|
||||
|
||||
// headWriter keeps the first max bytes and silently drops the rest.
|
||||
type headWriter struct {
|
||||
buf bytes.Buffer
|
||||
max int
|
||||
}
|
||||
|
||||
func (w *headWriter) Write(p []byte) (int, error) {
|
||||
if room := w.max - w.buf.Len(); room > 0 {
|
||||
w.buf.Write(p[:min(room, len(p))])
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// AllowedDemuxers are the input formats an upload may be. The input is
|
||||
@@ -64,18 +104,26 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error)
|
||||
args = append(args, "-print_format", "json", "-show_entries", probeEntries, path)
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
cmd.WaitDelay = killGrace
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
maxOut := f.MaxOutput
|
||||
if maxOut <= 0 {
|
||||
maxOut = DefaultMaxOutput
|
||||
}
|
||||
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 {
|
||||
return uploads.Probe{}, fmt.Errorf("audio: ffprobe: %w", ctxErr)
|
||||
}
|
||||
if errors.Is(err, errOutputCap) {
|
||||
return uploads.Probe{}, fmt.Errorf("%w: ffprobe printed more than %d bytes", uploads.ErrUnreadable, maxOut)
|
||||
}
|
||||
var exit *exec.ExitError
|
||||
if errors.As(err, &exit) {
|
||||
return uploads.Probe{}, fmt.Errorf("%w: ffprobe: %s", uploads.ErrUnreadable, exit.Stderr)
|
||||
return uploads.Probe{}, fmt.Errorf("%w: ffprobe: %s", uploads.ErrUnreadable, stderr.buf.String())
|
||||
}
|
||||
return uploads.Probe{}, fmt.Errorf("audio: run ffprobe: %w", err)
|
||||
}
|
||||
p, err := ParseProbe(out)
|
||||
p, err := ParseProbe(stdout.buf.Bytes())
|
||||
if err != nil {
|
||||
return uploads.Probe{}, fmt.Errorf("audio: ffprobe output: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user