Ver Fonte

IMG-57: isSecurityOptionAllowed; shared parse fns (#1528)

* isSecurityOptionAllowed; shared parse fns

* We are not the same
Victor Sokolov há 2 semanas atrás
pai
commit
f8133cd0b1

+ 558 - 0
options/apply.go

@@ -0,0 +1,558 @@
+package options
+
+import (
+	"encoding/base64"
+	"slices"
+	"strconv"
+	"time"
+
+	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/vips"
+	log "github.com/sirupsen/logrus"
+)
+
+func applyWidthOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveInt(&po.Width, "width", args...)
+}
+
+func applyHeightOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveInt(&po.Height, "height", args...)
+}
+
+func applyMinWidthOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveInt(&po.MinWidth, "min width", args...)
+}
+
+func applyMinHeightOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveInt(&po.MinHeight, "min height", args...)
+}
+
+func applyEnlargeOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.Enlarge, "enlarge", args...)
+}
+
+func applyExtendOption(po *ProcessingOptions, args []string) error {
+	return parseExtend(&po.Extend, "extend", args)
+}
+
+func applyExtendAspectRatioOption(po *ProcessingOptions, args []string) error {
+	return parseExtend(&po.ExtendAspectRatio, "extend_aspect_ratio", args)
+}
+
+func applySizeOption(po *ProcessingOptions, args []string) (err error) {
+	if err = ensureMaxArgs("size", args, 7); err != nil {
+		return
+	}
+
+	if len(args) >= 1 && len(args[0]) > 0 {
+		if err = applyWidthOption(po, args[0:1]); err != nil {
+			return
+		}
+	}
+
+	if len(args) >= 2 && len(args[1]) > 0 {
+		if err = applyHeightOption(po, args[1:2]); err != nil {
+			return
+		}
+	}
+
+	if len(args) >= 3 && len(args[2]) > 0 {
+		if err = applyEnlargeOption(po, args[2:3]); err != nil {
+			return
+		}
+	}
+
+	if len(args) >= 4 && len(args[3]) > 0 {
+		if err = applyExtendOption(po, args[3:]); err != nil {
+			return
+		}
+	}
+
+	return nil
+}
+
+func applyResizingTypeOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("resizing type", args, 1); err != nil {
+		return err
+	}
+
+	if r, ok := resizeTypes[args[0]]; ok {
+		po.ResizingType = r
+	} else {
+		return newOptionArgumentError("Invalid resize type: %s", args[0])
+	}
+
+	return nil
+}
+
+func applyResizeOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("resize", args, 8); err != nil {
+		return err
+	}
+
+	if len(args[0]) > 0 {
+		if err := applyResizingTypeOption(po, args[0:1]); err != nil {
+			return err
+		}
+	}
+
+	if len(args) > 1 {
+		if err := applySizeOption(po, args[1:]); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func applyZoomOption(po *ProcessingOptions, args []string) error {
+	nArgs := len(args)
+
+	if err := ensureMaxArgs("zoom", args, 2); err != nil {
+		return err
+	}
+
+	var z float64
+	if err := parsePositiveNonZeroFloat64(&z, "zoom", args[0]); err != nil {
+		return err
+	}
+
+	po.ZoomWidth = z
+	po.ZoomHeight = z
+
+	if nArgs > 1 {
+		if err := parsePositiveNonZeroFloat64(&po.ZoomHeight, "zoom height", args[1]); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func applyDprOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveNonZeroFloat64(&po.Dpr, "dpr", args...)
+}
+
+func applyGravityOption(po *ProcessingOptions, args []string) error {
+	return parseGravity(&po.Gravity, "gravity", args, cropGravityTypes)
+}
+
+func applyCropOption(po *ProcessingOptions, args []string) error {
+	if err := parsePositiveFloat64(&po.Crop.Width, "crop width", args[0]); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		if err := parsePositiveFloat64(&po.Crop.Height, "crop height", args[1]); err != nil {
+			return err
+		}
+	}
+
+	if len(args) > 2 {
+		return parseGravity(&po.Crop.Gravity, "crop gravity", args[2:], cropGravityTypes)
+	}
+
+	return nil
+}
+
+func applyPaddingOption(po *ProcessingOptions, args []string) error {
+	nArgs := len(args)
+
+	if nArgs < 1 || nArgs > 4 {
+		return newOptionArgumentError("Invalid padding arguments: %v", args)
+	}
+
+	po.Padding.Enabled = true
+
+	if nArgs > 0 && len(args[0]) > 0 {
+		if err := parsePositiveInt(&po.Padding.Top, "padding top (+all)", args[0]); err != nil {
+			return err
+		}
+		po.Padding.Right = po.Padding.Top
+		po.Padding.Bottom = po.Padding.Top
+		po.Padding.Left = po.Padding.Top
+	}
+
+	if nArgs > 1 && len(args[1]) > 0 {
+		if err := parsePositiveInt(&po.Padding.Right, "padding right (+left)", args[1]); err != nil {
+			return err
+		}
+		po.Padding.Left = po.Padding.Right
+	}
+
+	if nArgs > 2 && len(args[2]) > 0 {
+		if err := parsePositiveInt(&po.Padding.Bottom, "padding bottom", args[2]); err != nil {
+			return err
+		}
+	}
+
+	if nArgs > 3 && len(args[3]) > 0 {
+		if err := parsePositiveInt(&po.Padding.Left, "padding left", args[3]); err != nil {
+			return err
+		}
+	}
+
+	if po.Padding.Top == 0 && po.Padding.Right == 0 && po.Padding.Bottom == 0 && po.Padding.Left == 0 {
+		po.Padding.Enabled = false
+	}
+
+	return nil
+}
+
+func applyTrimOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("trim", args, 4); err != nil {
+		return err
+	}
+
+	nArgs := len(args)
+
+	if err := parseFloat64(&po.Trim.Threshold, "trim threshold", args[0]); err != nil {
+		return err
+	}
+
+	po.Trim.Enabled = true
+
+	if nArgs > 1 && len(args[1]) > 0 {
+		if c, err := vips.ColorFromHex(args[1]); err == nil {
+			po.Trim.Color = c
+			po.Trim.Smart = false
+		} else {
+			return newOptionArgumentError("Invalid trim color: %s", args[1])
+		}
+	}
+
+	if nArgs > 2 && len(args[2]) > 0 {
+		if err := parseBool(&po.Trim.EqualHor, "trim equal horizontal", args[2]); err != nil {
+			return err
+		}
+	}
+
+	if nArgs > 3 && len(args[3]) > 0 {
+		if err := parseBool(&po.Trim.EqualVer, "trim equal vertical", args[3]); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func applyRotateOption(po *ProcessingOptions, args []string) error {
+	if err := parseInt(&po.Rotate, "rotate", args...); err != nil {
+		return err
+	}
+
+	if po.Rotate%90 != 0 {
+		return newOptionArgumentError("Rotation angle must be a multiple of 90")
+	}
+
+	return nil
+}
+
+func applyQualityOption(po *ProcessingOptions, args []string) error {
+	return parseQualityInt(&po.Quality, "quality", args...)
+}
+
+func applyFormatQualityOption(po *ProcessingOptions, args []string) error {
+	argsLen := len(args)
+	if len(args)%2 != 0 {
+		return newOptionArgumentError("Missing quality for: %s", args[argsLen-1])
+	}
+
+	for i := 0; i < argsLen; i += 2 {
+		f, ok := imagetype.GetTypeByName(args[i])
+		if !ok {
+			return newOptionArgumentError("Invalid image format: %s", args[i])
+		}
+
+		var q int
+		if err := parseQualityInt(&q, args[i]+" quality", args[i+1]); err != nil {
+			return err
+		}
+
+		po.FormatQuality[f] = q
+	}
+
+	return nil
+}
+
+func applyMaxBytesOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveInt(&po.MaxBytes, "max_bytes", args...)
+}
+
+func applyBackgroundOption(po *ProcessingOptions, args []string) error {
+	switch len(args) {
+	case 1:
+		if len(args[0]) == 0 {
+			po.Flatten = false
+		} else if c, err := vips.ColorFromHex(args[0]); err == nil {
+			po.Flatten = true
+			po.Background = c
+		} else {
+			return newOptionArgumentError("Invalid background argument: %s", err)
+		}
+
+	case 3:
+		po.Flatten = true
+
+		if r, err := strconv.ParseUint(args[0], 10, 8); err == nil && r <= 255 {
+			po.Background.R = uint8(r)
+		} else {
+			return newOptionArgumentError("Invalid background red channel: %s", args[0])
+		}
+
+		if g, err := strconv.ParseUint(args[1], 10, 8); err == nil && g <= 255 {
+			po.Background.G = uint8(g)
+		} else {
+			return newOptionArgumentError("Invalid background green channel: %s", args[1])
+		}
+
+		if b, err := strconv.ParseUint(args[2], 10, 8); err == nil && b <= 255 {
+			po.Background.B = uint8(b)
+		} else {
+			return newOptionArgumentError("Invalid background blue channel: %s", args[2])
+		}
+
+	default:
+		return newOptionArgumentError("Invalid background arguments: %v", args)
+	}
+
+	return nil
+}
+
+func applyBlurOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveNonZeroFloat32(&po.Blur, "blur", args...)
+}
+
+func applySharpenOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveNonZeroFloat32(&po.Sharpen, "sharpen", args...)
+}
+
+func applyPixelateOption(po *ProcessingOptions, args []string) error {
+	return parsePositiveInt(&po.Pixelate, "pixelate", args...)
+}
+
+func applyWatermarkOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("watermark", args, 7); err != nil {
+		return err
+	}
+
+	if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 {
+		po.Watermark.Enabled = o > 0
+		po.Watermark.Opacity = o
+	} else {
+		return newOptionArgumentError("Invalid watermark opacity: %s", args[0])
+	}
+
+	if len(args) > 1 && len(args[1]) > 0 {
+		if g, ok := gravityTypes[args[1]]; ok && slices.Contains(watermarkGravityTypes, g) {
+			po.Watermark.Position.Type = g
+		} else {
+			return newOptionArgumentError("Invalid watermark position: %s", args[1])
+		}
+	}
+
+	if len(args) > 2 && len(args[2]) > 0 {
+		if err := parseFloat64(&po.Watermark.Position.X, "watermark X offset", args[2]); err != nil {
+			return err
+		}
+	}
+
+	if len(args) > 3 && len(args[3]) > 0 {
+		if err := parseFloat64(&po.Watermark.Position.Y, "watermark Y offset", args[3]); err != nil {
+			return err
+		}
+	}
+
+	if len(args) > 4 && len(args[4]) > 0 {
+		if err := parsePositiveNonZeroFloat64(&po.Watermark.Scale, "watermark scale", args[4]); err == nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func applyFormatOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("format", args, 1); err != nil {
+		return err
+	}
+
+	if f, ok := imagetype.GetTypeByName(args[0]); ok {
+		po.Format = f
+	} else {
+		return newOptionArgumentError("Invalid image format: %s", args[0])
+	}
+
+	return nil
+}
+
+func applyCacheBusterOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("cache buster", args, 1); err != nil {
+		return err
+	}
+
+	po.CacheBuster = args[0]
+
+	return nil
+}
+
+func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) error {
+	for _, format := range args {
+		if f, ok := imagetype.GetTypeByName(format); ok {
+			po.SkipProcessingFormats = append(po.SkipProcessingFormats, f)
+		} else {
+			return newOptionArgumentError("Invalid image format in skip processing: %s", format)
+		}
+	}
+
+	return nil
+}
+
+func applyRawOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.Raw, "raw", args...)
+}
+
+func applyFilenameOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("filename", args, 2); err != nil {
+		return err
+	}
+
+	po.Filename = args[0]
+
+	if len(args) == 1 {
+		return nil
+	}
+
+	var b bool
+	if err := parseBool(&b, "filename is base64", args[1]); err != nil || !b {
+		return err
+	}
+
+	decoded, err := base64.RawURLEncoding.DecodeString(po.Filename)
+	if err != nil {
+		return newOptionArgumentError("Invalid filename encoding: %s", err)
+	}
+
+	po.Filename = string(decoded)
+
+	return nil
+}
+
+func applyExpiresOption(po *ProcessingOptions, args []string) error {
+	if err := ensureMaxArgs("expires", args, 1); err != nil {
+		return err
+	}
+
+	timestamp, err := strconv.ParseInt(args[0], 10, 64)
+	if err != nil {
+		return newOptionArgumentError("Invalid expires argument: %v", args[0])
+	}
+
+	if timestamp > 0 && timestamp < time.Now().Unix() {
+		return newOptionArgumentError("Expired URL")
+	}
+
+	expires := time.Unix(timestamp, 0)
+	po.Expires = &expires
+
+	return nil
+}
+
+func applyStripMetadataOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.StripMetadata, "strip metadata", args...)
+}
+
+func applyKeepCopyrightOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.KeepCopyright, "keep copyright", args...)
+}
+
+func applyStripColorProfileOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.StripColorProfile, "strip color profile", args...)
+}
+
+func applyAutoRotateOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.AutoRotate, "auto rotate", args...)
+}
+
+func applyEnforceThumbnailOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.EnforceThumbnail, "enforce thumbnail", args...)
+}
+
+func applyReturnAttachmentOption(po *ProcessingOptions, args []string) error {
+	return parseBool(&po.ReturnAttachment, "return_attachment", args...)
+}
+
+func applyMaxSrcResolutionOption(po *ProcessingOptions, args []string) error {
+	if err := po.isSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	var v float64
+	if err := parsePositiveNonZeroFloat64(&v, "max_src_resolution", args...); err != nil {
+		return err
+	}
+
+	po.SecurityOptions.MaxSrcResolution = int(v * 1000000)
+
+	return nil
+}
+
+func applyMaxSrcFileSizeOption(po *ProcessingOptions, args []string) error {
+	if err := po.isSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	return parseInt(&po.SecurityOptions.MaxSrcFileSize, "max_src_file_size", args...)
+}
+
+func applyMaxAnimationFramesOption(po *ProcessingOptions, args []string) error {
+	if err := po.isSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	return parsePositiveNonZeroInt(&po.SecurityOptions.MaxAnimationFrames, "max_animation_frames", args...)
+}
+
+func applyMaxAnimationFrameResolutionOption(po *ProcessingOptions, args []string) error {
+	if err := po.isSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	var v float64
+	if err := parseFloat64(&v, "max_animation_frame_resolution", args...); err != nil {
+		return err
+	}
+
+	po.SecurityOptions.MaxAnimationFrameResolution = int(v * 1000000)
+
+	return nil
+}
+
+func applyMaxResultDimensionOption(po *ProcessingOptions, args []string) error {
+	if err := po.isSecurityOptionsAllowed(); err != nil {
+		return err
+	}
+
+	return parseInt(&po.SecurityOptions.MaxResultDimension, "max_result_dimension", args...)
+}
+
+func applyPresetOption(f *Factory, po *ProcessingOptions, args []string, usedPresets ...string) error {
+	for _, preset := range args {
+		if p, ok := f.presets[preset]; ok {
+			if slices.Contains(usedPresets, preset) {
+				log.Warningf("Recursive preset usage is detected: %s", preset)
+				continue
+			}
+
+			po.UsedPresets = append(po.UsedPresets, preset)
+
+			if err := f.applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
+				return err
+			}
+		} else {
+			return newOptionArgumentError("Unknown preset: %s", preset)
+		}
+	}
+
+	return nil
+}

