package web import ( "fmt" "regexp" "strings" "gitgud.io/mike/mpv-manager/pkg/constants" ) // Valid HWA methods var validHWAMethods = map[string]bool{ // Automatic selection methods "auto": true, "auto-safe": true, // NVIDIA CUDA decoding "nvdec": true, "nvdec-copy": true, // Video Acceleration API (Linux AMD/Intel) "vaapi": true, "vaapi-copy": true, // Vulkan video decoding (cross-platform) "vulkan": true, "vulkan-copy": true, // Direct3D 11 Video Acceleration (Windows) "d3d11va": true, "d3d11va-copy": true, // Apple VideoToolbox (macOS) "videotoolbox": true, "videotoolbox-copy": true, // Direct Rendering Manager (Linux AMD/Intel) "drm": true, "drm-copy": true, // Disable hardware decoding "no": true, } // Valid MPV config keys and their value patterns var validConfigKeys = map[string]*regexp.Regexp{ // Hardware acceleration - supports values with ,auto fallback suffix (e.g., "nvdec-copy,auto") constants.ConfigKeyHardwareDecoder: regexp.MustCompile(`^(auto|auto-safe|nvdec|nvdec-copy|vaapi|vaapi-copy|vulkan|vulkan-copy|d3d11va|d3d11va-copy|videotoolbox|videotoolbox-copy|drm|drm-copy|no)(,auto)?$`), // Video output driver constants.ConfigKeyVideoOutput: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), // Video scaling filter constants.ConfigKeyScaleFilter: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), // Dither algorithm constants.ConfigKeyDitherAlgorithm: regexp.MustCompile(`^(no|ordered|error-diffusion|floyd-steinberg)$`), // Save position on quit constants.ConfigKeySavePositionOnQuit: regexp.MustCompile(`^(yes|no)$`), // Window border/decoration constants.ConfigKeyBorder: regexp.MustCompile(`^(yes|no)$`), // Profile/quality preset constants.ConfigKeyProfile: regexp.MustCompile(`^(high-quality|fast|low-latency|sw-fast)$`), // Screenshot settings constants.ConfigKeyScreenshotFormat: regexp.MustCompile(`^(png|jpg|jpeg|webp|jxl|avif)$`), constants.ConfigKeyScreenshotTagColorspace: regexp.MustCompile(`^(yes|no)$`), constants.ConfigKeyScreenshotHighBitDepth: regexp.MustCompile(`^(yes|no)$`), constants.ConfigKeyScreenshotTemplate: regexp.MustCompile(`^[a-zA-Z0-9_\-\.%:\s\(\)]+$`), constants.ConfigKeyScreenshotDirectory: regexp.MustCompile(`^[\w\/\\\-\.~: ]+$`), constants.ConfigKeyScreenshotJpegQuality: regexp.MustCompile(`^(100|[1-9]?\d)$`), constants.ConfigKeyScreenshotPngCompression: regexp.MustCompile(`^[0-9]$`), constants.ConfigKeyScreenshotWebpLossless: regexp.MustCompile(`^(yes|no)$`), constants.ConfigKeyScreenshotWebpQuality: regexp.MustCompile(`^(100|[1-9]?\d)$`), constants.ConfigKeyScreenshotJxlDistance: regexp.MustCompile(`^(15\.0|([0-9]|1[0-4])(\.\d)?|[0-9]|1[0-5])$`), constants.ConfigKeyScreenshotAvifEncoder: regexp.MustCompile(`^[a-zA-Z0-9\-]+$`), constants.ConfigKeyScreenshotAvifPixfmt: regexp.MustCompile(`^[a-zA-Z0-9_\-]*$`), constants.ConfigKeyScreenshotAvifOpts: regexp.MustCompile(`^[a-zA-Z0-9_=,\-\.]+$`), } // ValidateConfigValue validates MPV config key-value pairs func ValidateConfigValue(key, value string) error { // Check if key is valid regex, ok := validConfigKeys[key] if !ok { return fmt.Errorf("invalid config key: %s", key) } // Check if value matches the pattern for this key if !regex.MatchString(value) { return fmt.Errorf("invalid value '%s' for key '%s'", value, key) } return nil } // ValidateMethodID validates that a method ID is valid func ValidateMethodID(methodID string) error { // Check against all known method IDs _, ok := constants.MethodIDToName[methodID] if !ok { return fmt.Errorf("invalid method ID: %s", methodID) } return nil } // ValidateAppName validates app names (alphanumeric with spaces and hyphens) func ValidateAppName(appName string) error { if appName == "" { return fmt.Errorf("app name cannot be empty") } // Allow alphanumeric, spaces, hyphens, and periods matched, err := regexp.MatchString(`^[a-zA-Z0-9\s\-\.]+$`, appName) if err != nil { return fmt.Errorf("failed to validate app name: %v", err) } if !matched { return fmt.Errorf("invalid app name: %s (must contain only letters, numbers, spaces, hyphens, or periods)", appName) } // Prevent excessively long names if len(appName) > 100 { return fmt.Errorf("app name too long: %s (maximum 100 characters)", appName) } return nil } // NormalizeAppType normalizes legacy app types to standard values func NormalizeAppType(appType string) string { // Already standard types if appType == "app" || appType == "frontend" { return appType } // Legacy types - normalize to "app" legacyTypes := map[string]string{ "flatpak": "app", "package-manager": "app", "executable": "app", "portable": "app", // Old app names (pre-Feb 2026) - normalize appropriately "MPV": "app", "IINA": "app", "MPC-QT": "app", "Celluloid": "frontend", } if normalized, ok := legacyTypes[appType]; ok { return normalized } // Unknown type - return as-is for validation to catch it return appType } // ValidateAppType validates app type (app, frontend, or legacy types) func ValidateAppType(appType string) error { // Check for empty type if appType == "" { return fmt.Errorf("invalid app type") } // Normalized type will always be valid normalized := NormalizeAppType(appType) if normalized != "app" && normalized != "frontend" { return fmt.Errorf("must be 'app' or 'frontend'") } return nil } // ValidateLanguageCode validates language code format (e.g., "en", "en-US", "zh-CN") func ValidateLanguageCode(code string) error { if code == "" { // Empty language code is valid (means no preference) return nil } // Accept basic language code (e.g., "en", "zh") if matched, _ := regexp.MatchString(`^[a-z]{2}$`, code); matched { return nil } // Accept locale with country (e.g., "en-US", "zh-CN") if matched, _ := regexp.MatchString(`^[a-z]{2}-[A-Z]{2}$`, code); matched { return nil } return fmt.Errorf("invalid language code: %s (format must be 'en' or 'en-US')", code) } // ValidateHWAValue validates hardware acceleration method // Accepts both simple values (e.g., "nvdec") and values with fallback (e.g., "nvdec,auto") func ValidateHWAValue(method string) error { if method == "" { // Empty value is valid (no change) return nil } // Check for ,auto suffix and strip it for validation baseMethod := method if strings.HasSuffix(method, ",auto") { baseMethod = strings.TrimSuffix(method, ",auto") } if !validHWAMethods[baseMethod] { return fmt.Errorf("invalid hardware acceleration method: %s", method) } return nil } // ValidateInstallMethod validates install method ID and ensures it's valid for current platform func ValidateInstallMethod(methodID string, isWindows, isDarwin, isLinux bool) error { // Check for component method IDs (UOSC, ModernZ, FFmpeg) - valid on all platforms switch methodID { case constants.MethodUOSC, constants.MethodModernZ: // UI components are valid on all platforms return nil case constants.MethodFFmpeg: // FFmpeg is only valid on Windows (bundled with MPV) if !isWindows { return fmt.Errorf("method %s is only available on Windows", methodID) } return nil } // First check if method ID is valid if err := ValidateMethodID(methodID); err != nil { return err } // Check platform compatibility switch methodID { case constants.MethodMPVApp, constants.MethodIINA: if !isDarwin { return fmt.Errorf("method %s is only available on macOS", methodID) } case constants.MethodMPCQT: if !isWindows { return fmt.Errorf("method %s is only available on Windows", methodID) } case constants.MethodMPVBinary, constants.MethodMPVBinaryV3: if isDarwin { return fmt.Errorf("method %s is not available on macOS (use %s instead)", methodID, constants.MethodMPVApp) } } // Check package manager methods pmMethods := []string{ constants.MethodMPVBrew, constants.MethodMPVFlatpak, constants.MethodCelluloidFlatpak, constants.MethodMPVPackage, constants.MethodCelluloidPackage, } for _, pmMethod := range pmMethods { if methodID == pmMethod && isWindows { return fmt.Errorf("method %s is not available on Windows", methodID) } } return nil } // ValidateScreenshotQuality validates quality values (0-100) func ValidateScreenshotQuality(quality string) error { if quality == "" { return nil // Empty is valid (will use default) } matched, _ := regexp.MatchString(`^(100|[1-9]?\d)$`, quality) if !matched { return fmt.Errorf("invalid quality value: %s (must be 0-100)", quality) } return nil } // ValidateScreenshotCompression validates PNG compression (0-9) func ValidateScreenshotCompression(compression string) error { if compression == "" { return nil // Empty is valid (will use default) } matched, _ := regexp.MatchString(`^[0-9]$`, compression) if !matched { return fmt.Errorf("invalid compression value: %s (must be 0-9)", compression) } return nil } // validHotkeyPresets is the set of accepted preset names. var validHotkeyPresets = map[string]bool{ "default": true, "vlc": true, "mpc-hc": true, } // ValidateHotkeyPreset validates that a hotkey preset name is recognized. func ValidateHotkeyPreset(preset string) error { if preset == "" { return fmt.Errorf("preset name cannot be empty") } if !validHotkeyPresets[preset] { return fmt.Errorf("invalid hotkey preset: %s (must be one of: default, vlc, mpc-hc)", preset) } return nil } // ValidateScreenshotJxlDistance validates JXL distance (0.0-15.0) func ValidateScreenshotJxlDistance(distance string) error { if distance == "" { return nil // Empty is valid (will use default) } matched, _ := regexp.MatchString(`^(15\.0|([0-9]|1[0-4])(\.\d)?|[0-9]|1[0-5])$`, distance) if !matched { return fmt.Errorf("invalid JXL distance: %s (must be 0.0-15.0, note: 0.0 is lossless)", distance) } return nil }