package ux import ( "fmt" "os" "path/filepath" "regexp" "strings" ) // Validator provides input validation functionality type Validator struct { verbose bool } // NewValidator creates a new validator func NewValidator(verbose bool) *Validator { return &Validator{verbose: verbose} } // ValidationResult represents the result of a validation type ValidationResult struct { Valid bool Message string Suggestions []string } // ValidateImageReference validates a container image reference func (v *Validator) ValidateImageReference(imageRef string) *ValidationResult { if imageRef == "" { return &ValidationResult{ Valid: false, Message: "Image reference cannot be empty", Suggestions: []string{ "Provide a valid container image reference", "Example: git.raines.xyz/particle-os/debian-bootc:latest", "Example: docker.io/debian:bookworm-slim", }, } } // Basic validation for image reference format // Should contain at least one colon or slash if !strings.Contains(imageRef, ":") && !strings.Contains(imageRef, "/") { return &ValidationResult{ Valid: false, Message: "Invalid image reference format", Suggestions: []string{ "Image reference should contain registry, namespace, and tag", "Example: registry.example.com/namespace/image:tag", "Example: image:tag", }, } } // Check for valid characters (allow hyphens in addition to other valid chars) validChars := regexp.MustCompile(`^[a-zA-Z0-9._/:+-]+$`) if !validChars.MatchString(imageRef) { return &ValidationResult{ Valid: false, Message: "Image reference contains invalid characters", Suggestions: []string{ "Use only alphanumeric characters, dots, underscores, slashes, hyphens, and colons", "Example: git.raines.xyz/particle-os/debian-bootc:latest", }, } } return &ValidationResult{Valid: true} } // ValidateImageTypes validates image type specifications func (v *Validator) ValidateImageTypes(imageTypes []string) *ValidationResult { if len(imageTypes) == 0 { return &ValidationResult{ Valid: false, Message: "At least one image type must be specified", Suggestions: []string{ "Specify one or more image types: qcow2, ami, vmdk, raw, debian-installer, calamares", "Example: --type qcow2 --type ami", }, } } validTypes := map[string]bool{ "qcow2": true, "ami": true, "vmdk": true, "raw": true, "debian-installer": true, "calamares": true, } var invalidTypes []string for _, imgType := range imageTypes { if !validTypes[imgType] { invalidTypes = append(invalidTypes, imgType) } } if len(invalidTypes) > 0 { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Invalid image types: %s", strings.Join(invalidTypes, ", ")), Suggestions: []string{ "Supported image types: qcow2, ami, vmdk, raw, debian-installer, calamares", "Check spelling and case sensitivity", }, } } return &ValidationResult{Valid: true} } // ValidateArchitecture validates target architecture func (v *Validator) ValidateArchitecture(arch string) *ValidationResult { if arch == "" { return &ValidationResult{ Valid: false, Message: "Target architecture cannot be empty", Suggestions: []string{ "Specify a valid architecture: amd64, arm64, armhf, ppc64el, s390x", "Example: --target-arch amd64", }, } } validArchs := map[string]bool{ "amd64": true, "arm64": true, "armhf": true, "ppc64el": true, "s390x": true, } if !validArchs[arch] { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Unsupported architecture: %s", arch), Suggestions: []string{ "Supported architectures: amd64, arm64, armhf, ppc64el, s390x", "Use --target-arch amd64 for x86_64 systems", }, } } return &ValidationResult{Valid: true} } // ValidateRootfsType validates root filesystem type func (v *Validator) ValidateRootfsType(rootfsType string) *ValidationResult { if rootfsType == "" { // Empty is valid (will use default) return &ValidationResult{Valid: true} } validTypes := map[string]bool{ "ext4": true, "xfs": true, "btrfs": true, } if !validTypes[rootfsType] { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Unsupported root filesystem type: %s", rootfsType), Suggestions: []string{ "Supported filesystem types: ext4, xfs, btrfs", "Leave empty to use default (ext4)", }, } } return &ValidationResult{Valid: true} } // ValidateDirectory validates a directory path func (v *Validator) ValidateDirectory(path, purpose string) *ValidationResult { if path == "" { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("%s path cannot be empty", purpose), Suggestions: []string{ fmt.Sprintf("Specify a valid directory path for %s", purpose), "Use absolute or relative paths", }, } } // Check if path exists and is a directory info, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { // Try to create the directory if err := os.MkdirAll(path, 0755); err != nil { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Cannot create %s directory: %v", purpose, err), Suggestions: []string{ "Check parent directory permissions", "Use a different directory path", "Run with appropriate permissions", }, } } return &ValidationResult{Valid: true} } return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Cannot access %s directory: %v", purpose, err), Suggestions: []string{ "Check directory permissions", "Ensure the path is accessible", }, } } if !info.IsDir() { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("%s path is not a directory: %s", purpose, path), Suggestions: []string{ "Specify a directory path, not a file", "Check the path exists and is a directory", }, } } return &ValidationResult{Valid: true} } // ValidateConfigFile validates a configuration file path func (v *Validator) ValidateConfigFile(configPath string) *ValidationResult { if configPath == "" { // Empty is valid (will use default) return &ValidationResult{Valid: true} } // Check if file exists info, err := os.Stat(configPath) if err != nil { if os.IsNotExist(err) { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Configuration file not found: %s", configPath), Suggestions: []string{ "Check the file path is correct", "Create a configuration file at the specified path", "Leave empty to use default configuration", }, } } return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Cannot access configuration file: %v", err), Suggestions: []string{ "Check file permissions", "Ensure the file is accessible", }, } } if info.IsDir() { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Configuration path is a directory, not a file: %s", configPath), Suggestions: []string{ "Specify a file path, not a directory", "Example: .config/registry.yaml", }, } } // Check file extension ext := strings.ToLower(filepath.Ext(configPath)) if ext != ".yaml" && ext != ".yml" { return &ValidationResult{ Valid: false, Message: fmt.Sprintf("Configuration file should be YAML format: %s", configPath), Suggestions: []string{ "Use .yaml or .yml extension", "Example: .config/registry.yaml", }, } } return &ValidationResult{Valid: true} } // ValidateDistroDefPath validates distribution definition path func (v *Validator) ValidateDistroDefPath(path string) *ValidationResult { if path == "" { return &ValidationResult{ Valid: false, Message: "Distribution definition path cannot be empty", Suggestions: []string{ "Specify a valid directory path containing package definitions", "Example: ./data/defs", }, } } return v.ValidateDirectory(path, "distribution definition") } // PrintValidationResult prints validation results with suggestions func (v *Validator) PrintValidationResult(result *ValidationResult) { if result.Valid { if v.verbose { PrintInfo(os.Stdout, "Validation passed") } return } PrintError(os.Stdout, result.Message) if len(result.Suggestions) > 0 { fmt.Println("💡 Suggestions:") for _, suggestion := range result.Suggestions { fmt.Printf(" • %s\n", suggestion) } } }