Support SVG sources (closes #68)
check / check (push) Canceled after 0s

The fetcher accepts image/svg+xml again and internal/magic detects SVG.
A raster format renders the SVG with librsvg at the density that makes
it cover the output size, so an enlarged SVG stays sharp; the rendered
size is held to MaxInputDimension. orig of an SVG at the orig size is
the SVG unchanged, cached as a variant; at any other size it is
ErrSVGNotOriginalSize, answered 400. The cache reads up to 1024 first
bytes of a file to find its format, as an SVG's root element can come
after a prolog. Tests that pinned SVG as refused now expect it to be
read.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 00:38:16 +00:00
1 parent 9f1e9f5253
commit 632fe0bd36
20 files changed
+669 -76

No files matched your search

+94 -18
View File
@@ -4,38 +4,114 @@ import (
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"sneak.berlin/go/pixa/internal/imageproxy"
"sneak.berlin/go/pixa/internal/imagerequest"
"sneak.berlin/go/pixa/internal/magic"
)
// TestImageProxyRefusesSVG requests an image the upstream answers as SVG,
// in its own format (orig) and resized as PNG, through pixa's router,
// upstream fetcher and image processor. pixa does not accept SVG as a
// source, so each request is answered 415, never with an image.
func TestImageProxyRefusesSVG(t *testing.T) {
t.Parallel()
// testSVG is the SVG the upstream serves in the SVG tests below.
const testSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="48">` +
`<rect width="64" height="48"/></svg>`
// startSVGUpstream starts an upstream that answers every request with
// testSVG as image/svg+xml, and returns it with its count of requests.
func startSVGUpstream(t *testing.T) (*httptest.Server, *atomic.Int64) {
t.Helper()
var requests atomic.Int64
upstream := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
requests.Add(1)
w.Header().Set("Content-Type", "image/svg+xml")
_, _ = io.WriteString(w, `<svg xmlns="http://www.w3.org/2000/svg" `+
`width="64" height="48"><rect width="64" height="48"/></svg>`)
_, _ = io.WriteString(w, testSVG)
}))
t.Cleanup(upstream.Close)
return upstream, &requests
}
// TestImageProxyServesSVGAsItIs requests an SVG source in its own format
// (orig) at the orig size through pixa's router, upstream fetcher, image
// processor and cache, twice. Both answers are the SVG itself, never another
// format, the second from the cache; the upstream is asked once. Scripts in
// the SVG cannot run, as the Content-Security-Policy allows no inline
// script. orig at another size is refused with 400.
func TestImageProxyServesSVGAsItIs(t *testing.T) {
t.Parallel()
upstream, requests := startSVGUpstream(t)
s, _, _ := startImageProxy(t, upstream)
target := "/v1/image/" + upstreamHost + "/drawing.svg/orig.orig"
for _, wantCache := range []imageproxy.CacheStatus{
imageproxy.CacheMiss, imageproxy.CacheHit,
} {
rec := getImage(t, s, target)
if rec.Header().Get("Content-Type") != "image/svg+xml" ||
rec.Body.String() != testSVG {
t.Errorf("GET %s: %s %q, want image/svg+xml %q", target,
rec.Header().Get("Content-Type"), rec.Body.String(), testSVG)
}
if got := rec.Header().Get("X-Pixa-Cache"); got != string(wantCache) {
t.Errorf("GET %s: X-Pixa-Cache = %q, want %q", target, got, wantCache)
}
policy := rec.Header().Get("Content-Security-Policy")
if !strings.Contains(policy, "script-src 'self'") ||
strings.Contains(policy, "unsafe-inline") {
t.Errorf("GET %s: Content-Security-Policy = %q, want script-src "+
"'self' and no unsafe-inline", target, policy)
}
}
if requests.Load() != 1 {
t.Errorf("upstream requests = %d, want 1", requests.Load())
}
resized := "/v1/image/" + upstreamHost + "/drawing.svg/32x24.orig"
rec := httptest.NewRecorder()
s.ServeHTTP(rec, httptest.NewRequestWithContext(
t.Context(), http.MethodGet, resized, nil))
t.Logf("GET %s: %d %s", resized, rec.Code, rec.Body.String())
if rec.Code != http.StatusBadRequest {
t.Errorf("GET %s: %d %s, want 400", resized, rec.Code, rec.Body.String())
}
}
// TestImageProxyRendersSVG requests an SVG source as each raster format
// through pixa's router, upstream fetcher and image processor. Each answer
// is an image in that format.
func TestImageProxyRendersSVG(t *testing.T) {
t.Parallel()
upstream, _ := startSVGUpstream(t)
s, _, _ := startImageProxy(t, upstream)
for _, sizeFormat := range []string{"64x48.orig", "32x24.png"} {
target := "/v1/image/" + upstreamHost + "/drawing.svg/" + sizeFormat
for _, format := range []imagerequest.Format{
imagerequest.FormatJPEG, imagerequest.FormatPNG, imagerequest.FormatGIF,
imagerequest.FormatWebP, imagerequest.FormatAVIF, imagerequest.FormatJXL,
} {
target := "/v1/image/" + upstreamHost + "/drawing.svg/32x24." +
string(format)
rec := httptest.NewRecorder()
s.ServeHTTP(rec, httptest.NewRequestWithContext(
t.Context(), http.MethodGet, target, nil))
t.Logf("GET %s: %d, Content-Type %s, body %s",
target, rec.Code, rec.Header().Get("Content-Type"), rec.Body.String())
rec := getImage(t, s, target)
detected, err := magic.DetectFormat(rec.Body.Bytes())
t.Logf("GET %s: %s, body in %q (%v)", target,
rec.Header().Get("Content-Type"), detected, err)
if rec.Code != http.StatusUnsupportedMediaType {
t.Errorf("GET %s status = %d, want %d",
target, rec.Code, http.StatusUnsupportedMediaType)
if rec.Header().Get("Content-Type") != format.MIMEType() ||
err != nil || detected != format {
t.Errorf("GET %s: %s, body in %q (%v); want %s", target,
rec.Header().Get("Content-Type"), detected, err, format.MIMEType())
}
}
}