fix: review findings — clone-route audio sniffing, zip caps, filename sanitization, WAV response caps
- speakWithReference now validates the response IS audio via the same audioResultMIME sniff the sfx/enhance surfaces use — a 2xx JSON soft error or HTML proxy page was previously wrapped up as audio/wav bytes. - audioResultMIME moves to audio.go (next to speechMIME; it was defined in sfx.go but shared by enhance/clone) and learns the Ogg container normalization (application/ogg -> audio/ogg). - Stems zip unpack gains entry-count (16) and total-decompressed (1GB) caps on top of the existing per-entry cap — the per-entry bound alone still let a many-entry bomb multiply up. - sanitizeFilename drops NUL and both path separators too, so upload metadata can never smuggle directory structure to a file-writing shim. - New maxAudioResponseBytes (256MB) for bodies that ARE one audio clip (clone, enhance, sfx): a long WAV legitimately passes the 64MB JSON cap. - speakWithReference local renamed path -> upPath (naming parity with stems/enhance). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWCQcYStWXBYUy5sZWnbLT
This commit is contained in:
@@ -2,12 +2,14 @@ package llamaswap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/majordomo/audio"
|
||||
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
||||
)
|
||||
|
||||
func TestSpeakWithReferenceUsesCloneRoute(t *testing.T) {
|
||||
@@ -54,7 +56,7 @@ func TestSpeakWithReferenceUsesCloneRoute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpeakWithReferenceDefaultsToWavMIME(t *testing.T) {
|
||||
func TestSpeakWithReferenceSniffsHeaderlessWav(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
t.Fatalf("parse multipart: %v", err)
|
||||
@@ -65,7 +67,7 @@ func TestSpeakWithReferenceDefaultsToWavMIME(t *testing.T) {
|
||||
}
|
||||
}
|
||||
w.Header()["Content-Type"] = nil // no declared type at all
|
||||
_, _ = w.Write([]byte("RAWAUDIO"))
|
||||
_, _ = w.Write(wavFixture())
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
@@ -77,9 +79,43 @@ func TestSpeakWithReferenceDefaultsToWavMIME(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Speak: %v", err)
|
||||
}
|
||||
// The clone route's default container is wav, not the JSON route's mp3.
|
||||
// Headerless RIFF sniffs audio/wave, normalized to the conventional wav.
|
||||
if res.MIME != "audio/wav" {
|
||||
t.Errorf("MIME = %q, want audio/wav fallback", res.MIME)
|
||||
t.Errorf("MIME = %q, want sniffed audio/wav", res.MIME)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpeakWithReferenceRejectsNonAudioResponse(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// A FastAPI-style 2xx soft error must not be wrapped up as audio.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"detail":"reference audio too short"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
|
||||
sm, _ := p.SpeechModel("chatterbox")
|
||||
_, err := sm.Speak(context.Background(),
|
||||
audio.SpeechRequest{Input: "hi"},
|
||||
audio.WithReferenceAudio([]byte("REF"), "audio/wav"))
|
||||
var apiErr *llm.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err = %v, want APIError for non-audio clone body", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilenameStripsPathAndControlBytes(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"song.mp3": "song.mp3",
|
||||
"../../etc/passwd": "....etcpasswd",
|
||||
"a\r\nContent-Type: evil": "aContent-Type: evil",
|
||||
"..\\..\\boot.ini": "....boot.ini",
|
||||
"nul\x00byte.wav": "nulbyte.wav",
|
||||
" / ": "",
|
||||
} {
|
||||
if got := sanitizeFilename(in); got != want {
|
||||
t.Errorf("sanitizeFilename(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user