All checks were successful
check / check (push) Successful in 2m3s
Replace .golangci.yml with the canonical v2-schema config (default: all minus six disabled linters, lll 88, tests included) and bump every golangci-lint pin to v2.12.2: - Dockerfile: golangci/golangci-lint:v2.12.2-alpine (hash-pinned) - script/bootstrap: GOLANGCI_LINT_VERSION 2.12.2 with new linux-amd64/arm64 release-archive sha256 pins Fix all 747 findings the stricter config surfaces, with no behavior changes: t.Parallel() throughout the test suite, static sentinel errors and errors.Is comparisons, checked error returns, context propagation (contextcheck/noctx), 88-column wrapping, extracted constants and helpers for goconst/dupl/funlen/cyclop, exhaustive switch cases replicating existing defaults, and white-box test files renamed to *_internal_test.go for testpackage. Three nolint:tagliatelle directives preserve the existing snake_case JSON wire and on-disk metadata formats.
527 lines
12 KiB
Go
527 lines
12 KiB
Go
package magic
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Shared test fixture strings.
|
|
const (
|
|
testNameEmpty = "empty"
|
|
testMIMEJPEG = "image/jpeg"
|
|
testMIMEJPEGParams = "image/jpeg; charset=utf-8"
|
|
testMIMEPNG = "image/png"
|
|
testMIMEWebP = "image/webp"
|
|
testMIMEGIF = "image/gif"
|
|
testMIMEAVIF = "image/avif"
|
|
)
|
|
|
|
// pad appends zero bytes so data is comfortably above MinMagicBytes.
|
|
func pad(b ...byte) []byte {
|
|
return append(b, make([]byte, 100)...)
|
|
}
|
|
|
|
func TestDetectFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
jpeg := pad(0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01)
|
|
png := pad(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D)
|
|
gif87a := pad(0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0, 0, 0, 0, 0, 0)
|
|
gif89a := pad(0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0, 0, 0, 0, 0)
|
|
// RIFF + size placeholder + WEBP
|
|
webp := pad(0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50)
|
|
// box size + ftyp + brand
|
|
avif := pad(0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66)
|
|
avis := pad(0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x73)
|
|
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
wantMIME MIMEType
|
|
wantErr error
|
|
}{
|
|
{name: "JPEG", data: jpeg, wantMIME: MIMETypeJPEG},
|
|
{name: "PNG", data: png, wantMIME: MIMETypePNG},
|
|
{name: "GIF87a", data: gif87a, wantMIME: MIMETypeGIF},
|
|
{name: "GIF89a", data: gif89a, wantMIME: MIMETypeGIF},
|
|
{name: "WebP", data: webp, wantMIME: MIMETypeWebP},
|
|
{name: "AVIF", data: avif, wantMIME: MIMETypeAVIF},
|
|
{name: "AVIF sequence", data: avis, wantMIME: MIMETypeAVIF},
|
|
{
|
|
name: "SVG with XML declaration",
|
|
data: []byte(`<?xml version="1.0"?><svg></svg>`),
|
|
wantMIME: MIMETypeSVG,
|
|
},
|
|
{
|
|
name: "SVG without declaration",
|
|
data: []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`),
|
|
wantMIME: MIMETypeSVG,
|
|
},
|
|
{
|
|
name: "SVG with whitespace",
|
|
data: []byte(` <?xml version="1.0"?><svg></svg>`),
|
|
wantMIME: MIMETypeSVG,
|
|
},
|
|
{
|
|
name: "SVG with BOM",
|
|
data: append([]byte{0xEF, 0xBB, 0xBF}, []byte(`<svg></svg>`)...),
|
|
wantMIME: MIMETypeSVG,
|
|
},
|
|
{
|
|
name: "unknown format",
|
|
data: make([]byte, MinMagicBytes),
|
|
wantErr: ErrUnknownFormat,
|
|
},
|
|
{name: "too short", data: []byte{0xFF, 0xD8}, wantErr: ErrNotEnoughData},
|
|
{name: testNameEmpty, data: []byte{}, wantErr: ErrNotEnoughData},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := DetectFormat(tt.data)
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("DetectFormat() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
return
|
|
}
|
|
|
|
if got != tt.wantMIME {
|
|
t.Errorf("DetectFormat() = %v, want %v", got, tt.wantMIME)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateMagicBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
jpegData := pad(0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01)
|
|
pngData := pad(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D)
|
|
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
declaredType string
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "matching JPEG",
|
|
data: jpegData,
|
|
declaredType: testMIMEJPEG,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "matching JPEG with params",
|
|
data: jpegData,
|
|
declaredType: testMIMEJPEGParams,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "matching PNG",
|
|
data: pngData,
|
|
declaredType: testMIMEPNG,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "mismatched type",
|
|
data: jpegData,
|
|
declaredType: testMIMEPNG,
|
|
wantErr: ErrMagicByteMismatch,
|
|
},
|
|
{
|
|
name: "unknown data",
|
|
data: make([]byte, MinMagicBytes),
|
|
declaredType: testMIMEJPEG,
|
|
wantErr: ErrUnknownFormat,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := ValidateMagicBytes(tt.data, tt.declaredType)
|
|
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("ValidateMagicBytes() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSupportedMIMEType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
mimeType string
|
|
want bool
|
|
}{
|
|
{testMIMEJPEG, true},
|
|
{testMIMEPNG, true},
|
|
{testMIMEWebP, true},
|
|
{testMIMEGIF, true},
|
|
{testMIMEAVIF, true},
|
|
{"image/svg+xml", true},
|
|
{"IMAGE/JPEG", true},
|
|
{testMIMEJPEGParams, true},
|
|
{"image/tiff", false},
|
|
{"image/bmp", false},
|
|
{mimeOctetStream, false},
|
|
{"text/plain", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.mimeType, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := IsSupportedMIMEType(tt.mimeType); got != tt.want {
|
|
t.Errorf("IsSupportedMIMEType(%q) = %v, want %v", tt.mimeType, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPeekAndValidate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
jpegMagic := []byte{
|
|
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
|
|
}
|
|
pngMagic := []byte{
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
|
}
|
|
jpegData := slices.Concat(jpegMagic, []byte("rest of jpeg data"))
|
|
pngData := slices.Concat(pngMagic, []byte("rest of png data"))
|
|
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
declaredType string
|
|
wantErr bool
|
|
wantData []byte
|
|
}{
|
|
{
|
|
name: "valid JPEG",
|
|
data: jpegData,
|
|
declaredType: testMIMEJPEG,
|
|
wantErr: false,
|
|
wantData: jpegData,
|
|
},
|
|
{
|
|
name: "valid PNG",
|
|
data: pngData,
|
|
declaredType: testMIMEPNG,
|
|
wantErr: false,
|
|
wantData: pngData,
|
|
},
|
|
{
|
|
name: "mismatched type",
|
|
data: jpegData,
|
|
declaredType: testMIMEPNG,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := bytes.NewReader(tt.data)
|
|
|
|
result, err := PeekAndValidate(r, tt.declaredType)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("PeekAndValidate() expected error, got nil")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("PeekAndValidate() unexpected error = %v", err)
|
|
|
|
return
|
|
}
|
|
|
|
// Read all data from result reader
|
|
got, err := io.ReadAll(result)
|
|
if err != nil {
|
|
t.Errorf("Failed to read result: %v", err)
|
|
|
|
return
|
|
}
|
|
|
|
if !bytes.Equal(got, tt.wantData) {
|
|
t.Errorf(
|
|
"PeekAndValidate() data mismatch: got %d bytes, want %d bytes",
|
|
len(got), len(tt.wantData),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMIMEToImageFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
mimeType string
|
|
wantFormat ImageFormat
|
|
wantOk bool
|
|
}{
|
|
{testMIMEJPEG, FormatJPEG, true},
|
|
{testMIMEPNG, FormatPNG, true},
|
|
{testMIMEWebP, FormatWebP, true},
|
|
{testMIMEGIF, FormatGIF, true},
|
|
{testMIMEAVIF, FormatAVIF, true},
|
|
{"image/svg+xml", "", false}, // SVG doesn't convert to ImageFormat
|
|
{"image/tiff", "", false},
|
|
{"text/plain", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.mimeType, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, ok := MIMEToImageFormat(tt.mimeType)
|
|
|
|
if ok != tt.wantOk {
|
|
t.Errorf("MIMEToImageFormat(%q) ok = %v, want %v", tt.mimeType, ok, tt.wantOk)
|
|
}
|
|
|
|
if got != tt.wantFormat {
|
|
t.Errorf("MIMEToImageFormat(%q) = %v, want %v", tt.mimeType, got, tt.wantFormat)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestImageFormatToMIME(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
format ImageFormat
|
|
wantMIME string
|
|
}{
|
|
{FormatJPEG, testMIMEJPEG},
|
|
{FormatPNG, testMIMEPNG},
|
|
{FormatWebP, testMIMEWebP},
|
|
{FormatGIF, testMIMEGIF},
|
|
{FormatAVIF, testMIMEAVIF},
|
|
{FormatOriginal, mimeOctetStream},
|
|
{"unknown", mimeOctetStream},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.format), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := ImageFormatToMIME(tt.format)
|
|
|
|
if got != tt.wantMIME {
|
|
t.Errorf("ImageFormatToMIME(%q) = %v, want %v", tt.format, got, tt.wantMIME)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMIMEType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{testMIMEJPEG, testMIMEJPEG},
|
|
{"IMAGE/JPEG", testMIMEJPEG},
|
|
{testMIMEJPEGParams, testMIMEJPEG},
|
|
{" image/jpeg ", testMIMEJPEG},
|
|
{"image/jpeg; boundary=something", testMIMEJPEG},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := normalizeMIMEType(tt.input)
|
|
|
|
if got != tt.want {
|
|
t.Errorf("normalizeMIMEType(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectSVG(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
want bool
|
|
}{
|
|
{"xml declaration", `<?xml version="1.0"?><svg></svg>`, true},
|
|
{"svg element", `<svg xmlns="http://www.w3.org/2000/svg"></svg>`, true},
|
|
{
|
|
"doctype",
|
|
`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ` +
|
|
`"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`,
|
|
true,
|
|
},
|
|
{"with whitespace", `
|
|
<?xml version="1.0"?><svg></svg>`, true},
|
|
{"uppercase", `<SVG></SVG>`, true},
|
|
{"not svg", `<html></html>`, false},
|
|
{"random text", `hello world`, false},
|
|
{testNameEmpty, ``, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := detectSVG([]byte(tt.data))
|
|
|
|
if got != tt.want {
|
|
t.Errorf("detectSVG(%q) = %v, want %v", tt.data, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSkipBOM(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
want []byte
|
|
}{
|
|
{"with BOM", []byte{0xEF, 0xBB, 0xBF, 'h', 'e', 'l', 'l', 'o'}, []byte("hello")},
|
|
{"without BOM", []byte("hello"), []byte("hello")},
|
|
{testNameEmpty, []byte{}, []byte{}},
|
|
{"only BOM", []byte{0xEF, 0xBB, 0xBF}, []byte{}},
|
|
{"partial BOM", []byte{0xEF, 0xBB}, []byte{0xEF, 0xBB}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := skipBOM(tt.data)
|
|
|
|
if !bytes.Equal(got, tt.want) {
|
|
t.Errorf("skipBOM() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRealWorldSVGPatterns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test various real-world SVG patterns
|
|
svgPatterns := []string{
|
|
`<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
<circle cx="50" cy="50" r="40"/>
|
|
</svg>`,
|
|
`<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
</svg>`,
|
|
`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ` +
|
|
`"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">` + `
|
|
<svg xmlns="http://www.w3.org/2000/svg">
|
|
</svg>`,
|
|
}
|
|
|
|
for i, pattern := range svgPatterns {
|
|
data := []byte(pattern)
|
|
if len(data) < MinMagicBytes {
|
|
// Pad short SVGs for detection
|
|
data = append(data, make([]byte, MinMagicBytes-len(data))...)
|
|
}
|
|
|
|
got, err := DetectFormat(data)
|
|
if err != nil {
|
|
t.Errorf("Pattern %d: DetectFormat() error = %v", i, err)
|
|
|
|
continue
|
|
}
|
|
|
|
if got != MIMETypeSVG {
|
|
t.Errorf("Pattern %d: DetectFormat() = %v, want %v", i, got, MIMETypeSVG)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetectFormatRIFFNotWebP(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// RIFF container but not WebP (e.g., WAV file)
|
|
wavData := []byte{
|
|
0x52, 0x49, 0x46, 0x46, // RIFF
|
|
0x00, 0x00, 0x00, 0x00, // file size
|
|
0x57, 0x41, 0x56, 0x45, // WAVE (not WEBP)
|
|
}
|
|
|
|
_, err := DetectFormat(wavData)
|
|
if !errors.Is(err, ErrUnknownFormat) {
|
|
t.Errorf("DetectFormat(WAV) error = %v, want %v", err, ErrUnknownFormat)
|
|
}
|
|
}
|
|
|
|
func TestDetectFormatFtypNotAVIF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// ftyp container but not AVIF (e.g., MP4)
|
|
mp4Data := []byte{
|
|
0x00, 0x00, 0x00, 0x1C, // box size
|
|
0x66, 0x74, 0x79, 0x70, // ftyp
|
|
0x69, 0x73, 0x6F, 0x6D, // isom brand (not avif)
|
|
}
|
|
|
|
_, err := DetectFormat(mp4Data)
|
|
if !errors.Is(err, ErrUnknownFormat) {
|
|
t.Errorf("DetectFormat(MP4) error = %v, want %v", err, ErrUnknownFormat)
|
|
}
|
|
}
|
|
|
|
func TestPeekAndValidatePreservesReader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Ensure that after PeekAndValidate, we can read the complete
|
|
// original content
|
|
originalContent := append(
|
|
[]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D},
|
|
[]byte(strings.Repeat("PNG IDAT chunk data here ", 100))...,
|
|
)
|
|
|
|
r := bytes.NewReader(originalContent)
|
|
|
|
validated, err := PeekAndValidate(r, testMIMEPNG)
|
|
if err != nil {
|
|
t.Fatalf("PeekAndValidate() error = %v", err)
|
|
}
|
|
|
|
// Read everything from the validated reader
|
|
got, err := io.ReadAll(validated)
|
|
if err != nil {
|
|
t.Fatalf("io.ReadAll() error = %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(got, originalContent) {
|
|
t.Errorf(
|
|
"Content mismatch: got %d bytes, want %d bytes",
|
|
len(got), len(originalContent),
|
|
)
|
|
}
|
|
}
|