check / check (push) Canceled after 0s
magic.DetectFormat returns an imagerequest.Format. ValidateMagicBytes returns the format it checked against the upstream Content-Type. The cache stores that format in the source's row, so a variant made from a cached source uses it too. The proxy hands it to Process, which still refuses input libvips would load with another loader, such as an SVG passing as AVIF. Format.MIMEType declares each MIME string once, and the fetcher, the cache's variant read and the Accept matching use it. magic.MIMEType, magic.ImageFormat, MIMEToImageFormat, magic's SVG detection, detectFormat, formatFromString, FormatToMIME and Result.InputFormat are gone. Model: opus-5-5
133 lines
3.9 KiB
Go
133 lines
3.9 KiB
Go
// Package magic detects image formats from magic bytes and validates
|
|
// content against declared MIME types.
|
|
package magic
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// Magic byte errors.
|
|
var (
|
|
ErrUnknownFormat = errors.New("unknown image format")
|
|
ErrMagicByteMismatch = errors.New("content does not match declared Content-Type")
|
|
ErrNotEnoughData = errors.New("not enough data to detect format")
|
|
)
|
|
|
|
// MinMagicBytes is the minimum number of bytes needed to detect format.
|
|
const MinMagicBytes = 12
|
|
|
|
// Magic byte signatures for supported formats.
|
|
// These are effectively constants but Go doesn't support const slices.
|
|
//
|
|
//nolint:gochecknoglobals // immutable lookup data
|
|
var (
|
|
magicJPEG = []byte{0xFF, 0xD8, 0xFF}
|
|
magicPNG = []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
|
|
magicGIF = []byte{0x47, 0x49, 0x46, 0x38} // GIF8 (GIF87a or GIF89a)
|
|
magicWebP = []byte{0x52, 0x49, 0x46, 0x46} // RIFF (WebP starts with RIFF....WEBP)
|
|
// AVIF uses the ftyp box with brand "avif" or "avis"
|
|
// Format: size(4 bytes) + "ftyp" + brand(4 bytes)
|
|
magicFtyp = []byte{0x66, 0x74, 0x79, 0x70} // "ftyp"
|
|
// JPEG XL is a bare codestream or a codestream in a container
|
|
magicJXLCodestream = []byte{0xFF, 0x0A}
|
|
magicJXLContainer = []byte{
|
|
0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A,
|
|
}
|
|
)
|
|
|
|
// WebP identifier appears at offset 8 after RIFF header.
|
|
//
|
|
//nolint:gochecknoglobals // immutable lookup data
|
|
var webpIdent = []byte{0x57, 0x45, 0x42, 0x50} // "WEBP"
|
|
|
|
// AVIF brand identifiers.
|
|
//
|
|
//nolint:gochecknoglobals // immutable lookup data
|
|
var (
|
|
avifBrand = []byte{0x61, 0x76, 0x69, 0x66} // "avif"
|
|
avisBrand = []byte{0x61, 0x76, 0x69, 0x73} // "avis" (AVIF sequence)
|
|
)
|
|
|
|
// DetectFormat detects the image format from magic bytes. Data in none of
|
|
// the formats pixa reads, SVG included, is ErrUnknownFormat.
|
|
func DetectFormat(data []byte) (imagerequest.Format, error) {
|
|
if len(data) < MinMagicBytes {
|
|
return "", ErrNotEnoughData
|
|
}
|
|
|
|
// Check JPEG (FFD8FF)
|
|
if bytes.HasPrefix(data, magicJPEG) {
|
|
return imagerequest.FormatJPEG, nil
|
|
}
|
|
|
|
// Check PNG (89504E47 0D0A1A0A)
|
|
if bytes.HasPrefix(data, magicPNG) {
|
|
return imagerequest.FormatPNG, nil
|
|
}
|
|
|
|
// Check GIF (GIF87a or GIF89a)
|
|
if bytes.HasPrefix(data, magicGIF) {
|
|
return imagerequest.FormatGIF, nil
|
|
}
|
|
|
|
// Check WebP (RIFF....WEBP)
|
|
if bytes.HasPrefix(data, magicWebP) && len(data) >= 12 {
|
|
if bytes.Equal(data[8:12], webpIdent) {
|
|
return imagerequest.FormatWebP, nil
|
|
}
|
|
}
|
|
|
|
// Check AVIF (....ftypavif or ....ftypavis)
|
|
// The ftyp box can start at offset 4 (after size bytes)
|
|
if len(data) >= 12 && bytes.Equal(data[4:8], magicFtyp) {
|
|
brand := data[8:12]
|
|
if bytes.Equal(brand, avifBrand) || bytes.Equal(brand, avisBrand) {
|
|
return imagerequest.FormatAVIF, nil
|
|
}
|
|
}
|
|
|
|
// Check JPEG XL (FF0A, or the 12-byte container signature)
|
|
if bytes.HasPrefix(data, magicJXLCodestream) ||
|
|
bytes.HasPrefix(data, magicJXLContainer) {
|
|
return imagerequest.FormatJXL, nil
|
|
}
|
|
|
|
return "", ErrUnknownFormat
|
|
}
|
|
|
|
// ValidateMagicBytes detects the format of data from its magic bytes, and
|
|
// returns it when it is the format the declared MIME type names. Data in
|
|
// another format is ErrMagicByteMismatch.
|
|
func ValidateMagicBytes(
|
|
data []byte, declaredType string,
|
|
) (imagerequest.Format, error) {
|
|
detected, err := DetectFormat(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Normalize the declared type (remove parameters like charset)
|
|
normalizedDeclared := normalizeMIMEType(declaredType)
|
|
|
|
// Check if they match
|
|
if detected.MIMEType() != normalizedDeclared {
|
|
return "", ErrMagicByteMismatch
|
|
}
|
|
|
|
return detected, nil
|
|
}
|
|
|
|
// normalizeMIMEType extracts just the media type, removing parameters.
|
|
func normalizeMIMEType(mimeType string) string {
|
|
// Handle "image/jpeg; charset=utf-8" -> "image/jpeg"
|
|
if idx := strings.Index(mimeType, ";"); idx != -1 {
|
|
mimeType = mimeType[:idx]
|
|
}
|
|
|
|
return strings.TrimSpace(strings.ToLower(mimeType))
|
|
}
|