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(``), wantMIME: MIMETypeSVG, }, { name: "SVG without declaration", data: []byte(``), wantMIME: MIMETypeSVG, }, { name: "SVG with whitespace", data: []byte(` `), wantMIME: MIMETypeSVG, }, { name: "SVG with BOM", data: append([]byte{0xEF, 0xBB, 0xBF}, []byte(``)...), 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", ``, true}, {"svg element", ``, true}, { "doctype", ``, true, }, {"with whitespace", ` `, true}, {"uppercase", ``, true}, {"not svg", ``, 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{ ` `, ` `, `` + ` `, } 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), ) } }