chore: update golangci-lint to v2.12.2 with canonical config
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.
This commit is contained in:
2026-08-07 17:10:27 +00:00
parent 5d0b5f864e
commit 23506df609
55 changed files with 2584 additions and 1863 deletions

View File

@@ -46,6 +46,10 @@ const (
// MinMagicBytes is the minimum number of bytes needed to detect format.
const MinMagicBytes = 12
// mimeOctetStream is the fallback MIME type for formats without a
// specific MIME type.
const mimeOctetStream = "application/octet-stream"
// Magic byte signatures for supported formats.
// These are effectively constants but Go doesn't support const slices.
//
@@ -190,14 +194,17 @@ func IsSupportedMIMEType(mimeType string) bool {
func PeekAndValidate(r io.Reader, declaredType string) (io.Reader, error) {
// Read minimum bytes for detection
buf := make([]byte, MinMagicBytes)
n, err := io.ReadFull(r, buf)
if err != nil && err != io.ErrUnexpectedEOF {
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return nil, err
}
buf = buf[:n]
// Validate magic bytes
if err := ValidateMagicBytes(buf, declaredType); err != nil {
err = ValidateMagicBytes(buf, declaredType)
if err != nil {
return nil, err
}
@@ -219,6 +226,9 @@ func MIMEToImageFormat(mimeType string) (ImageFormat, bool) {
return FormatGIF, true
case MIMETypeAVIF:
return FormatAVIF, true
case MIMETypeSVG:
// SVG has no corresponding output format.
return "", false
default:
return "", false
}
@@ -237,7 +247,10 @@ func ImageFormatToMIME(format ImageFormat) string {
return string(MIMETypeGIF)
case FormatAVIF:
return string(MIMETypeAVIF)
case FormatOriginal:
// Original format passes content through unchanged.
return mimeOctetStream
default:
return "application/octet-stream"
return mimeOctetStream
}
}

View File

@@ -2,121 +2,90 @@ 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: append([]byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01}, make([]byte, 100)...),
wantMIME: MIMETypeJPEG,
wantErr: nil,
},
{
name: "PNG",
data: append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D}, make([]byte, 100)...),
wantMIME: MIMETypePNG,
wantErr: nil,
},
{
name: "GIF87a",
data: append([]byte{0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, make([]byte, 100)...),
wantMIME: MIMETypeGIF,
wantErr: nil,
},
{
name: "GIF89a",
data: append([]byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, make([]byte, 100)...),
wantMIME: MIMETypeGIF,
wantErr: nil,
},
{
name: "WebP",
data: append([]byte{
0x52, 0x49, 0x46, 0x46, // RIFF
0x00, 0x00, 0x00, 0x00, // file size (placeholder)
0x57, 0x45, 0x42, 0x50, // WEBP
}, make([]byte, 100)...),
wantMIME: MIMETypeWebP,
wantErr: nil,
},
{
name: "AVIF",
data: append([]byte{
0x00, 0x00, 0x00, 0x1C, // box size
0x66, 0x74, 0x79, 0x70, // ftyp
0x61, 0x76, 0x69, 0x66, // avif brand
}, make([]byte, 100)...),
wantMIME: MIMETypeAVIF,
wantErr: nil,
},
{
name: "AVIF sequence",
data: append([]byte{
0x00, 0x00, 0x00, 0x1C, // box size
0x66, 0x74, 0x79, 0x70, // ftyp
0x61, 0x76, 0x69, 0x73, // avis brand
}, make([]byte, 100)...),
wantMIME: MIMETypeAVIF,
wantErr: nil,
},
{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,
wantErr: nil,
},
{
name: "SVG without declaration",
data: []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`),
wantMIME: MIMETypeSVG,
wantErr: nil,
},
{
name: "SVG with whitespace",
data: []byte(` <?xml version="1.0"?><svg></svg>`),
wantMIME: MIMETypeSVG,
wantErr: nil,
},
{
name: "SVG with BOM",
data: append([]byte{0xEF, 0xBB, 0xBF}, []byte(`<svg></svg>`)...),
wantMIME: MIMETypeSVG,
wantErr: nil,
},
{
name: "unknown format",
data: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
wantMIME: "",
wantErr: ErrUnknownFormat,
},
{
name: "too short",
data: []byte{0xFF, 0xD8},
wantMIME: "",
wantErr: ErrNotEnoughData,
},
{
name: "empty",
data: []byte{},
wantMIME: "",
wantErr: ErrNotEnoughData,
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) {
got, err := DetectFormat(tt.data)
t.Parallel()
if err != tt.wantErr {
got, err := DetectFormat(tt.data)
if !errors.Is(err, tt.wantErr) {
t.Errorf("DetectFormat() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -130,8 +99,10 @@ func TestDetectFormat(t *testing.T) {
}
func TestValidateMagicBytes(t *testing.T) {
jpegData := append([]byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01}, make([]byte, 100)...)
pngData := append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D}, make([]byte, 100)...)
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
@@ -142,40 +113,42 @@ func TestValidateMagicBytes(t *testing.T) {
{
name: "matching JPEG",
data: jpegData,
declaredType: "image/jpeg",
declaredType: testMIMEJPEG,
wantErr: nil,
},
{
name: "matching JPEG with params",
data: jpegData,
declaredType: "image/jpeg; charset=utf-8",
declaredType: testMIMEJPEGParams,
wantErr: nil,
},
{
name: "matching PNG",
data: pngData,
declaredType: "image/png",
declaredType: testMIMEPNG,
wantErr: nil,
},
{
name: "mismatched type",
data: jpegData,
declaredType: "image/png",
declaredType: testMIMEPNG,
wantErr: ErrMagicByteMismatch,
},
{
name: "unknown data",
data: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
declaredType: "image/jpeg",
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 err != tt.wantErr {
if !errors.Is(err, tt.wantErr) {
t.Errorf("ValidateMagicBytes() error = %v, wantErr %v", err, tt.wantErr)
}
})
@@ -183,27 +156,31 @@ func TestValidateMagicBytes(t *testing.T) {
}
func TestIsSupportedMIMEType(t *testing.T) {
t.Parallel()
tests := []struct {
mimeType string
want bool
}{
{"image/jpeg", true},
{"image/png", true},
{"image/webp", true},
{"image/gif", true},
{"image/avif", true},
{testMIMEJPEG, true},
{testMIMEPNG, true},
{testMIMEWebP, true},
{testMIMEGIF, true},
{testMIMEAVIF, true},
{"image/svg+xml", true},
{"IMAGE/JPEG", true},
{"image/jpeg; charset=utf-8", true},
{testMIMEJPEGParams, true},
{"image/tiff", false},
{"image/bmp", false},
{"application/octet-stream", 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)
}
@@ -212,8 +189,16 @@ func TestIsSupportedMIMEType(t *testing.T) {
}
func TestPeekAndValidate(t *testing.T) {
jpegData := append([]byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01}, []byte("rest of jpeg data")...)
pngData := append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D}, []byte("rest of png data")...)
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
@@ -225,30 +210,32 @@ func TestPeekAndValidate(t *testing.T) {
{
name: "valid JPEG",
data: jpegData,
declaredType: "image/jpeg",
declaredType: testMIMEJPEG,
wantErr: false,
wantData: jpegData,
},
{
name: "valid PNG",
data: pngData,
declaredType: "image/png",
declaredType: testMIMEPNG,
wantErr: false,
wantData: pngData,
},
{
name: "mismatched type",
data: jpegData,
declaredType: "image/png",
declaredType: testMIMEPNG,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
result, err := PeekAndValidate(r, tt.declaredType)
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")
@@ -272,23 +259,28 @@ func TestPeekAndValidate(t *testing.T) {
}
if !bytes.Equal(got, tt.wantData) {
t.Errorf("PeekAndValidate() data mismatch: got %d bytes, want %d bytes", len(got), len(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
}{
{"image/jpeg", FormatJPEG, true},
{"image/png", FormatPNG, true},
{"image/webp", FormatWebP, true},
{"image/gif", FormatGIF, true},
{"image/avif", FormatAVIF, true},
{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},
@@ -296,6 +288,8 @@ func TestMIMEToImageFormat(t *testing.T) {
for _, tt := range tests {
t.Run(tt.mimeType, func(t *testing.T) {
t.Parallel()
got, ok := MIMEToImageFormat(tt.mimeType)
if ok != tt.wantOk {
@@ -310,21 +304,25 @@ func TestMIMEToImageFormat(t *testing.T) {
}
func TestImageFormatToMIME(t *testing.T) {
t.Parallel()
tests := []struct {
format ImageFormat
wantMIME string
}{
{FormatJPEG, "image/jpeg"},
{FormatPNG, "image/png"},
{FormatWebP, "image/webp"},
{FormatGIF, "image/gif"},
{FormatAVIF, "image/avif"},
{FormatOriginal, "application/octet-stream"},
{"unknown", "application/octet-stream"},
{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 {
@@ -335,19 +333,23 @@ func TestImageFormatToMIME(t *testing.T) {
}
func TestNormalizeMIMEType(t *testing.T) {
t.Parallel()
tests := []struct {
input string
want string
}{
{"image/jpeg", "image/jpeg"},
{"IMAGE/JPEG", "image/jpeg"},
{"image/jpeg; charset=utf-8", "image/jpeg"},
{" image/jpeg ", "image/jpeg"},
{"image/jpeg; boundary=something", "image/jpeg"},
{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 {
@@ -358,6 +360,8 @@ func TestNormalizeMIMEType(t *testing.T) {
}
func TestDetectSVG(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data string
@@ -365,17 +369,24 @@ func TestDetectSVG(t *testing.T) {
}{
{"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},
{
"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},
{"empty", ``, 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 {
@@ -386,6 +397,8 @@ func TestDetectSVG(t *testing.T) {
}
func TestSkipBOM(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
@@ -393,13 +406,15 @@ func TestSkipBOM(t *testing.T) {
}{
{"with BOM", []byte{0xEF, 0xBB, 0xBF, 'h', 'e', 'l', 'l', 'o'}, []byte("hello")},
{"without BOM", []byte("hello"), []byte("hello")},
{"empty", []byte{}, []byte{}},
{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) {
@@ -410,6 +425,8 @@ func TestSkipBOM(t *testing.T) {
}
func TestRealWorldSVGPatterns(t *testing.T) {
t.Parallel()
// Test various real-world SVG patterns
svgPatterns := []string{
`<?xml version="1.0" encoding="UTF-8"?>
@@ -419,7 +436,8 @@ func TestRealWorldSVGPatterns(t *testing.T) {
`<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">
`<!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>`,
}
@@ -445,6 +463,8 @@ func TestRealWorldSVGPatterns(t *testing.T) {
}
func TestDetectFormatRIFFNotWebP(t *testing.T) {
t.Parallel()
// RIFF container but not WebP (e.g., WAV file)
wavData := []byte{
0x52, 0x49, 0x46, 0x46, // RIFF
@@ -453,12 +473,14 @@ func TestDetectFormatRIFFNotWebP(t *testing.T) {
}
_, err := DetectFormat(wavData)
if err != ErrUnknownFormat {
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
@@ -467,20 +489,24 @@ func TestDetectFormatFtypNotAVIF(t *testing.T) {
}
_, err := DetectFormat(mp4Data)
if err != ErrUnknownFormat {
if !errors.Is(err, ErrUnknownFormat) {
t.Errorf("DetectFormat(MP4) error = %v, want %v", err, ErrUnknownFormat)
}
}
func TestPeekAndValidatePreservesReader(t *testing.T) {
// Ensure that after PeekAndValidate, we can read the complete original content
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, "image/png")
validated, err := PeekAndValidate(r, testMIMEPNG)
if err != nil {
t.Fatalf("PeekAndValidate() error = %v", err)
}
@@ -492,6 +518,9 @@ func TestPeekAndValidatePreservesReader(t *testing.T) {
}
if !bytes.Equal(got, originalContent) {
t.Errorf("Content mismatch: got %d bytes, want %d bytes", len(got), len(originalContent))
t.Errorf(
"Content mismatch: got %d bytes, want %d bytes",
len(got), len(originalContent),
)
}
}