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 at the least size that covers the
output, held to 8192 on a side, through libvips' thumbnail, which
renders at any scale, so an enlarged SVG stays sharp. An SVG's own size
is not limited; one with a side more than 8192 times the other is
ErrSVGTooThin, answered 415. 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. Render time and memory grow with
what the SVG draws; their bound is the owner's decision on #68. 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 12:08:53 +00:00
1 parent 1379497897
commit 98b79c2e27
19 files changed
+796 -82

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())
}
}
}