// Copyright (c) 2026 Petr BalvĂ­n (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(``) if !SignatureMatches(svg, ExtSVG) { t.Fatal("svg signature rejected") } prologed := []byte("\ufeff\n") if !SignatureMatches(prologed, ExtSVG) { t.Fatal("svg with prolog and comment rejected") } if SignatureMatches([]byte("hi"), ExtSVG) { t.Fatal("an html document accepted as svg") } if SignatureMatches([]byte(""), 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("")); 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("\n"), } { 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(``), 800, 600, true}, {"svg viewbox", []byte("\n"), 1200, 900, true}, {"svg percent length", []byte(``), 0, 0, false}, {"svg no size", []byte(``), 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) } }