The fetcher accepts image/svg+xml; internal/magic detects SVG. orig of an SVG at the orig size is the SVG unchanged, cached, with a Content-Security-Policy allowing only its styles and data: images; at another size, ErrSVGNotOriginalSize (400). Any other format renders it at the least size covering the output, at most 8192 on a side, so it stays sharp. Rendering refuses an SVG with a side over 8192 times the other (ErrSVGTooThin, 415), and the orig size of one above 8192 on a side (ErrSVGTooLargeForOrigSize, 400). The cache reads up to 1024 first bytes to find a file's format. Tests pinning the SVG refusal now expect SVG. Render time and memory are unbounded: P1 in TODO.md until the owner decides on #68. Model: opus-5-5
This commit is contained in:
1 parent
092176d917
commit
d47aea67b8
20 files changed
+945
-97
No files matched your search
+27
-1
@@ -20,6 +20,10 @@ var (
|
||||
// MinMagicBytes is the minimum number of bytes needed to detect format.
|
||||
const MinMagicBytes = 12
|
||||
|
||||
// MaxMagicBytes is the most bytes DetectFormat looks at. An SVG's root
|
||||
// element must start within them, as libvips requires.
|
||||
const MaxMagicBytes = 1024
|
||||
|
||||
// Magic byte signatures for supported formats.
|
||||
// These are effectively constants but Go doesn't support const slices.
|
||||
//
|
||||
@@ -39,6 +43,12 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// utf8BOM is the UTF-8 byte order mark, which a text file such as an SVG may
|
||||
// start with.
|
||||
//
|
||||
//nolint:gochecknoglobals // immutable lookup data
|
||||
var utf8BOM = []byte{0xEF, 0xBB, 0xBF}
|
||||
|
||||
// WebP identifier appears at offset 8 after RIFF header.
|
||||
//
|
||||
//nolint:gochecknoglobals // immutable lookup data
|
||||
@@ -53,7 +63,7 @@ var (
|
||||
)
|
||||
|
||||
// DetectFormat detects the image format from magic bytes. Data in none of
|
||||
// the formats pixa reads, SVG included, is ErrUnknownFormat.
|
||||
// the formats pixa reads is ErrUnknownFormat.
|
||||
func DetectFormat(data []byte) (imagerequest.Format, error) {
|
||||
if len(data) < MinMagicBytes {
|
||||
return "", ErrNotEnoughData
|
||||
@@ -96,9 +106,25 @@ func DetectFormat(data []byte) (imagerequest.Format, error) {
|
||||
return imagerequest.FormatJXL, nil
|
||||
}
|
||||
|
||||
if isSVG(data) {
|
||||
return imagerequest.FormatSVG, nil
|
||||
}
|
||||
|
||||
return "", ErrUnknownFormat
|
||||
}
|
||||
|
||||
// isSVG reports whether data starts as an SVG does: with a "<", after an
|
||||
// optional UTF-8 byte order mark and white space, and with "<svg" within the
|
||||
// first MaxMagicBytes bytes. Before the root element an SVG may have an XML
|
||||
// declaration, comments and a DOCTYPE.
|
||||
func isSVG(data []byte) bool {
|
||||
data = data[:min(len(data), MaxMagicBytes)]
|
||||
text := bytes.TrimLeft(bytes.TrimPrefix(data, utf8BOM), " \t\r\n")
|
||||
|
||||
return bytes.HasPrefix(text, []byte("<")) &&
|
||||
bytes.Contains(text, []byte("<svg"))
|
||||
}
|
||||
|
||||
// ValidateMagicBytes detects the format of data from its magic bytes, and
|
||||
// returns it when it is the format the declared MIME type names. Data in
|
||||
// another format is ErrMagicByteMismatch.
|
||||
|
||||
@@ -47,9 +47,9 @@ func TestDetectFormat(t *testing.T) {
|
||||
{name: "AVIF", data: avif, wantFormat: imagerequest.FormatAVIF},
|
||||
{name: "AVIF sequence", data: avis, wantFormat: imagerequest.FormatAVIF},
|
||||
{
|
||||
name: "SVG",
|
||||
data: []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`),
|
||||
wantErr: ErrUnknownFormat,
|
||||
name: "SVG",
|
||||
data: []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`),
|
||||
wantFormat: imagerequest.FormatSVG,
|
||||
},
|
||||
{
|
||||
name: "unknown format",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||||
)
|
||||
|
||||
// TestDetectFormat_SVG checks that DetectFormat names SVG text with what may
|
||||
// come before the root element, and does not name as SVG text that does not
|
||||
// start with "<" or has no "<svg" within its first MaxMagicBytes bytes.
|
||||
func TestDetectFormat_SVG(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const root = `<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"/>`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
wantErr error
|
||||
}{
|
||||
{name: "root element only", data: root},
|
||||
{name: "XML declaration", data: `<?xml version="1.0"?>` + "\n" + root},
|
||||
{name: "byte order mark and white space", data: "\xEF\xBB\xBF \n\t" + root},
|
||||
{name: "comment", data: "<!-- a drawing -->" + root},
|
||||
{
|
||||
name: "DOCTYPE",
|
||||
data: `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ` +
|
||||
`"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">` + root,
|
||||
},
|
||||
{
|
||||
name: "root element after the first bytes",
|
||||
data: "<!--" + strings.Repeat(" ", MaxMagicBytes) + "-->" + root,
|
||||
wantErr: ErrUnknownFormat,
|
||||
},
|
||||
{name: "text naming <svg", data: "draw it with " + root, wantErr: ErrUnknownFormat},
|
||||
{name: "HTML", data: "<!DOCTYPE html><html></html>", wantErr: ErrUnknownFormat},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := DetectFormat([]byte(tt.data))
|
||||
t.Logf("DetectFormat() = %q, %v", got, err)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("DetectFormat() error = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil || got != imagerequest.FormatSVG {
|
||||
t.Errorf("DetectFormat() = %q, %v; want %q", got, err,
|
||||
imagerequest.FormatSVG)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMagicBytes_SVG checks that SVG data is SVG for the
|
||||
// Content-Type image/svg+xml, with a charset too, and a mismatch for another.
|
||||
func TestValidateMagicBytes_SVG(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"/>`)
|
||||
|
||||
for _, declared := range []string{"image/svg+xml", "image/svg+xml; charset=utf-8"} {
|
||||
got, err := ValidateMagicBytes(data, declared)
|
||||
if err != nil || got != imagerequest.FormatSVG {
|
||||
t.Errorf("ValidateMagicBytes(%q) = %q, %v; want %q", declared, got,
|
||||
err, imagerequest.FormatSVG)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := ValidateMagicBytes(data, "image/png")
|
||||
if !errors.Is(err, ErrMagicByteMismatch) {
|
||||
t.Errorf("ValidateMagicBytes(image/png) error = %v, want %v", err,
|
||||
ErrMagicByteMismatch)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user