Files
volumen/internal/imagefile/imagefile_test.go
petrbalvin f8ed33df83
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

239 lines
8.2 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package imagefile
import "testing"
// webpBytes is a minimal RIFF/WEBP header, enough for the signature
// check.
func webpBytes() []byte {
data := append([]byte("RIFF"), 0, 0, 0, 0)
return append(data, []byte("WEBPVP8 ")...)
}
// avifBytes is a minimal ISO BMFF header carrying the avif brand.
func avifBytes() []byte {
return []byte{0, 0, 0, 24, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f', 0, 0, 0, 0}
}
func TestSignatureMatches(t *testing.T) {
if !SignatureMatches(webpBytes(), ExtWebP) {
t.Fatal("webp signature rejected")
}
if !SignatureMatches(avifBytes(), ExtAVIF) {
t.Fatal("avif signature rejected")
}
svg := []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="8" height="6"><rect/></svg>`)
if !SignatureMatches(svg, ExtSVG) {
t.Fatal("svg signature rejected")
}
prologed := []byte("\ufeff<?xml version=\"1.0\"?>\n<!-- figure --><svg></svg>")
if !SignatureMatches(prologed, ExtSVG) {
t.Fatal("svg with prolog and comment rejected")
}
if SignatureMatches([]byte("<html><body>hi</body></html>"), ExtSVG) {
t.Fatal("an html document accepted as svg")
}
if SignatureMatches([]byte("<svgrect/>"), ExtSVG) {
t.Fatal("a lookalike element name accepted as svg")
}
// The avis brand is a valid AVIF image sequence.
avis := append([]byte{}, avifBytes()...)
copy(avis[8:12], "avis")
if !SignatureMatches(avis, ExtAVIF) {
t.Fatal("avis brand rejected")
}
if SignatureMatches([]byte("short"), ExtWebP) {
t.Fatal("short data accepted as webp")
}
if SignatureMatches([]byte("plain text here!!!"), ExtAVIF) {
t.Fatal("garbage accepted as avif")
}
if SignatureMatches(webpBytes(), ".png") {
t.Fatal("unknown extension accepted")
}
if SignatureMatches(avifBytes(), ExtWebP) {
t.Fatal("avif bytes accepted as webp")
}
}
func TestDetect(t *testing.T) {
if got := Detect(webpBytes()); got != ExtWebP {
t.Fatalf("Detect(webp) = %q", got)
}
if got := Detect(avifBytes()); got != ExtAVIF {
t.Fatalf("Detect(avif) = %q", got)
}
if got := Detect([]byte("<?xml version=\"1.0\"?><svg xmlns=\"http://www.w3.org/2000/svg\"></svg>")); got != ExtSVG {
t.Fatalf("Detect(svg) = %q", got)
}
for _, data := range [][]byte{
nil, []byte("RIFF"), []byte("GIF89a and then padding"),
[]byte("not an image at all, but long enough"),
[]byte("<!DOCTYPE html>\n<html></html>"),
} {
if got := Detect(data); got != "" {
t.Fatalf("Detect(%q) = %q, want empty", data, got)
}
}
}
func TestContentType(t *testing.T) {
cases := map[string]string{
"photo.webp": MIMEWebP,
"photo.AVIF": MIMEAVIF,
"figure.svg": MIMESVG,
"nested/dir/photo.webp": MIMEWebP,
"photo.png": "",
"photo": "",
"": "",
"photo.webp.html": "",
}
for name, want := range cases {
if got := ContentType(name); got != want {
t.Errorf("ContentType(%q) = %q, want %q", name, got, want)
}
if Allowed(name) != (want != "") {
t.Errorf("Allowed(%q) = %v, want %v", name, Allowed(name), want != "")
}
}
}
// mkbox assembles one ISO-BMFF box: a big-endian size, the four-byte type
// and the body.
func mkbox(typ string, body []byte) []byte {
size := len(body) + 8
out := make([]byte, 8, size)
out[0] = byte(size >> 24)
out[1] = byte(size >> 16)
out[2] = byte(size >> 8)
out[3] = byte(size)
copy(out[4:], typ)
return append(out, body...)
}
// avifWithDimensions assembles a minimal but structurally honest AVIF:
// ftyp, then meta naming item 1 as primary and associating its ispe.
func avifWithDimensions(width, height int) []byte {
ispe := []byte{0, 0, 0, 0} // full box: version and flags
ispe = append(ispe,
byte(width>>24), byte(width>>16), byte(width>>8), byte(width),
byte(height>>24), byte(height>>16), byte(height>>8), byte(height))
ipco := mkbox("ipco", mkbox("ispe", ispe))
// ipma v0, flags 0: one entry, item 1, one association naming
// property 1 (the ispe).
ipma := []byte{0, 0, 0, 0,
0, 0, 0, 1, // entry count
0, 1, // item id 1
1, // one association
1, // property index 1
}
pitm := []byte{0, 0, 0, 0, 0, 1} // v0, item id 1
iprp := append(ipco, mkbox("ipma", ipma)...)
metaBody := append([]byte{0, 0, 0, 0}, mkbox("pitm", pitm)...)
metaBody = append(metaBody, mkbox("iprp", iprp)...)
meta := mkbox("meta", metaBody)
ftyp := []byte{0, 0, 0, 16, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f', 0, 0, 0, 0}
return append(ftyp, meta...)
}
func TestDimensions(t *testing.T) {
// webpChunk builds a RIFF/WEBP file whose first chunk carries the
// payload: fourcc, little-endian size, then the bytes.
webpChunk := func(chunk string, payload ...byte) []byte {
out := append([]byte("RIFF"), 0, 0, 0, 0)
out = append(out, []byte("WEBP")...)
out = append(out, chunk...)
out = append(out, byte(len(payload)), 0, 0, 0)
return append(out, payload...)
}
// VP8L opens with 0x2f and packs width-1 then height-1 into 14-bit
// little-endian fields.
bits := uint32(1919) | uint32(1079)<<14
vp8l := webpChunk("VP8L", 0x2f, byte(bits), byte(bits>>8), byte(bits>>16), byte(bits>>24))
// VP8X carries two 24-bit minus-one canvas values after the flags
// and three reserved bytes: 999 and 599, little-endian.
vp8x := webpChunk("VP8X", 0, 0, 0, 0, 0xE7, 0x03, 0x00, 0x57, 0x02, 0x00)
// VP8 lossy: frame tag, the sync bytes, then two scaled 14-bit
// little-endian values.
vp8 := webpChunk("VP8 ", 0, 0, 0, 0x9d, 0x01, 0x2a, 0x80, 0x02, 0xe0, 0x01)
// The AVIF structure assembled above.
avif := avifWithDimensions(800, 600)
cases := []struct {
name string
data []byte
w, h int
ok bool
}{
{"webp lossless", vp8l, 1920, 1080, true},
{"webp extended", vp8x, 1000, 600, true},
{"webp lossy", vp8, 640, 480, true},
{"avif primary item", avif, 800, 600, true},
{"empty", nil, 0, 0, false},
{"truncated webp", vp8x[:18], 0, 0, false},
{"text", []byte("plainly not an image at all"), 0, 0, false},
{"svg attributes",
[]byte(`<svg xmlns="http://www.w3.org/2000/svg" width="800px" height="600px"><g/></svg>`), 800, 600, true},
{"svg viewbox",
[]byte("<?xml version=\"1.0\"?>\n<svg viewBox=\"0 -10 1200 900\"></svg>"), 1200, 900, true},
{"svg percent length",
[]byte(`<svg width="100%" height="100%"></svg>`), 0, 0, false},
{"svg no size",
[]byte(`<svg xmlns="http://www.w3.org/2000/svg"><circle/></svg>`), 0, 0, false},
}
for _, tc := range cases {
w, h, ok := Dimensions(tc.data)
if ok != tc.ok || (ok && (w != tc.w || h != tc.h)) {
t.Errorf("%s: Dimensions = %d, %d, %v; want %d, %d, %v",
tc.name, w, h, ok, tc.w, tc.h, tc.ok)
}
}
}
// A box whose declared content is cut short must be refused, not panic:
// the walk may only read bytes the box really holds, and a truncated
// pitm or meta sits at the end of the buffer, where a read past the box
// runs past the whole input.
func TestDimensionsTruncatedBoxes(t *testing.T) {
ftyp := []byte{0, 0, 0, 16, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f', 0, 0, 0, 0}
fullBox := func(typ string, body []byte) []byte {
return append(ftyp, mkbox(typ, body)...)
}
// A v0 pitm carries a two-byte item id; only one byte of it survives.
shortPitm := []byte{0, 0, 0, 0, 0}
// A v1 pitm carries a four-byte item id; three bytes survive.
widePitm := []byte{1, 0, 0, 0, 1, 2, 3}
// A meta box whose full-box header itself is cut in half.
for _, tc := range []struct {
name string
data []byte
}{
{"truncated pitm", fullBox("meta", append([]byte{0, 0, 0, 0}, mkbox("pitm", shortPitm)...))},
{"truncated wide pitm", fullBox("meta", append([]byte{0, 0, 0, 0}, mkbox("pitm", widePitm)...))},
{"truncated meta header", fullBox("meta", []byte{0, 0})},
{"header-only pitm", fullBox("meta", append([]byte{0, 0, 0, 0}, mkbox("pitm", nil)...))},
} {
if _, _, ok := Dimensions(tc.data); ok {
t.Errorf("%s: Dimensions reported a size", tc.name)
}
}
}
// A prefix is enough: the media listing reads only the head of a file,
// so the sizes must come from there too.
func TestDimensionsFromPrefix(t *testing.T) {
full := avifWithDimensions(123, 45)
head := make([]byte, 64<<10)
copy(head, full)
if w, h, ok := Dimensions(head[:len(full)+1024]); !ok || w != 123 || h != 45 {
t.Fatalf("prefix Dimensions = %d, %d, %v", w, h, ok)
}
}