From 69dd0143e8ddd2c91725e5e7b3411b000e85ecfd Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 13 Oct 2026 03:18:22 +0000 Subject: [PATCH] Test that an SVG served as AVIF is refused, and that each source format loads An SVG starting with an XML comment holding "ftypavis" passes the first-bytes check for AVIF; resized as PNG, it is answered 200 with the SVG rendered. The new server test asks for it through both image routes, resized and as orig, and wants 415 for each. The new processor test checks that an image in each of the six source formats loads and is saved again in its own format. Model: opus-5-5 --- .../source_formats_internal_test.go | 46 ++++++++++++ internal/server/svg_as_avif_internal_test.go | 73 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 internal/imageprocessor/source_formats_internal_test.go create mode 100644 internal/server/svg_as_avif_internal_test.go diff --git a/internal/imageprocessor/source_formats_internal_test.go b/internal/imageprocessor/source_formats_internal_test.go new file mode 100644 index 0000000..5f1dbbd --- /dev/null +++ b/internal/imageprocessor/source_formats_internal_test.go @@ -0,0 +1,46 @@ +package imageprocessor + +import ( + "bytes" + "io" + "testing" +) + +// TestImageProcessor_LoadsEachSourceFormat verifies that an image in each of +// the six source formats, saved by the processor itself, is loaded and saved +// again in its own format (orig). +func TestImageProcessor_LoadsEachSourceFormat(t *testing.T) { + t.Parallel() + + proc := New(Params{}) + + for _, format := range []Format{ + FormatJPEG, FormatPNG, FormatGIF, FormatWebP, FormatAVIF, FormatJXL, + } { + saved, err := proc.Process(t.Context(), + bytes.NewReader(createTestPNG(t, 20, 20)), &Request{Format: format}) + if err != nil { + t.Fatalf("saving the test image as %s: %v", format, err) + } + + source, err := io.ReadAll(saved.Content) + if err != nil { + t.Fatalf("reading the %s image: %v", format, err) + } + + result, err := proc.Process(t.Context(), bytes.NewReader(source), + &Request{Format: FormatOriginal}) + if err != nil { + t.Errorf("orig of the %s image: %v", format, err) + + continue + } + + t.Logf("orig of the %s image: %s", format, result.ContentType) + + if result.ContentType != FormatToMIME(format) { + t.Errorf("orig of the %s image is %s, want %s", + format, result.ContentType, FormatToMIME(format)) + } + } +} diff --git a/internal/server/svg_as_avif_internal_test.go b/internal/server/svg_as_avif_internal_test.go new file mode 100644 index 0000000..214ec96 --- /dev/null +++ b/internal/server/svg_as_avif_internal_test.go @@ -0,0 +1,73 @@ +package server + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "sneak.berlin/go/pixa/internal/encurl" + "sneak.berlin/go/pixa/internal/imgcache" +) + +// svgServedAsAVIF is an SVG whose first bytes pass the first-bytes check for +// AVIF: its XML comment puts "ftypavis" at bytes 4 to 11, where an AVIF file +// has its ftyp box and brand. libvips would load it as SVG. +const svgServedAsAVIF = `` + +// TestImageProxyRefusesSVGServedAsAVIF requests svgServedAsAVIF, which the +// upstream answers as image/avif, through both image routes, resized as PNG +// and in its own format (orig). Each request is answered 415, as for an SVG +// source, never with an image. +func TestImageProxyRefusesSVGServedAsAVIF(t *testing.T) { + t.Parallel() + + upstream := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "image/avif") + _, _ = io.WriteString(w, svgServedAsAVIF) + })) + t.Cleanup(upstream.Close) + + s, _, _ := startImageProxy(t, upstream) + + generator, err := encurl.NewGenerator(testSigningKey) + if err != nil { + t.Fatalf("encurl.NewGenerator: %v", err) + } + + targets := []string{ + "/v1/image/" + upstreamHost + "/drawing.avif/32x24.png", + "/v1/image/" + upstreamHost + "/drawing.avif/64x48.orig", + } + + for _, format := range []imgcache.ImageFormat{ + imgcache.FormatPNG, imgcache.FormatOriginal, + } { + token, err := generator.Generate(&encurl.Payload{ + SourceHost: upstreamHost, SourcePath: "/drawing.avif", + Width: 32, Height: 24, Format: format, + }) + if err != nil { + t.Fatalf("encurl Generate: %v", err) + } + + targets = append(targets, "/v1/e/"+token+"/drawing."+string(format)) + } + + for _, target := range targets { + rec := httptest.NewRecorder() + s.ServeHTTP(rec, httptest.NewRequestWithContext( + t.Context(), http.MethodGet, target, nil)) + t.Logf("GET %s: %d, Content-Type %s, body %q", + target, rec.Code, rec.Header().Get("Content-Type"), rec.Body.String()) + + if rec.Code != http.StatusUnsupportedMediaType || + !strings.Contains(rec.Body.String(), "unsupported source format") { + t.Errorf("GET %s: %d %q, want %d unsupported source format", + target, rec.Code, rec.Body.String(), http.StatusUnsupportedMediaType) + } + } +}