package jdenticon import ( "fmt" "regexp" "gitea.dockr.co/kev/go-jdenticon/internal/constants" "gitea.dockr.co/kev/go-jdenticon/internal/engine" ) // hexColorRegex validates hex color strings in #RGB or #RRGGBB format. // It's compiled once at package initialization for efficiency. var hexColorRegex = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`) // isValidHexColor checks if the given string is a valid hex color code. // It accepts both #RGB and #RRGGBB formats, case-insensitive. // Returns true for empty strings, which are treated as "transparent background". func isValidHexColor(color string) bool { if color == "" { return true // Empty string means transparent background } return hexColorRegex.MatchString(color) } // Config holds the configuration options for identicon generation. // This is the public API configuration that wraps the internal engine config. type Config struct { // Color configuration ColorSaturation float64 // Saturation for colored shapes (0.0-1.0) GrayscaleSaturation float64 // Saturation for grayscale shapes (0.0-1.0) ColorLightnessRange [2]float64 // Lightness range for colored shapes [min, max] (0.0-1.0) GrayscaleLightnessRange [2]float64 // Lightness range for grayscale shapes [min, max] (0.0-1.0) // Hue restrictions - if specified, only these hues will be used HueRestrictions []float64 // Specific hue values in degrees (0-360) // Layout configuration Padding float64 // Padding as percentage of icon size (0.0-0.5) BackgroundColor string // Background color in hex format (e.g., "#ffffff") or empty for transparent // PNG rendering configuration PNGSupersampling int // Supersampling factor for PNG rendering (1-16) // Security limits for DoS protection. // // MaxIconSize sets the maximum icon dimension in pixels to prevent memory exhaustion. // - 0 (default): Use library default of 4096 pixels (64MB max memory) // - Positive value: Use custom pixel limit // - -1: Disable size limits entirely (use with caution in server environments) MaxIconSize int // MaxInputLength sets the maximum input string length in bytes to prevent hash DoS. // - 0 (default): Use library default of 1MB // - Positive value: Use custom byte limit // - -1: Disable input length limits entirely MaxInputLength int // MaxComplexity sets the maximum geometric complexity score to prevent resource exhaustion. // This is calculated as the sum of complexity scores for all shapes in an identicon. // - 0 (default): Use library default complexity limit // - Positive value: Use custom complexity limit // - -1: Disable complexity limits entirely (use with caution) MaxComplexity int } // DefaultConfig returns a Config with sensible default values. func DefaultConfig() Config { return Config{ ColorSaturation: 0.5, GrayscaleSaturation: 0.0, ColorLightnessRange: [2]float64{0.4, 0.8}, GrayscaleLightnessRange: [2]float64{0.3, 0.9}, HueRestrictions: nil, // No restrictions by default Padding: 0.08, BackgroundColor: "", // Transparent background PNGSupersampling: 8, } } // Validate checks if the configuration values are valid. func (c *Config) Validate() error { // Validate saturation ranges if c.ColorSaturation < 0.0 || c.ColorSaturation > 1.0 { return fmt.Errorf("color saturation must be between 0.0 and 1.0, got %f", c.ColorSaturation) } if c.GrayscaleSaturation < 0.0 || c.GrayscaleSaturation > 1.0 { return fmt.Errorf("grayscale saturation must be between 0.0 and 1.0, got %f", c.GrayscaleSaturation) } // Validate lightness ranges if c.ColorLightnessRange[0] < 0.0 || c.ColorLightnessRange[0] > 1.0 || c.ColorLightnessRange[1] < 0.0 || c.ColorLightnessRange[1] > 1.0 { return fmt.Errorf("color lightness range values must be between 0.0 and 1.0, got [%f, %f]", c.ColorLightnessRange[0], c.ColorLightnessRange[1]) } if c.ColorLightnessRange[0] >= c.ColorLightnessRange[1] { return fmt.Errorf("color lightness range min must be less than max, got [%f, %f]", c.ColorLightnessRange[0], c.ColorLightnessRange[1]) } if c.GrayscaleLightnessRange[0] < 0.0 || c.GrayscaleLightnessRange[0] > 1.0 || c.GrayscaleLightnessRange[1] < 0.0 || c.GrayscaleLightnessRange[1] > 1.0 { return fmt.Errorf("grayscale lightness range values must be between 0.0 and 1.0, got [%f, %f]", c.GrayscaleLightnessRange[0], c.GrayscaleLightnessRange[1]) } if c.GrayscaleLightnessRange[0] >= c.GrayscaleLightnessRange[1] { return fmt.Errorf("grayscale lightness range min must be less than max, got [%f, %f]", c.GrayscaleLightnessRange[0], c.GrayscaleLightnessRange[1]) } // Validate padding if c.Padding < 0.0 || c.Padding > 0.5 { return fmt.Errorf("padding must be between 0.0 and 0.5, got %f", c.Padding) } // Validate hue restrictions for i, hue := range c.HueRestrictions { if hue < 0.0 || hue > 360.0 { return fmt.Errorf("hue restriction at index %d must be between 0.0 and 360.0, got %f", i, hue) } } // Validate PNG supersampling if c.PNGSupersampling < 1 || c.PNGSupersampling > 16 { return fmt.Errorf("PNG supersampling must be between 1 and 16, got %d", c.PNGSupersampling) } // Validate background color format if !isValidHexColor(c.BackgroundColor) { return NewErrInvalidInput("background_color", c.BackgroundColor, "must be a valid hex color in #RGB or #RRGGBB format") } return nil } // effectiveMaxIconSize resolves the configured icon size limit, applying the default if necessary. // Returns -1 if the limit is disabled. func (c *Config) effectiveMaxIconSize() int { if c.MaxIconSize < 0 { return -1 // Disabled } if c.MaxIconSize == 0 { return constants.DefaultMaxIconSize } return c.MaxIconSize } // effectiveMaxInputLength resolves the configured input length limit, applying the default if necessary. // Returns -1 if the limit is disabled. func (c *Config) effectiveMaxInputLength() int { if c.MaxInputLength < 0 { return -1 // Disabled } if c.MaxInputLength == 0 { return constants.DefaultMaxInputLength } return c.MaxInputLength } // effectiveMaxComplexity resolves the configured complexity limit, applying the default if necessary. // Returns -1 if the limit is disabled. func (c *Config) effectiveMaxComplexity() int { if c.MaxComplexity < 0 { return -1 // Disabled } if c.MaxComplexity == 0 { return constants.DefaultMaxComplexity } return c.MaxComplexity } // toEngineColorConfig converts the public Config to the internal engine.ColorConfig. func (c *Config) toEngineColorConfig() (engine.ColorConfig, error) { if err := c.Validate(); err != nil { return engine.ColorConfig{}, fmt.Errorf("invalid configuration: %w", err) } colorConfig := engine.ColorConfig{ ColorSaturation: c.ColorSaturation, GrayscaleSaturation: c.GrayscaleSaturation, ColorLightness: engine.LightnessRange{ Min: c.ColorLightnessRange[0], Max: c.ColorLightnessRange[1], }, GrayscaleLightness: engine.LightnessRange{ Min: c.GrayscaleLightnessRange[0], Max: c.GrayscaleLightnessRange[1], }, Hues: c.HueRestrictions, BackColor: nil, // Will be set below if background color is specified IconPadding: c.Padding, } // Handle background color conversion from hex string to engine.Color if c.BackgroundColor != "" { // Validation is already handled by c.Validate() above. bgColor, err := engine.ParseHexColorToEngine(c.BackgroundColor) if err != nil { return colorConfig, fmt.Errorf("failed to parse background color: %w", err) } colorConfig.BackColor = &bgColor } return colorConfig, nil } // ConfigOption represents a functional option for configuring identicon generation. type ConfigOption func(*Config) error // Configure applies configuration options to create a new Config. func Configure(options ...ConfigOption) (Config, error) { config := DefaultConfig() for _, option := range options { if err := option(&config); err != nil { return config, fmt.Errorf("configuration option failed: %w", err) } } if err := config.Validate(); err != nil { return config, fmt.Errorf("invalid configuration after applying options: %w", err) } return config, nil } // WithColorSaturation sets the color saturation for colored shapes. func WithColorSaturation(saturation float64) ConfigOption { return func(c *Config) error { if saturation < 0.0 || saturation > 1.0 { return fmt.Errorf("color saturation must be between 0.0 and 1.0, got %f", saturation) } c.ColorSaturation = saturation return nil } } // WithGrayscaleSaturation sets the saturation for grayscale shapes. func WithGrayscaleSaturation(saturation float64) ConfigOption { return func(c *Config) error { if saturation < 0.0 || saturation > 1.0 { return fmt.Errorf("grayscale saturation must be between 0.0 and 1.0, got %f", saturation) } c.GrayscaleSaturation = saturation return nil } } // WithPadding sets the padding as a percentage of the icon size. func WithPadding(padding float64) ConfigOption { return func(c *Config) error { if padding < 0.0 || padding > 0.5 { return fmt.Errorf("padding must be between 0.0 and 0.5, got %f", padding) } c.Padding = padding return nil } } // WithBackgroundColor sets the background color (hex format like "#ffffff"). // The color must be in #RGB or #RRGGBB format. An empty string means transparent background. func WithBackgroundColor(color string) ConfigOption { return func(c *Config) error { if !isValidHexColor(color) { return NewErrInvalidInput("background_color", color, "must be a valid hex color in #RGB or #RRGGBB format") } c.BackgroundColor = color return nil } } // WithColorLightnessRange sets the lightness range for colored shapes. func WithColorLightnessRange(min, max float64) ConfigOption { return func(c *Config) error { if min < 0.0 || min > 1.0 || max < 0.0 || max > 1.0 { return fmt.Errorf("lightness values must be between 0.0 and 1.0, got min=%f, max=%f", min, max) } if min >= max { return fmt.Errorf("lightness min must be less than max, got min=%f, max=%f", min, max) } c.ColorLightnessRange = [2]float64{min, max} return nil } } // WithGrayscaleLightnessRange sets the lightness range for grayscale shapes. func WithGrayscaleLightnessRange(min, max float64) ConfigOption { return func(c *Config) error { if min < 0.0 || min > 1.0 || max < 0.0 || max > 1.0 { return fmt.Errorf("lightness values must be between 0.0 and 1.0, got min=%f, max=%f", min, max) } if min >= max { return fmt.Errorf("lightness min must be less than max, got min=%f, max=%f", min, max) } c.GrayscaleLightnessRange = [2]float64{min, max} return nil } } // WithHueRestrictions restricts hues to specific values in degrees (0-360). func WithHueRestrictions(hues []float64) ConfigOption { return func(c *Config) error { for i, hue := range hues { if hue < 0.0 || hue > 360.0 { return fmt.Errorf("hue at index %d must be between 0.0 and 360.0, got %f", i, hue) } } c.HueRestrictions = make([]float64, len(hues)) copy(c.HueRestrictions, hues) return nil } } // WithPNGSupersampling sets the PNG supersampling factor (1-16). func WithPNGSupersampling(factor int) ConfigOption { return func(c *Config) error { if factor < 1 || factor > 16 { return fmt.Errorf("PNG supersampling must be between 1 and 16, got %d", factor) } c.PNGSupersampling = factor return nil } } // WithMaxComplexity sets the maximum geometric complexity limit for resource protection. // Use -1 to disable complexity limits entirely (use with caution). func WithMaxComplexity(maxComplexity int) ConfigOption { return func(c *Config) error { if maxComplexity < -1 { return fmt.Errorf("max complexity must be >= -1, got %d", maxComplexity) } c.MaxComplexity = maxComplexity return nil } } // WithMaxIconSize sets the maximum icon dimension in pixels for DoS protection. // Use 0 for the library default (4096 pixels), or -1 to disable limits entirely. func WithMaxIconSize(maxSize int) ConfigOption { return func(c *Config) error { if maxSize < -1 { return fmt.Errorf("max icon size must be >= -1, got %d", maxSize) } c.MaxIconSize = maxSize return nil } } // WithMaxInputLength sets the maximum input string length in bytes for DoS protection. // Use 0 for the library default (1MB), or -1 to disable limits entirely. func WithMaxInputLength(maxLength int) ConfigOption { return func(c *Config) error { if maxLength < -1 { return fmt.Errorf("max input length must be >= -1, got %d", maxLength) } c.MaxInputLength = maxLength return nil } }