harden manifest detection and downloader failure coverage

This commit is contained in:
2026-04-21 20:55:14 +02:00
parent dfa8095e1d
commit 96348a6a34
2 changed files with 74 additions and 1 deletions

View File

@@ -386,7 +386,7 @@ func isManifestResponse(contentType string, peek []byte) bool {
return true return true
} }
s := strings.TrimSpace(strings.ToLower(string(peek))) s := strings.TrimSpace(strings.ToLower(string(peek)))
if strings.HasPrefix(s, "<?xml") && strings.Contains(s, "<mpd") { if (strings.HasPrefix(s, "<?xml") && strings.Contains(s, "<mpd")) || strings.HasPrefix(s, "<mpd") {
return true return true
} }
if strings.HasPrefix(s, "#extm3u") { if strings.HasPrefix(s, "#extm3u") {

View File

@@ -8,8 +8,11 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time"
"golang.org/x/crypto/blowfish" "golang.org/x/crypto/blowfish"
) )
@@ -46,9 +49,15 @@ func TestManifestDetection(t *testing.T) {
if !isManifestResponse("application/dash+xml", []byte("x")) { if !isManifestResponse("application/dash+xml", []byte("x")) {
t.Fatalf("expected dash content-type to be manifest") t.Fatalf("expected dash content-type to be manifest")
} }
if !isManifestResponse("application/vnd.apple.mpegurl", []byte("x")) {
t.Fatalf("expected mpegurl content-type to be manifest")
}
if !isManifestResponse("application/octet-stream", []byte("<?xml version='1.0'?><MPD></MPD>")) { if !isManifestResponse("application/octet-stream", []byte("<?xml version='1.0'?><MPD></MPD>")) {
t.Fatalf("expected MPD XML body to be manifest") t.Fatalf("expected MPD XML body to be manifest")
} }
if !isManifestResponse("application/octet-stream", []byte("<MPD type='static'></MPD>")) {
t.Fatalf("expected MPD body without xml prolog to be manifest")
}
if !isManifestResponse("text/plain", []byte("#EXTM3U\n#EXT-X-VERSION:3")) { if !isManifestResponse("text/plain", []byte("#EXTM3U\n#EXT-X-VERSION:3")) {
t.Fatalf("expected HLS body to be manifest") t.Fatalf("expected HLS body to be manifest")
} }
@@ -158,3 +167,67 @@ func TestFileDeezerEncryptedTruncatedResponseRemovesPartialFile(t *testing.T) {
t.Fatalf("expected no partial file, stat err=%v", statErr) t.Fatalf("expected no partial file, stat err=%v", statErr)
} }
} }
func TestFileDeezerEncryptedBadStatus(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer ts.Close()
d := NewWithOptions(true, false)
out := filepath.Join(t.TempDir(), "x", "a.flac")
err := d.FileDeezerEncrypted(context.Background(), ts.URL, out, "3135556")
if err == nil || !strings.Contains(err.Error(), "status=403") {
t.Fatalf("expected status error, got %v", err)
}
}
func TestDownloaderFileContextCancellationRemovesPartialFile(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = w.Write([]byte("partial-data"))
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
<-r.Context().Done()
}))
defer ts.Close()
d := NewWithOptions(true, false)
out := filepath.Join(t.TempDir(), "x", "cancel.bin")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
defer cancel()
err := d.File(ctx, ts.URL, out)
if err == nil {
t.Fatalf("expected context cancellation error")
}
if _, statErr := os.Stat(out); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("expected no partial file, stat err=%v", statErr)
}
}
func TestStreamManifestWithFFmpegMissing(t *testing.T) {
d := NewWithOptions(true, false)
t.Setenv("PATH", "")
err := d.streamManifestWithFFmpeg(context.Background(), "https://example.com/live.m3u8", filepath.Join(t.TempDir(), "out.m4a"), false)
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "ffmpeg not found") {
t.Fatalf("expected ffmpeg missing error, got %v", err)
}
}
func TestStreamManifestWithFFmpegFailureRemovesPartialFile(t *testing.T) {
if _, err := exec.LookPath("ffmpeg"); err != nil {
t.Skip("ffmpeg not installed")
}
d := NewWithOptions(true, false)
out := filepath.Join(t.TempDir(), "out.m4a")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := d.streamManifestWithFFmpeg(ctx, "https://127.0.0.1:1/unreachable.m3u8", out, false)
if err == nil || !strings.Contains(err.Error(), "ffmpeg stream copy failed") {
t.Fatalf("expected ffmpeg failure error, got %v", err)
}
if _, statErr := os.Stat(out); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("expected no partial file after ffmpeg failure, stat err=%v", statErr)
}
}