+ 6 - 0
options/config.go

@@ -54,6 +54,8 @@ type Config struct {
 	BaseURL                   string           // Base URL for relative URLs
 	URLReplacements           []URLReplacement // URL replacement rules
 	Base64URLIncludesFilename bool             // Whether base64 URLs include filename
+
+	AllowSecurityOptions bool // Whether to allow security options in URLs
 }
 
 // NewDefaultConfig creates a new default configuration for options processing
@@ -92,6 +94,8 @@ func NewDefaultConfig() Config {
 		ArgumentsSeparator:        ":",
 		BaseURL:                   "",
 		Base64URLIncludesFilename: false,
+
+		AllowSecurityOptions: false,
 	}
 }
 
@@ -138,6 +142,8 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 	c.URLReplacements = slices.Clone(config.URLReplacements)
 	c.Base64URLIncludesFilename = config.Base64URLIncludesFilename
 
+	c.AllowSecurityOptions = config.AllowSecurityOptions
+
 	return c, nil
 }
 

+ 27 - 3
options/errors.go

@@ -3,14 +3,16 @@ package options
 import (
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 )
 
 type (
-	InvalidURLError     string
-	UnknownOptionError  string
-	OptionArgumentError string
+	InvalidURLError      string
+	UnknownOptionError   string
+	OptionArgumentError  string
+	SecurityOptionsError struct{}
 )
 
 func newInvalidURLError(format string, args ...interface{}) error {
@@ -58,3 +60,25 @@ func newOptionArgumentError(format string, args ...interface{}) error {
 }
 
 func (e OptionArgumentError) Error() string { return string(e) }
+
+func newSecurityOptionsError() error {
+	return ierrors.Wrap(
+		SecurityOptionsError{},
+		1,
+		ierrors.WithStatusCode(http.StatusForbidden),
+		ierrors.WithPublicMessage("Invalid URL"),
+		ierrors.WithShouldReport(false),
+	)
+}
+
+func (e SecurityOptionsError) Error() string { return "Security processing options are not allowed" }
+
+// newInvalidArgsError creates a standardized error for invalid arguments
+func newInvalidArgsError(name string, args []string, expected ...string) error {
+	msg := "Invalid %s arguments: %s"
+	if len(expected) > 0 {
+		msg += " (expected " + strings.Join(expected, ", ") + ")"
+	}
+
+	return newOptionArgumentError(msg, name, args, expected[0])
+}

+ 212 - 0
options/parse.go

@@ -0,0 +1,212 @@
+package options
+
+import (
+	"slices"
+	"strconv"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// ensureMaxArgs checks if the number of arguments is as expected
+func ensureMaxArgs(name string, args []string, max int) error {
+	if len(args) > max {
+		return newInvalidArgsError(name, args)
+	}
+	return nil
+}
+
+// parseBool parses a boolean option value and warns if the value is invalid
+func parseBool(value *bool, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	b, err := strconv.ParseBool(args[0])
+
+	if err != nil {
+		log.Warningf("%s `%s` is not a valid boolean value. Treated as false", name, args[0])
+	}
+
+	*value = b
+	return nil
+}
+
+// parseFloat64 parses a float64 option value
+func parseFloat64(value *float64, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	f, err := strconv.ParseFloat(args[0], 64)
+	if err != nil {
+		return newInvalidArgsError(name, args)
+	}
+
+	*value = f
+	return nil
+}
+
+// parsePositiveFloat64 parses a positive float64 option value
+func parsePositiveFloat64(value *float64, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	f, err := strconv.ParseFloat(args[0], 64)
+	if err != nil || f < 0 {
+		return newInvalidArgsError(name, args, "positive number or 0")
+	}
+	*value = f
+	return nil
+}
+
+// parsePositiveFloat64 parses a positive float64 option value
+func parsePositiveNonZeroFloat64(value *float64, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	f, err := strconv.ParseFloat(args[0], 64)
+	if err != nil || f <= 0 {
+		return newInvalidArgsError(name, args, "positive number")
+	}
+	*value = f
+	return nil
+}
+
+// parsePositiveFloat32 parses a positive float32 option value
+func parsePositiveNonZeroFloat32(value *float32, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	f, err := strconv.ParseFloat(args[0], 32)
+	if err != nil || f <= 0 {
+		return newInvalidArgsError(name, args, "positive number")
+	}
+	*value = float32(f)
+	return nil
+}
+
+// parseInt parses a positive integer option value
+func parseInt(value *int, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	i, err := strconv.Atoi(args[0])
+	if err != nil {
+		return newOptionArgumentError(name, args)
+	}
+	*value = i
+	return nil
+}
+
+// parsePositiveNonZeroInt parses a positive non-zero integer option value
+func parsePositiveNonZeroInt(value *int, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	i, err := strconv.Atoi(args[0])
+	if err != nil || i <= 0 {
+		return newInvalidArgsError(name, args, "positive number")
+	}
+	*value = i
+	return nil
+}
+
+// parsePositiveInt parses a positive integer option value
+func parsePositiveInt(value *int, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	i, err := strconv.Atoi(args[0])
+	if err != nil || i < 0 {
+		return newOptionArgumentError("Invalid %s arguments: %s (expected positive number)", name, args)
+	}
+	*value = i
+	return nil
+}
+
+// parseQualityInt parses a quality integer option value (1-100)
+func parseQualityInt(value *int, name string, args ...string) error {
+	if err := ensureMaxArgs(name, args, 1); err != nil {
+		return err
+	}
+
+	i, err := strconv.Atoi(args[0])
+	if err != nil || i < 1 || i > 100 {
+		return newInvalidArgsError(name, args, "number in range 1-100")
+	}
+	*value = i
+	return nil
+}
+
+func isGravityOffcetValid(gravity GravityType, offset float64) bool {
+	return gravity != GravityFocusPoint || (offset >= 0 && offset <= 1)
+}
+
+func parseGravity(g *GravityOptions, name string, args []string, allowedTypes []GravityType) error {
+	nArgs := len(args)
+
+	if t, ok := gravityTypes[args[0]]; ok && slices.Contains(allowedTypes, t) {
+		g.Type = t
+	} else {
+		return newOptionArgumentError("Invalid %s: %s", name, args[0])
+	}
+
+	switch g.Type {
+	case GravitySmart:
+		if nArgs > 1 {
+			return newInvalidArgsError(name, args)
+		}
+		g.X, g.Y = 0.0, 0.0
+
+	case GravityFocusPoint:
+		if nArgs != 3 {
+			return newInvalidArgsError(name, args)
+		}
+		fallthrough
+
+	default:
+		if nArgs > 3 {
+			return newInvalidArgsError(name, args)
+		}
+
+		if nArgs > 1 {
+			if x, err := strconv.ParseFloat(args[1], 64); err == nil && isGravityOffcetValid(g.Type, x) {
+				g.X = x
+			} else {
+				return newOptionArgumentError("Invalid %s X: %s", name, args[1])
+			}
+		}
+
+		if nArgs > 2 {
+			if y, err := strconv.ParseFloat(args[2], 64); err == nil && isGravityOffcetValid(g.Type, y) {
+				g.Y = y
+			} else {
+				return newOptionArgumentError("Invalid %s Y: %s", name, args[2])
+			}
+		}
+	}
+
+	return nil
+}
+
+func parseExtend(opts *ExtendOptions, name string, args []string) error {
+	if err := ensureMaxArgs(name, args, 4); err != nil {
+		return err
+	}
+
+	if err := parseBool(&opts.Enabled, name+" enabled", args[0]); err != nil {
+		return err
+	}
+
+	if len(args) > 1 {
+		return parseGravity(&opts.Gravity, name+" gravity", args[1:], extendGravityTypes)
+	}
+
+	return nil
+}

+ 17 - 805
options/processing_options.go

@@ -1,7 +1,6 @@
 package options
 
 import (
-	"encoding/base64"
 	"maps"
 	"net/http"
 	"slices"
@@ -9,8 +8,6 @@ import (
 	"strings"
 	"time"
 
-	log "github.com/sirupsen/logrus"
-
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imath"
@@ -61,6 +58,9 @@ func (wo WatermarkOptions) ShouldReplicate() bool {
 }
 
 type ProcessingOptions struct {
+	defaultOptions *ProcessingOptions
+	config         *Config
+
 	ResizingType      ResizeType
 	Width             int
 	Height            int
@@ -115,13 +115,12 @@ type ProcessingOptions struct {
 	UsedPresets []string
 
 	SecurityOptions security.Options
-
-	defaultQuality int
-	defaultOptions *ProcessingOptions
 }
 
 func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
 	po := ProcessingOptions{
+		config: config,
+
 		ResizingType:      ResizeFit,
 		Width:             0,
 		Height:            0,
@@ -153,8 +152,6 @@ func newDefaultProcessingOptions(config *Config, security *security.Checker) *Pr
 		SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
 
 		SecurityOptions: security.NewOptions(),
-
-		defaultQuality: config.Quality,
 	}
 
 	return &po
@@ -168,7 +165,7 @@ func (po *ProcessingOptions) GetQuality() int {
 	}
 
 	if q == 0 {
-		q = po.defaultQuality
+		q = po.config.Quality
 	}
 
 	return q
@@ -214,799 +211,6 @@ func (po *ProcessingOptions) clone() *ProcessingOptions {
 	return &clone
 }
 
-func parseDimension(d *int, name, arg string) error {
-	if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
-		*d = v
-	} else {
-		return newOptionArgumentError("Invalid %s: %s", name, arg)
-	}
-
-	return nil
-}
-
-func parseBoolOption(str string) bool {
-	b, err := strconv.ParseBool(str)
-
-	if err != nil {
-		log.Warningf("`%s` is not a valid boolean value. Treated as false", str)
-	}
-
-	return b
-}
-
-func isGravityOffcetValid(gravity GravityType, offset float64) bool {
-	return gravity != GravityFocusPoint || (offset >= 0 && offset <= 1)
-}
-
-func parseGravity(g *GravityOptions, name string, args []string, allowedTypes []GravityType) error {
-	nArgs := len(args)
-
-	if t, ok := gravityTypes[args[0]]; ok && slices.Contains(allowedTypes, t) {
-		g.Type = t
-	} else {
-		return newOptionArgumentError("Invalid %s: %s", name, args[0])
-	}
-
-	switch g.Type {
-	case GravitySmart:
-		if nArgs > 1 {
-			return newOptionArgumentError("Invalid %s arguments: %v", name, args)
-		}
-		g.X, g.Y = 0.0, 0.0
-
-	case GravityFocusPoint:
-		if nArgs != 3 {
-			return newOptionArgumentError("Invalid %s arguments: %v", name, args)
-		}
-		fallthrough
-
-	default:
-		if nArgs > 3 {
-			return newOptionArgumentError("Invalid %s arguments: %v", name, args)
-		}
-
-		if nArgs > 1 {
-			if x, err := strconv.ParseFloat(args[1], 64); err == nil && isGravityOffcetValid(g.Type, x) {
-				g.X = x
-			} else {
-				return newOptionArgumentError("Invalid %s X: %s", name, args[1])
-			}
-		}
-
-		if nArgs > 2 {
-			if y, err := strconv.ParseFloat(args[2], 64); err == nil && isGravityOffcetValid(g.Type, y) {
-				g.Y = y
-			} else {
-				return newOptionArgumentError("Invalid %s Y: %s", name, args[2])
-			}
-		}
-	}
-
-	return nil
-}
-
-func parseExtend(opts *ExtendOptions, name string, args []string) error {
-	if len(args) > 4 {
-		return newOptionArgumentError("Invalid %s arguments: %v", name, args)
-	}
-
-	opts.Enabled = parseBoolOption(args[0])
-
-	if len(args) > 1 {
-		return parseGravity(&opts.Gravity, name+" gravity", args[1:], extendGravityTypes)
-	}
-
-	return nil
-}
-
-func applyWidthOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid width arguments: %v", args)
-	}
-
-	return parseDimension(&po.Width, "width", args[0])
-}
-
-func applyHeightOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid height arguments: %v", args)
-	}
-
-	return parseDimension(&po.Height, "height", args[0])
-}
-
-func applyMinWidthOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid min width arguments: %v", args)
-	}
-
-	return parseDimension(&po.MinWidth, "min width", args[0])
-}
-
-func applyMinHeightOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid min height arguments: %v", args)
-	}
-
-	return parseDimension(&po.MinHeight, " min height", args[0])
-}
-
-func applyEnlargeOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid enlarge arguments: %v", args)
-	}
-
-	po.Enlarge = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyExtendOption(po *ProcessingOptions, args []string) error {
-	return parseExtend(&po.Extend, "extend", args)
-}
-
-func applyExtendAspectRatioOption(po *ProcessingOptions, args []string) error {
-	return parseExtend(&po.ExtendAspectRatio, "extend_aspect_ratio", args)
-}
-
-func applySizeOption(po *ProcessingOptions, args []string) (err error) {
-	if len(args) > 7 {
-		return newOptionArgumentError("Invalid size arguments: %v", args)
-	}
-
-	if len(args) >= 1 && len(args[0]) > 0 {
-		if err = applyWidthOption(po, args[0:1]); err != nil {
-			return
-		}
-	}
-
-	if len(args) >= 2 && len(args[1]) > 0 {
-		if err = applyHeightOption(po, args[1:2]); err != nil {
-			return
-		}
-	}
-
-	if len(args) >= 3 && len(args[2]) > 0 {
-		if err = applyEnlargeOption(po, args[2:3]); err != nil {
-			return
-		}
-	}
-
-	if len(args) >= 4 && len(args[3]) > 0 {
-		if err = applyExtendOption(po, args[3:]); err != nil {
-			return
-		}
-	}
-
-	return nil
-}
-
-func applyResizingTypeOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid resizing type arguments: %v", args)
-	}
-
-	if r, ok := resizeTypes[args[0]]; ok {
-		po.ResizingType = r
-	} else {
-		return newOptionArgumentError("Invalid resize type: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyResizeOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 8 {
-		return newOptionArgumentError("Invalid resize arguments: %v", args)
-	}
-
-	if len(args[0]) > 0 {
-		if err := applyResizingTypeOption(po, args[0:1]); err != nil {
-			return err
-		}
-	}
-
-	if len(args) > 1 {
-		if err := applySizeOption(po, args[1:]); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func applyZoomOption(po *ProcessingOptions, args []string) error {
-	nArgs := len(args)
-
-	if nArgs > 2 {
-		return newOptionArgumentError("Invalid zoom arguments: %v", args)
-	}
-
-	if z, err := strconv.ParseFloat(args[0], 64); err == nil && z > 0 {
-		po.ZoomWidth = z
-		po.ZoomHeight = z
-	} else {
-		return newOptionArgumentError("Invalid zoom value: %s", args[0])
-	}
-
-	if nArgs > 1 {
-		if z, err := strconv.ParseFloat(args[1], 64); err == nil && z > 0 {
-			po.ZoomHeight = z
-		} else {
-			return newOptionArgumentError("Invalid zoom value: %s", args[1])
-		}
-	}
-
-	return nil
-}
-
-func applyDprOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid dpr arguments: %v", args)
-	}
-
-	if d, err := strconv.ParseFloat(args[0], 64); err == nil && d > 0 {
-		po.Dpr = d
-	} else {
-		return newOptionArgumentError("Invalid dpr: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyGravityOption(po *ProcessingOptions, args []string) error {
-	return parseGravity(&po.Gravity, "gravity", args, cropGravityTypes)
-}
-
-func applyCropOption(po *ProcessingOptions, args []string) error {
-	if w, err := strconv.ParseFloat(args[0], 64); err == nil && w >= 0 {
-		po.Crop.Width = w
-	} else {
-		return newOptionArgumentError("Invalid crop width: %s", args[0])
-	}
-
-	if len(args) > 1 {
-		if h, err := strconv.ParseFloat(args[1], 64); err == nil && h >= 0 {
-			po.Crop.Height = h
-		} else {
-			return newOptionArgumentError("Invalid crop height: %s", args[1])
-		}
-	}
-
-	if len(args) > 2 {
-		return parseGravity(&po.Crop.Gravity, "crop gravity", args[2:], cropGravityTypes)
-	}
-
-	return nil
-}
-
-func applyPaddingOption(po *ProcessingOptions, args []string) error {
-	nArgs := len(args)
-
-	if nArgs < 1 || nArgs > 4 {
-		return newOptionArgumentError("Invalid padding arguments: %v", args)
-	}
-
-	po.Padding.Enabled = true
-
-	if nArgs > 0 && len(args[0]) > 0 {
-		if err := parseDimension(&po.Padding.Top, "padding top (+all)", args[0]); err != nil {
-			return err
-		}
-		po.Padding.Right = po.Padding.Top
-		po.Padding.Bottom = po.Padding.Top
-		po.Padding.Left = po.Padding.Top
-	}
-
-	if nArgs > 1 && len(args[1]) > 0 {
-		if err := parseDimension(&po.Padding.Right, "padding right (+left)", args[1]); err != nil {
-			return err
-		}
-		po.Padding.Left = po.Padding.Right
-	}
-
-	if nArgs > 2 && len(args[2]) > 0 {
-		if err := parseDimension(&po.Padding.Bottom, "padding bottom", args[2]); err != nil {
-			return err
-		}
-	}
-
-	if nArgs > 3 && len(args[3]) > 0 {
-		if err := parseDimension(&po.Padding.Left, "padding left", args[3]); err != nil {
-			return err
-		}
-	}
-
-	if po.Padding.Top == 0 && po.Padding.Right == 0 && po.Padding.Bottom == 0 && po.Padding.Left == 0 {
-		po.Padding.Enabled = false
-	}
-
-	return nil
-}
-
-func applyTrimOption(po *ProcessingOptions, args []string) error {
-	nArgs := len(args)
-
-	if nArgs > 4 {
-		return newOptionArgumentError("Invalid trim arguments: %v", args)
-	}
-
-	if t, err := strconv.ParseFloat(args[0], 64); err == nil && t >= 0 {
-		po.Trim.Enabled = true
-		po.Trim.Threshold = t
-	} else {
-		return newOptionArgumentError("Invalid trim threshold: %s", args[0])
-	}
-
-	if nArgs > 1 && len(args[1]) > 0 {
-		if c, err := vips.ColorFromHex(args[1]); err == nil {
-			po.Trim.Color = c
-			po.Trim.Smart = false
-		} else {
-			return newOptionArgumentError("Invalid trim color: %s", args[1])
-		}
-	}
-
-	if nArgs > 2 && len(args[2]) > 0 {
-		po.Trim.EqualHor = parseBoolOption(args[2])
-	}
-
-	if nArgs > 3 && len(args[3]) > 0 {
-		po.Trim.EqualVer = parseBoolOption(args[3])
-	}
-
-	return nil
-}
-
-func applyRotateOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid rotate arguments: %v", args)
-	}
-
-	if r, err := strconv.Atoi(args[0]); err == nil && r%90 == 0 {
-		po.Rotate = r
-	} else {
-		return newOptionArgumentError("Invalid rotation angle: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyQualityOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid quality arguments: %v", args)
-	}
-
-	if q, err := strconv.Atoi(args[0]); err == nil && q >= 0 && q <= 100 {
-		po.Quality = q
-	} else {
-		return newOptionArgumentError("Invalid quality: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyFormatQualityOption(po *ProcessingOptions, args []string) error {
-	argsLen := len(args)
-	if len(args)%2 != 0 {
-		return newOptionArgumentError("Missing quality for: %s", args[argsLen-1])
-	}
-
-	for i := 0; i < argsLen; i += 2 {
-		f, ok := imagetype.GetTypeByName(args[i])
-		if !ok {
-			return newOptionArgumentError("Invalid image format: %s", args[i])
-		}
-
-		if q, err := strconv.Atoi(args[i+1]); err == nil && q >= 0 && q <= 100 {
-			po.FormatQuality[f] = q
-		} else {
-			return newOptionArgumentError("Invalid quality for %s: %s", args[i], args[i+1])
-		}
-	}
-
-	return nil
-}
-
-func applyMaxBytesOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid max_bytes arguments: %v", args)
-	}
-
-	if max, err := strconv.Atoi(args[0]); err == nil && max >= 0 {
-		po.MaxBytes = max
-	} else {
-		return newOptionArgumentError("Invalid max_bytes: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyBackgroundOption(po *ProcessingOptions, args []string) error {
-	switch len(args) {
-	case 1:
-		if len(args[0]) == 0 {
-			po.Flatten = false
-		} else if c, err := vips.ColorFromHex(args[0]); err == nil {
-			po.Flatten = true
-			po.Background = c
-		} else {
-			return newOptionArgumentError("Invalid background argument: %s", err)
-		}
-
-	case 3:
-		po.Flatten = true
-
-		if r, err := strconv.ParseUint(args[0], 10, 8); err == nil && r <= 255 {
-			po.Background.R = uint8(r)
-		} else {
-			return newOptionArgumentError("Invalid background red channel: %s", args[0])
-		}
-
-		if g, err := strconv.ParseUint(args[1], 10, 8); err == nil && g <= 255 {
-			po.Background.G = uint8(g)
-		} else {
-			return newOptionArgumentError("Invalid background green channel: %s", args[1])
-		}
-
-		if b, err := strconv.ParseUint(args[2], 10, 8); err == nil && b <= 255 {
-			po.Background.B = uint8(b)
-		} else {
-			return newOptionArgumentError("Invalid background blue channel: %s", args[2])
-		}
-
-	default:
-		return newOptionArgumentError("Invalid background arguments: %v", args)
-	}
-
-	return nil
-}
-
-func applyBlurOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid blur arguments: %v", args)
-	}
-
-	if b, err := strconv.ParseFloat(args[0], 32); err == nil && b >= 0 {
-		po.Blur = float32(b)
-	} else {
-		return newOptionArgumentError("Invalid blur: %s", args[0])
-	}
-
-	return nil
-}
-
-func applySharpenOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid sharpen arguments: %v", args)
-	}
-
-	if s, err := strconv.ParseFloat(args[0], 32); err == nil && s >= 0 {
-		po.Sharpen = float32(s)
-	} else {
-		return newOptionArgumentError("Invalid sharpen: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyPixelateOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid pixelate arguments: %v", args)
-	}
-
-	if p, err := strconv.Atoi(args[0]); err == nil && p >= 0 {
-		po.Pixelate = p
-	} else {
-		return newOptionArgumentError("Invalid pixelate: %s", args[0])
-	}
-
-	return nil
-}
-
-func (f *Factory) applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
-	for _, preset := range args {
-		if p, ok := f.presets[preset]; ok {
-			if slices.Contains(usedPresets, preset) {
-				log.Warningf("Recursive preset usage is detected: %s", preset)
-				continue
-			}
-
-			po.UsedPresets = append(po.UsedPresets, preset)
-
-			if err := f.applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
-				return err
-			}
-		} else {
-			return newOptionArgumentError("Unknown preset: %s", preset)
-		}
-	}
-
-	return nil
-}
-
-func applyWatermarkOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 7 {
-		return newOptionArgumentError("Invalid watermark arguments: %v", args)
-	}
-
-	if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 {
-		po.Watermark.Enabled = o > 0
-		po.Watermark.Opacity = o
-	} else {
-		return newOptionArgumentError("Invalid watermark opacity: %s", args[0])
-	}
-
-	if len(args) > 1 && len(args[1]) > 0 {
-		if g, ok := gravityTypes[args[1]]; ok && slices.Contains(watermarkGravityTypes, g) {
-			po.Watermark.Position.Type = g
-		} else {
-			return newOptionArgumentError("Invalid watermark position: %s", args[1])
-		}
-	}
-
-	if len(args) > 2 && len(args[2]) > 0 {
-		if x, err := strconv.ParseFloat(args[2], 64); err == nil {
-			po.Watermark.Position.X = x
-		} else {
-			return newOptionArgumentError("Invalid watermark X offset: %s", args[2])
-		}
-	}
-
-	if len(args) > 3 && len(args[3]) > 0 {
-		if y, err := strconv.ParseFloat(args[3], 64); err == nil {
-			po.Watermark.Position.Y = y
-		} else {
-			return newOptionArgumentError("Invalid watermark Y offset: %s", args[3])
-		}
-	}
-
-	if len(args) > 4 && len(args[4]) > 0 {
-		if s, err := strconv.ParseFloat(args[4], 64); err == nil && s >= 0 {
-			po.Watermark.Scale = s
-		} else {
-			return newOptionArgumentError("Invalid watermark scale: %s", args[4])
-		}
-	}
-
-	return nil
-}
-
-func applyFormatOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid format arguments: %v", args)
-	}
-
-	if f, ok := imagetype.GetTypeByName(args[0]); ok {
-		po.Format = f
-	} else {
-		return newOptionArgumentError("Invalid image format: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyCacheBusterOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid cache buster arguments: %v", args)
-	}
-
-	po.CacheBuster = args[0]
-
-	return nil
-}
-
-func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) error {
-	for _, format := range args {
-		if f, ok := imagetype.GetTypeByName(format); ok {
-			po.SkipProcessingFormats = append(po.SkipProcessingFormats, f)
-		} else {
-			return newOptionArgumentError("Invalid image format in skip processing: %s", format)
-		}
-	}
-
-	return nil
-}
-
-func applyRawOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid return_attachment arguments: %v", args)
-	}
-
-	po.Raw = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyFilenameOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 2 {
-		return newOptionArgumentError("Invalid filename arguments: %v", args)
-	}
-
-	po.Filename = args[0]
-
-	if len(args) > 1 && parseBoolOption(args[1]) {
-		decoded, err := base64.RawURLEncoding.DecodeString(po.Filename)
-		if err != nil {
-			return newOptionArgumentError("Invalid filename encoding: %s", err)
-		}
-
-		po.Filename = string(decoded)
-	}
-
-	return nil
-}
-
-func applyExpiresOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid expires arguments: %v", args)
-	}
-
-	timestamp, err := strconv.ParseInt(args[0], 10, 64)
-	if err != nil {
-		return newOptionArgumentError("Invalid expires argument: %v", args[0])
-	}
-
-	if timestamp > 0 && timestamp < time.Now().Unix() {
-		return newOptionArgumentError("Expired URL")
-	}
-
-	expires := time.Unix(timestamp, 0)
-	po.Expires = &expires
-
-	return nil
-}
-
-func applyStripMetadataOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid strip metadata arguments: %v", args)
-	}
-
-	po.StripMetadata = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyKeepCopyrightOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid keep copyright arguments: %v", args)
-	}
-
-	po.KeepCopyright = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyStripColorProfileOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid strip color profile arguments: %v", args)
-	}
-
-	po.StripColorProfile = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyAutoRotateOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid auto rotate arguments: %v", args)
-	}
-
-	po.AutoRotate = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyEnforceThumbnailOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid enforce thumbnail arguments: %v", args)
-	}
-
-	po.EnforceThumbnail = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyReturnAttachmentOption(po *ProcessingOptions, args []string) error {
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid return_attachment arguments: %v", args)
-	}
-
-	po.ReturnAttachment = parseBoolOption(args[0])
-
-	return nil
-}
-
-func applyMaxSrcResolutionOption(po *ProcessingOptions, args []string) error {
-	if err := security.IsSecurityOptionsAllowed(); err != nil {
-		return err
-	}
-
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid max_src_resolution arguments: %v", args)
-	}
-
-	if x, err := strconv.ParseFloat(args[0], 64); err == nil && x > 0 {
-		po.SecurityOptions.MaxSrcResolution = int(x * 1000000)
-	} else {
-		return newOptionArgumentError("Invalid max_src_resolution: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyMaxSrcFileSizeOption(po *ProcessingOptions, args []string) error {
-	if err := security.IsSecurityOptionsAllowed(); err != nil {
-		return err
-	}
-
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid max_src_file_size arguments: %v", args)
-	}
-
-	if x, err := strconv.Atoi(args[0]); err == nil {
-		po.SecurityOptions.MaxSrcFileSize = x
-	} else {
-		return newOptionArgumentError("Invalid max_src_file_size: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyMaxAnimationFramesOption(po *ProcessingOptions, args []string) error {
-	if err := security.IsSecurityOptionsAllowed(); err != nil {
-		return err
-	}
-
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid max_animation_frames arguments: %v", args)
-	}
-
-	if x, err := strconv.Atoi(args[0]); err == nil && x > 0 {
-		po.SecurityOptions.MaxAnimationFrames = x
-	} else {
-		return newOptionArgumentError("Invalid max_animation_frames: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyMaxAnimationFrameResolutionOption(po *ProcessingOptions, args []string) error {
-	if err := security.IsSecurityOptionsAllowed(); err != nil {
-		return err
-	}
-
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid max_animation_frame_resolution arguments: %v", args)
-	}
-
-	if x, err := strconv.ParseFloat(args[0], 64); err == nil {
-		po.SecurityOptions.MaxAnimationFrameResolution = int(x * 1000000)
-	} else {
-		return newOptionArgumentError("Invalid max_animation_frame_resolution: %s", args[0])
-	}
-
-	return nil
-}
-
-func applyMaxResultDimensionOption(po *ProcessingOptions, args []string) error {
-	if err := security.IsSecurityOptionsAllowed(); err != nil {
-		return err
-	}
-
-	if len(args) > 1 {
-		return newOptionArgumentError("Invalid max_result_dimension arguments: %v", args)
-	}
-
-	if x, err := strconv.Atoi(args[0]); err == nil {
-		po.SecurityOptions.MaxResultDimension = x
-	} else {
-		return newOptionArgumentError("Invalid max_result_dimension: %s", args[0])
-	}
-
-	return nil
-}
-
 func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
 	switch name {
 	case "resize", "rs":
@@ -1087,7 +291,7 @@ func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []stri
 		return applyReturnAttachmentOption(po, args)
 	// Presets
 	case "preset", "pr":
-		return f.applyPresetOption(po, args, usedPresets...)
+		return applyPresetOption(f, po, args, usedPresets...)
 	// Security
 	case "max_src_resolution", "msr":
 		return applyMaxSrcResolutionOption(po, args)
@@ -1163,7 +367,7 @@ func (f *Factory) defaultProcessingOptions(headers http.Header) (*ProcessingOpti
 	}
 
 	if _, ok := f.presets["default"]; ok {
-		if err := f.applyPresetOption(po, []string{"default"}); err != nil {
+		if err := applyPresetOption(f, po, []string{"default"}); err != nil {
 			return po, err
 		}
 	}
@@ -1236,7 +440,7 @@ func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*Proces
 	presets := strings.Split(parts[0], f.config.ArgumentsSeparator)
 	urlParts := parts[1:]
 
-	if err = f.applyPresetOption(po, presets); err != nil {
+	if err = applyPresetOption(f, po, presets); err != nil {
 		return nil, "", err
 	}
 
@@ -1253,3 +457,11 @@ func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*Proces
 
 	return po, url, nil
 }
+
+func (po *ProcessingOptions) isSecurityOptionsAllowed() error {
+	if po.config.AllowSecurityOptions {
+		return nil
+	}
+
+	return newSecurityOptionsError()
+}

+ 0 - 13
security/errors.go

@@ -10,7 +10,6 @@ import (
 type (
 	SignatureError       string
 	ImageResolutionError string
-	SecurityOptionsError struct{}
 	SourceURLError       string
 )
 
@@ -38,18 +37,6 @@ func newImageResolutionError(msg string) error {
 
 func (e ImageResolutionError) Error() string { return string(e) }
 
-func newSecurityOptionsError() error {
-	return ierrors.Wrap(
-		SecurityOptionsError{},
-		1,
-		ierrors.WithStatusCode(http.StatusForbidden),
-		ierrors.WithPublicMessage("Invalid URL"),
-		ierrors.WithShouldReport(false),
-	)
-}
-
-func (e SecurityOptionsError) Error() string { return "Security processing options are not allowed" }
-
 func newSourceURLError(imageURL string) error {
 	return ierrors.Wrap(
 		SourceURLError(fmt.Sprintf("Source URL is not allowed: %s", imageURL)),

+ 0 - 13
security/options.go

@@ -1,9 +1,5 @@
 package security
 
-import (
-	"github.com/imgproxy/imgproxy/v3/config"
-)
-
 // Security options (part of processing options)
 type Options struct {
 	MaxSrcResolution            int
@@ -13,15 +9,6 @@ type Options struct {
 	MaxResultDimension          int
 }
 
-// NOTE: This function is a part of processing option, we'll move it in the next PR
-func IsSecurityOptionsAllowed() error {
-	if config.AllowSecurityOptions {
-		return nil
-	}
-
-	return newSecurityOptionsError()
-}
-
 // CheckDimensions checks if the given dimensions are within the allowed limits
 func (o *Options) CheckDimensions(width, height, frames int) error {
 	frames = max(frames, 1)

+ 63 - 0
testutil/equal_but_not_same.go

@@ -102,3 +102,66 @@ func anyToString(v any) string {
 		return reflect.ValueOf(val).String()
 	}
 }
+
+// ........-------------------============++===============+=+==+++++++++++++++++++
+// .........------------------==-=====#%%%%%%%%%#+============++==+==++++++++++++++
+// .........--------------------===++#%##%%%##%#####===========+==+=++=++=+++++=+++
+//  ..........-----------------=-=+#%#####+++=-=-++#%#===========+==+==++=+==++++=+
+//  ..........-----------------==###+#####++=---.. .=++==============+===+=+=+=++=+
+//  .........-.---------------=--+=-=####++==---..    -=================++=+==++=++
+// .. ........-.--------------==+.  -#####+==---.      ====================++==+++=
+// .. ......---.-----------==-==-   .+#++++=----.      =====================++==++=
+//  ........--.-------------===-.   -#%%%#+-=-=---.    -=====================++==++
+//  ...........------------==++%=  .+#+=+%#+#.=-.--    =====================+==++=+
+// .........--------------===###   ++=#%=%+#+ -+#+=.  .--====================+==+==
+// ........----------------===%#   +.+###%+#- =+##.=.   -===================++=++++
+// ........---------------====##   ++#%%##+#=  -=-     .==================++++++=++
+// .......----------------===--+  .+#%#++++#=   ..     =========+=====+++=++=++++++
+// .....----------------=-====--  -+==+%%#%#+- =       ============+==++++=++==++=+
+// ..----------------==========-  -- #%%#%#+=. =-     ========+===+=++=++++++++++++
+// .----------------==-=========. ==-%#++#=-.   =.   ====+=====+==+==+++==++=++++++
+// .--------------=--============.=-#%@%%%#+++--=   ===+==++==++=+=+==+++++++++++++
+// -------------=-================-=#####%#+-  .-   ==++++++=++=+++++++++++++++++++
+// -------------=================- -.+%##+-.+-. .    .+=++=++++++=+++++++++++++++++
+// ---------==-================= = --.+%%#=.           =+++++++++++++++++++++++++++
+// -------=---===============.  #= =+-==#++++.     .      -++++++++++++++++++++++++
+// -----=-==============-  ..  +#=  =%###++       .-          .=+++++++++++++++++++
+// --==-===========-  .-==..  -###   --++.        +.       .      .=+++++++++++++++
+// ============-  .-=+##=++-. =#%#  =+%##+       #=.                    =++++++++++
+// ========== = =##+###=+===- +%%#..%+%+-+#     #+-                       -++++++++
+// ========= +==##%%##++-#++- ##=++=.+###%%%   +++.   .. .                  +++++++
+// ========-.%++%%%%#+=+-## ++==++++=-..# -=  =+#-   -...        .          ++##+##
+// ======++ -%+#%%%%#+=#=#  =+++===+++. . +# -+%#-  .---..........          ++##+#+
+// ======+. -%=#%%%%#=+#+. =%%#+++##++=. .=#+=#%+. ..--..-........     -   -++++##+
+// +++++++-..%=#%%%%#=+#..###%%#+=+++=---+.=###%=. ..--.--...--...    -.    ++++#++
+// ++=++++.%#+=#%%%#+++ =++%%@%%###%##++=.-###%+-..-------.------... .=.     ++##+#
+// +++++++.#%%=#%%%#+ .##%%%%%%%###%@@%##%=#%%#=-.--=-=---------...  +=.     =#####
+// ++++++ .%%%#+#### ==##%%%@%%#=%@@@%%###+#%%+=--==-===--------....-#=.      +####
+// +++++= +%%%%-#- +#+#%%%@%%%#+%@@@@%%%%%%%%+==========--------..--#+--..... .####
+// ++++= -.#%%%- #++##%%%%%%%#=%%@@@%#%%%%%%#+==+============------=#=--.. .... ###
+// +++++ .#+%% .=%### +%%#%%@%=#%@@@%%%@@%%%+++++++================%+==--..  . . ##
+// ++++  .==. =+##%%#%%%%%%%@%=#%@@@@%%%%@@%++++++++=++===========#%++=----.     +#
+// +++ .+#..=###%##%#@%#%#%%%%=##@@@%#=%%@%#######++++++++++=+++++%%#++=---..-   =#
+// +==--=.+##%%%%+=++++++#@%%%+#%@@@@%+=+#%###%#####++++++++=++++#%%%#++=---..--.-#
+// +%%%-+=%#@@%%%+=#%+++#%@%%%##%%@@@@%%#=++#%%%%######+++++=++++%%%%##++===-.  . +
+// .- --%%@#@@@%%+++#%##@@@@%%%%%%%######%%##+#%######++#+++++++#%%%%##++++###+=  -
+// ..=++%@@%@@@%%%##%#%@@@@@%%%@@@@@%%%#++++++++%###+++###++++++@@%%%%##+==--==++#=
+// -+#%%%@@%@@@@%%%%%@@@@@@@@%@@%%%%%%%###++++++++#####+++++++++%@%%%%######+=--..-
+// +##%%%%@@@@@@@%%@@@@@@@@@@@@@@@@%%%##++#%#+++++++++++#####+++%@@@%%%#++=====++=-
+// #%#@%@%@@@@@%%@@@@@@@@@@@@@%%%%%%%@%%#%%#####++++#+====++++++%@@@@@@@@@%#+==----
+// ##%%@%%@@@@%@@@@@@@@@@@@@@@@@%%%%%%%%%%%#%%%%%###%+++======+++##+=+#+++##%%#%%%+
+// +#%%@@@%@@@%@@%+#@@@@@@@@@@@@%%%%@@%%%%%@%%%%%%%%@#+++++===-===#++==+++=+++#%#%+
+// #%%@@@%%@%%%%@#+#%@@@@%%%@@@@@@@%%%%@@%%%%@%%%%%%@#######+#+==+++=++=======+=++%
+// ##@@@@@%%%%@#++++#%%%%%%%%@@@@@@@@@@@@@@@@@@@%%@@@##########+=++++++#++++++###+#
+// ++#%@%%%%%#++++#+-###%%%%%%%@@%@@@@@@@@@@@@@@@@@@@##+##+####%#########+#########
+// +++++++#++++##+#+ ++####%%%%%%@%@@@@@@@@@@@@@@@%%%####%%####%%#####%#%%#####%%##
+// +#++#++++++++#+++ -+#+##%%%%@@%%@@@@@@@@@@@@@@@@@%####%%%%%%%%%%%%##%@@%%%%#%%%%
+// +#+++#+#####++++= .=++##%#%%%%%%%@@@@@@@@@@@@@@@%####%%%%@%%@%%%%%%%%%@@%@%%%%%#
+// ####+#+++++###+#-..=++##%%%%%%%#%@@@@@@@@@@@@@@@%####%%%%%%%%@%#################
+// #++#++##+#++#++#..-=+###%%%%%%#+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#################
+// ++#+#++#+##++##=---=++####%%%###@@@%@@@@@@%%@@@@@@@@@@@@@@@@@%%+################
+// +##+##++#++#++#--=+=++###%#%####@@@@@@@@@@@@@@@%@@@%%@%%%%%%###=+###############
+// +++++##+#++#+++.-===+#####%%####@@@@@@@@%%@@%%@@%%%%%%########+==###############
+// +++#++++++#+++..--==+##########%%%@@@@@%%%%%@@%%%%########%##+#=-###############
+// +#++#++#+#++++..--===%#########%%%%%%%@%%%%%###%##############+==+##############
+// ++++##+++++#++.==--=+@%##+###+#%%%%%%%%%%%####%##%%#%%%#######++==##############