소스 검색

Always fallback in C-D, GetTypeMap -> GetTypeByName

Viktor Sokolov 1 개월 전
부모
커밋
f1dd92672a

+ 2 - 2
config/configurators/configurators.go

@@ -147,7 +147,7 @@ func ImageTypes(it *[]imagetype.Type, name string) error {
 		// For every part passed through the environment variable,
 		// check if it matches any of the image types defined in
 		// the imagetype package or return error.
-		t, ok := imagetype.GetTypeMap()[part]
+		t, ok := imagetype.GetTypeByName(part)
 		if !ok {
 			return fmt.Errorf("unknown image format: %s", part)
 		}
@@ -180,7 +180,7 @@ func ImageTypesQuality(m map[imagetype.Type]int, name string) error {
 			return fmt.Errorf("invalid quality: %s", p)
 		}
 
-		t, ok := imagetype.GetTypeMap()[imgtypeStr]
+		t, ok := imagetype.GetTypeByName(imgtypeStr)
 		if !ok {
 			return fmt.Errorf("unknown image format: %s", imgtypeStr)
 		}

+ 76 - 0
httpheaders/cdv.go

@@ -0,0 +1,76 @@
+package httpheaders
+
+import (
+	"fmt"
+	"mime"
+	"path/filepath"
+	"strings"
+)
+
+const (
+	// fallbackStem is used when the stem cannot be determined from the URL.
+	fallbackStem = "image"
+
+	// Content-Disposition header format
+	contentDispositionsHeader = "%s; filename=\"%s%s\""
+
+	// "inline" disposition types
+	inlineDisposition = "inline"
+
+	// "attachment" disposition type
+	attachmentDisposition = "attachment"
+)
+
+// ContentDispositionValue generates the content-disposition header value.
+//
+// It uses the following priorities:
+// 1. By default, it uses the filename and extension from the URL.
+// 2. If `filename` is provided, it overrides the URL filename.
+// 3. If `contentType` is provided, it tries to determine the extension from the content type.
+// 4. If `ext` is provided, it overrides any extension determined from the URL or header.
+// 5. If the filename is still empty, it uses fallback stem.
+func ContentDispositionValue(url, filename, ext, contentType string, returnAttachment bool) string {
+	// By default, let's use the URL filename and extension
+	_, urlFilename := filepath.Split(url)
+	urlExt := filepath.Ext(urlFilename)
+
+	var rStem string
+
+	// Avoid strings.TrimSuffix allocation by using slice operation
+	if urlExt != "" {
+		rStem = urlFilename[:len(urlFilename)-len(urlExt)]
+	} else {
+		rStem = urlFilename
+	}
+
+	var rExt = urlExt
+
+	// If filename is provided explicitly, use it
+	if len(filename) > 0 {
+		rStem = filename
+	}
+
+	// If ext is provided explicitly, use it
+	if len(ext) > 0 {
+		rExt = ext
+	} else if len(contentType) > 0 {
+		exts, err := mime.ExtensionsByType(contentType)
+		if err == nil && len(exts) != 0 {
+			rExt = exts[0]
+		}
+	}
+
+	// If fallback is requested, and filename is still empty, override it with fallbackStem
+	if len(rStem) == 0 {
+		rStem = fallbackStem
+	}
+
+	disposition := inlineDisposition
+
+	// Create the content-disposition header value
+	if returnAttachment {
+		disposition = attachmentDisposition
+	}
+
+	return fmt.Sprintf(contentDispositionsHeader, disposition, strings.ReplaceAll(rStem, `"`, "%22"), rExt)
+}

+ 5 - 17
httpheaders/headers_test.go → httpheaders/cdv_test.go

@@ -6,15 +6,14 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestValue(t *testing.T) {
-	// Test cases for Value function that generates content-disposition headers
+func TestContentDispositionValue(t *testing.T) {
+	// Test cases for ContentDispositionValue function that generates content-disposition headers
 	tests := []struct {
 		name             string
 		url              string
 		filename         string
 		ext              string
 		returnAttachment bool
-		fallback         bool
 		expected         string
 		contentType      string
 	}{
@@ -25,7 +24,6 @@ func TestValue(t *testing.T) {
 			ext:              "",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
 			expected:         "inline; filename=\"test.jpg\"",
 		},
 		{
@@ -35,8 +33,7 @@ func TestValue(t *testing.T) {
 			ext:              "",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
-			expected:         "inline; filename=\"\"",
+			expected:         "inline; filename=\"image\"",
 		},
 		{
 			name:             "EmptyFilenameWithExt",
@@ -45,8 +42,7 @@ func TestValue(t *testing.T) {
 			ext:              ".png",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
-			expected:         "inline; filename=\".png\"",
+			expected:         "inline; filename=\"image.png\"",
 		},
 		{
 			name:             "EmptyFilenameWithFilenameAndExt",
@@ -55,7 +51,6 @@ func TestValue(t *testing.T) {
 			ext:              ".png",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
 			expected:         "inline; filename=\"example.png\"",
 		},
 		{
@@ -65,7 +60,6 @@ func TestValue(t *testing.T) {
 			ext:              ".jpg",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
 			expected:         "inline; filename=\"example.jpg\"",
 		},
 		{
@@ -75,7 +69,6 @@ func TestValue(t *testing.T) {
 			ext:              ".jpg",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
 			expected:         "inline; filename=\"face.jpg\"",
 		},
 		{
@@ -85,7 +78,6 @@ func TestValue(t *testing.T) {
 			ext:              ".jpg",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         false,
 			expected:         "inline; filename=\"face.jpg\"",
 		},
 		{
@@ -95,7 +87,6 @@ func TestValue(t *testing.T) {
 			ext:              ".png",
 			contentType:      "",
 			returnAttachment: false,
-			fallback:         true,
 			expected:         "inline; filename=\"image.png\"",
 		},
 		{
@@ -105,7 +96,6 @@ func TestValue(t *testing.T) {
 			ext:              "",
 			contentType:      "",
 			returnAttachment: true,
-			fallback:         false,
 			expected:         "attachment; filename=\"test.jpg\"",
 		},
 		{
@@ -114,7 +104,6 @@ func TestValue(t *testing.T) {
 			filename:         "my\"file",
 			ext:              ".png",
 			returnAttachment: false,
-			fallback:         false,
 			contentType:      "",
 			expected:         "inline; filename=\"my%22file.png\"",
 		},
@@ -125,14 +114,13 @@ func TestValue(t *testing.T) {
 			ext:              "",
 			contentType:      "image/png",
 			returnAttachment: false,
-			fallback:         false,
 			expected:         "inline; filename=\"my%22file.png\"",
 		},
 	}
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			result := ContentDispositionValue(tc.url, tc.filename, tc.ext, tc.contentType, tc.returnAttachment, tc.fallback)
+			result := ContentDispositionValue(tc.url, tc.filename, tc.ext, tc.contentType, tc.returnAttachment)
 			require.Equal(t, tc.expected, result)
 		})
 	}

+ 0 - 75
httpheaders/headers.go

@@ -2,13 +2,6 @@
 // Thanks, Matt Robenolt!
 package httpheaders
 
-import (
-	"fmt"
-	"mime"
-	"path/filepath"
-	"strings"
-)
-
 const (
 	Accept                          = "Accept"
 	AcceptCharset                   = "Accept-Charset"
@@ -74,71 +67,3 @@ const (
 	XResultHeight                   = "X-Result-Height"
 	XOriginContentLength            = "X-Origin-Content-Length"
 )
-
-const (
-	// fallbackStem is used when the stem cannot be determined from the URL.
-	fallbackStem = "image"
-
-	// Content-Disposition header format
-	contentDispositionsHeader = "%s; filename=\"%s%s\""
-
-	// "inline" disposition types
-	inlineDisposition = "inline"
-
-	// "attachment" disposition type
-	attachmentDisposition = "attachment"
-)
-
-// Value generates the content-disposition header value.
-//
-// It uses the following priorities:
-// 1. By default, it uses the filename and extension from the URL.
-// 2. If `filename` is provided, it overrides the URL filename.
-// 3. If `contentType` is provided, it tries to determine the extension from the content type.
-// 4. If `ext` is provided, it overrides any extension determined from the URL or header.
-// 5. If `fallback` is true and the filename is still empty, it uses fallback stem.
-func ContentDispositionValue(url, filename, ext, contentType string, returnAttachment, fallback bool) string {
-	// By default, let's use the URL filename and extension
-	_, urlFilename := filepath.Split(url)
-	urlExt := filepath.Ext(urlFilename)
-
-	var rStem string
-
-	// Avoid strings.TrimSuffix allocation by using slice operation
-	if urlExt != "" {
-		rStem = urlFilename[:len(urlFilename)-len(urlExt)]
-	} else {
-		rStem = urlFilename
-	}
-
-	var rExt = urlExt
-
-	// If filename is provided explicitly, use it
-	if len(filename) > 0 {
-		rStem = filename
-	}
-
-	// If ext is provided explicitly, use it
-	if len(ext) > 0 {
-		rExt = ext
-	} else if len(contentType) > 0 {
-		exts, err := mime.ExtensionsByType(contentType)
-		if err == nil && len(exts) != 0 {
-			rExt = exts[0]
-		}
-	}
-
-	// If fallback is requested, and filename is still empty, override it with fallbackStem
-	if fallback && len(rStem) == 0 {
-		rStem = fallbackStem
-	}
-
-	disposition := inlineDisposition
-
-	// Create the content-disposition header value
-	if returnAttachment {
-		disposition = attachmentDisposition
-	}
-
-	return fmt.Sprintf(contentDispositionsHeader, disposition, strings.ReplaceAll(rStem, `"`, "%22"), rExt)
-}

+ 0 - 89
imagedetect/detect.go

@@ -1,89 +0,0 @@
-package imagedetect
-
-import (
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/bufreader"
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-const (
-	// maxDetectionLimit is maximum bytes detectors allowed to read from the source
-	maxDetectionLimit = 32768
-)
-
-// Detect attempts to detect the image type from a reader.
-// It first tries magic byte detection, then custom detectors in registration order
-func Detect(r io.Reader) (imagetype.Type, error) {
-	br := bufreader.New(io.LimitReader(r, maxDetectionLimit))
-
-	for _, fn := range registry.detectors {
-		br.Rewind()
-		if typ, err := fn(br); err == nil && typ != imagetype.Unknown {
-			return typ, nil
-		}
-	}
-
-	return imagetype.Unknown, newUnknownFormatError()
-}
-
-// hasMagicBytes checks if the data matches a magic byte signature
-// Supports '?' characters in signature which match any byte
-func hasMagicBytes(data []byte, magic []byte) bool {
-	if len(data) < len(magic) {
-		return false
-	}
-
-	for i, c := range magic {
-		if c != data[i] && c != '?' {
-			return false
-		}
-	}
-	return true
-}
-
-// init registers default magic bytes for common image formats
-func init() {
-	// JPEG magic bytes
-	RegisterMagicBytes([]byte("\xff\xd8"), imagetype.JPEG)
-
-	// JXL magic bytes
-	//
-	// NOTE: for "naked" jxl (0xff 0x0a) there is no way to ensure this is a JXL file, except to fully
-	// decode it. The data starts right after it, no additional marker bytes are provided.
-	// We stuck with the potential false positives here.
-	RegisterMagicBytes([]byte{0xff, 0x0a}, imagetype.JXL)                                                             // JXL codestream (can't use string due to 0x0a)
-	RegisterMagicBytes([]byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A}, imagetype.JXL) // JXL container (has null bytes)
-
-	// PNG magic bytes
-	RegisterMagicBytes([]byte("\x89PNG\r\n\x1a\n"), imagetype.PNG)
-
-	// WEBP magic bytes (RIFF container with WEBP fourcc) - using wildcard for size
-	RegisterMagicBytes([]byte("RIFF????WEBP"), imagetype.WEBP)
-
-	// GIF magic bytes
-	RegisterMagicBytes([]byte("GIF8?a"), imagetype.GIF)
-
-	// ICO magic bytes
-	RegisterMagicBytes([]byte{0, 0, 1, 0}, imagetype.ICO) // ICO (has null bytes)
-
-	// HEIC/HEIF magic bytes with wildcards for size
-	RegisterMagicBytes([]byte("????ftypheic"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftypheix"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftyphevc"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftypheim"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftypheis"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftyphevm"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftyphevs"), imagetype.HEIC)
-	RegisterMagicBytes([]byte("????ftypmif1"), imagetype.HEIC)
-
-	// AVIF magic bytes
-	RegisterMagicBytes([]byte("????ftypavif"), imagetype.AVIF)
-
-	// BMP magic bytes
-	RegisterMagicBytes([]byte("BM"), imagetype.BMP)
-
-	// TIFF magic bytes (little-endian and big-endian)
-	RegisterMagicBytes([]byte("II*\x00"), imagetype.TIFF) // Little-endian
-	RegisterMagicBytes([]byte("MM\x00*"), imagetype.TIFF) // Big-endian
-}

+ 0 - 41
imagedetect/detect_test.go

@@ -1,41 +0,0 @@
-package imagedetect
-
-import (
-	"os"
-	"testing"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-	"github.com/stretchr/testify/require"
-)
-
-func TestDetect(t *testing.T) {
-	tests := []struct {
-		name string
-		file string
-		want imagetype.Type
-	}{
-		{"JPEG", "../testdata/test-images/jpg/jpg.jpg", imagetype.JPEG},
-		{"JXL", "../testdata/test-images/jxl/jxl.jxl", imagetype.JXL},
-		{"PNG", "../testdata/test-images/png/png.png", imagetype.PNG},
-		{"WEBP", "../testdata/test-images/webp/webp.webp", imagetype.WEBP},
-		{"GIF", "../testdata/test-images/gif/gif.gif", imagetype.GIF},
-		{"ICO", "../testdata/test-images/ico/png-256x256.ico", imagetype.ICO},
-		{"SVG", "../testdata/test-images/svg/svg.svg", imagetype.SVG},
-		{"HEIC", "../testdata/test-images/heif/heif.heif", imagetype.HEIC},
-		{"BMP", "../testdata/test-images/bmp/24-bpp.bmp", imagetype.BMP},
-		{"TIFF", "../testdata/test-images/tiff/tiff.tiff", imagetype.TIFF},
-		{"SVG", "../testdata/test-images/svg/svg.svg", imagetype.SVG},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			f, err := os.Open(tt.file)
-			require.NoError(t, err)
-			defer f.Close()
-
-			got, err := Detect(f)
-			require.NoError(t, err)
-			require.Equal(t, tt.want, got)
-		})
-	}
-}

+ 0 - 50
imagedetect/registry.go

@@ -1,50 +0,0 @@
-package imagedetect
-
-import (
-	"github.com/imgproxy/imgproxy/v3/bufreader"
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-// DetectFunc is a function that detects the image type from byte data
-type DetectFunc func(r bufreader.ReadPeeker) (imagetype.Type, error)
-
-// Registry manages the registration and execution of image type detectors
-type Registry struct {
-	detectors []DetectFunc
-}
-
-// Global registry instance
-var registry = &Registry{}
-
-// RegisterDetector registers a custom detector function
-// Detectors are tried in the order they were registered
-func RegisterDetector(detector DetectFunc, bytesNeeded int) {
-	registry.RegisterDetector(detector, bytesNeeded)
-}
-
-// RegisterMagicBytes registers magic bytes for a specific image type
-// Magic byte detectors are always tried before custom detectors
-func RegisterMagicBytes(signature []byte, typ imagetype.Type) {
-	registry.RegisterMagicBytes(signature, typ)
-}
-
-// RegisterDetector registers a custom detector function on this registry instance
-func (r *Registry) RegisterDetector(detector DetectFunc, bytesNeeded int) {
-	r.detectors = append(r.detectors, detector)
-}
-
-// RegisterMagicBytes registers magic bytes for a specific image type on this registry instance
-func (r *Registry) RegisterMagicBytes(signature []byte, typ imagetype.Type) {
-	r.detectors = append(r.detectors, func(r bufreader.ReadPeeker) (imagetype.Type, error) {
-		b, err := r.Peek(len(signature))
-		if err != nil {
-			return imagetype.Unknown, err
-		}
-
-		if hasMagicBytes(b, signature) {
-			return typ, nil
-		}
-
-		return imagetype.Unknown, nil
-	})
-}

+ 0 - 47
imagedetect/registry_test.go

@@ -1,47 +0,0 @@
-package imagedetect
-
-import (
-	"testing"
-
-	"github.com/imgproxy/imgproxy/v3/bufreader"
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-	"github.com/stretchr/testify/require"
-)
-
-func TestRegisterDetector(t *testing.T) {
-	// Create a test registry to avoid interfering with global state
-	testRegistry := &Registry{}
-
-	// Create a test detector function
-	testDetector := func(r bufreader.ReadPeeker) (imagetype.Type, error) {
-		b, err := r.Peek(2)
-		if err != nil {
-			return imagetype.Unknown, err
-		}
-		if len(b) >= 2 && b[0] == 0xFF && b[1] == 0xD8 {
-			return imagetype.JPEG, nil
-		}
-		return imagetype.Unknown, newUnknownFormatError()
-	}
-
-	// Register the detector using the method
-	testRegistry.RegisterDetector(testDetector, 64)
-
-	// Verify the detector is registered
-	require.Len(t, testRegistry.detectors, 1)
-	require.NotNil(t, testRegistry.detectors[0])
-}
-
-func TestRegisterMagicBytes(t *testing.T) {
-	// Create a test registry to avoid interfering with global state
-	testRegistry := &Registry{}
-
-	require.Empty(t, testRegistry.detectors)
-
-	// Register magic bytes for JPEG using the method
-	jpegMagic := []byte{0xFF, 0xD8}
-	testRegistry.RegisterMagicBytes(jpegMagic, imagetype.JPEG)
-
-	// Verify the magic bytes are registered
-	require.Len(t, testRegistry.detectors, 1)
-}

+ 8 - 2
imagetype/registry.go

@@ -61,8 +61,8 @@ func GetTypeDesc(t Type) *TypeDesc {
 }
 
 // GetTypes returns all registered image types.
-func GetTypeMap() map[string]Type {
-	return globalRegistry.typesByName
+func GetTypeByName(name string) (Type, bool) {
+	return globalRegistry.GetTypeByName(name)
 }
 
 // RegisterType registers a new image type in this registry.
@@ -98,6 +98,12 @@ func (r *registry) GetTypeDesc(t Type) *TypeDesc {
 	return r.types[t-1]
 }
 
+// GetTypeByName returns Type by it's name
+func (r *registry) GetTypeByName(name string) (Type, bool) {
+	typ, ok := r.typesByName[name]
+	return typ, ok
+}
+
 // RegisterDetector registers a custom detector function
 // Detectors are tried in the order they were registered
 func RegisterDetector(detector DetectFunc) {

+ 3 - 3
options/processing_options.go

@@ -570,7 +570,7 @@ func applyFormatQualityOption(po *ProcessingOptions, args []string) error {
 	}
 
 	for i := 0; i < argsLen; i += 2 {
-		f, ok := imagetype.GetTypeMap()[args[i]]
+		f, ok := imagetype.GetTypeByName(args[i])
 		if !ok {
 			return newOptionArgumentError("Invalid image format: %s", args[i])
 		}
@@ -754,7 +754,7 @@ func applyFormatOption(po *ProcessingOptions, args []string) error {
 		return newOptionArgumentError("Invalid format arguments: %v", args)
 	}
 
-	if f, ok := imagetype.GetTypeMap()[args[0]]; ok {
+	if f, ok := imagetype.GetTypeByName(args[0]); ok {
 		po.Format = f
 	} else {
 		return newOptionArgumentError("Invalid image format: %s", args[0])
@@ -775,7 +775,7 @@ func applyCacheBusterOption(po *ProcessingOptions, args []string) error {
 
 func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) error {
 	for _, format := range args {
-		if f, ok := imagetype.GetTypeMap()[format]; ok {
+		if f, ok := imagetype.GetTypeByName(format); ok {
 			po.SkipProcessingFormats = append(po.SkipProcessingFormats, f)
 		} else {
 			return newOptionArgumentError("Invalid image format in skip processing: %s", format)

+ 0 - 1
processing_handler.go

@@ -164,7 +164,6 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
 		resultData.Format().Ext(),
 		originHeaders.Get(httpheaders.ContentType),
 		po.ReturnAttachment,
-		true,
 	)
 
 	rw.Header().Set(httpheaders.ContentType, resultData.Format().Mime())

+ 0 - 1
stream.go

@@ -96,7 +96,6 @@ func streamOriginImage(ctx context.Context, reqID string, r *http.Request, rw ht
 			"",
 			rw.Header().Get(httpheaders.ContentType),
 			po.ReturnAttachment,
-			false, // no fallback in this case
 		)
 		rw.Header().Set("Content-Disposition", contentDisposition)
 	}