package validation import ( "fmt" "reflect" "regexp" "strconv" "strings" "unicode" ) var EmailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) var UsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]{3,50}$`) var URLRegex = regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`) func ValidateEmail(email string) error { email = strings.TrimSpace(email) if email == "" { return &ValidationError{Field: "email", Message: "Email is required"} } if len(email) > 254 { return &ValidationError{Field: "email", Message: "Email is too long"} } if !EmailRegex.MatchString(email) { return &ValidationError{Field: "email", Message: "Invalid email format"} } return nil } func ValidateUsername(username string) error { username = strings.TrimSpace(username) if username == "" { return &ValidationError{Field: "username", Message: "Username is required"} } if len(username) < 3 { return &ValidationError{Field: "username", Message: "Username must be at least 3 characters"} } if len(username) > 50 { return &ValidationError{Field: "username", Message: "Username must be at most 50 characters"} } if !UsernameRegex.MatchString(username) { return &ValidationError{Field: "username", Message: "Username can only contain letters, numbers, underscores, and hyphens"} } return nil } func ValidatePassword(password string) error { if password == "" { return &ValidationError{Field: "password", Message: "Password is required"} } if len(password) < 8 { return &ValidationError{Field: "password", Message: "Password must be at least 8 characters long"} } if len(password) > 128 { return &ValidationError{Field: "password", Message: "Password must be no more than 128 characters long"} } hasLetter := false for _, char := range password { if unicode.IsLetter(char) { hasLetter = true break } } if !hasLetter { return &ValidationError{Field: "password", Message: "Password must contain at least one letter"} } hasNumber := false for _, char := range password { if unicode.IsNumber(char) { hasNumber = true break } } if !hasNumber { return &ValidationError{Field: "password", Message: "Password must contain at least one number"} } hasSpecial := false specialChars := "!@#$%^&*()_+-=[]{}|\\:\";'<>?,./~`" for _, char := range password { if strings.ContainsRune(specialChars, char) { hasSpecial = true break } } if !hasSpecial { return &ValidationError{Field: "password", Message: "Password must contain at least one special character"} } return nil } func ValidateTitle(title string) error { title = strings.TrimSpace(title) if title == "" { return &ValidationError{Field: "title", Message: "Title is required"} } if len(title) < 3 { return &ValidationError{Field: "title", Message: "Title must be at least 3 characters"} } if len(title) > 200 { return &ValidationError{Field: "title", Message: "Title must be at most 200 characters"} } return nil } func ValidateContent(content string) error { content = strings.TrimSpace(content) if len(content) > 10000 { return &ValidationError{Field: "content", Message: "Content must be at most 10000 characters"} } return nil } func ValidateURL(url string) error { url = strings.TrimSpace(url) if url == "" { return &ValidationError{Field: "url", Message: "URL is required"} } if len(url) > 2048 { return &ValidationError{Field: "url", Message: "URL is too long"} } if !URLRegex.MatchString(url) { return &ValidationError{Field: "url", Message: "Invalid URL format. Only HTTP and HTTPS URLs are allowed"} } return nil } func ValidateSearchQuery(query string) error { query = strings.TrimSpace(query) if len(query) > 100 { return &ValidationError{Field: "query", Message: "Search query is too long"} } return nil } func SanitizeString(input string) string { input = strings.ReplaceAll(input, "\x00", "") input = strings.ReplaceAll(input, "\r", "") input = strings.ReplaceAll(input, "\n", "") input = strings.ReplaceAll(input, "\t", "") return strings.TrimSpace(input) } type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return e.Message } type StructValidationError struct { Errors []FieldValidationError } func (e *StructValidationError) Error() string { if len(e.Errors) == 0 { return "validation failed" } messages := make([]string, len(e.Errors)) for i, err := range e.Errors { messages[i] = err.Message } return strings.Join(messages, "; ") } type FieldValidationError struct { Field string Tag string Param string Message string } func ValidateStruct(s interface{}) error { if s == nil { return nil } val := reflect.ValueOf(s) typ := reflect.TypeOf(s) if val.Kind() == reflect.Ptr { if val.IsNil() { return nil } val = val.Elem() typ = typ.Elem() } if val.Kind() != reflect.Struct { return fmt.Errorf("ValidateStruct: expected struct, got %s", val.Kind()) } var errors []FieldValidationError for i := 0; i < val.NumField(); i++ { field := typ.Field(i) fieldVal := val.Field(i) if !fieldVal.CanInterface() { continue } validateTag := field.Tag.Get("validate") if validateTag == "" { continue } tags := strings.Split(validateTag, ",") omitempty := false for _, tag := range tags { tag = strings.TrimSpace(tag) if tag == "" { continue } if tag == "omitempty" { omitempty = true continue } var tagName, param string if idx := strings.Index(tag, "="); idx != -1 { tagName = tag[:idx] param = tag[idx+1:] } else { tagName = tag } if err := validateField(field.Name, fieldVal, tagName, param, omitempty); err != nil { errors = append(errors, FieldValidationError{ Field: field.Name, Tag: tagName, Param: param, Message: err.Message, }) if tagName == "required" { break } } } } if len(errors) > 0 { return &StructValidationError{Errors: errors} } return nil } func validateField(fieldName string, fieldVal reflect.Value, tagName, param string, omitempty bool) *ValidationError { if omitempty && isEmptyValue(fieldVal) { return nil } switch tagName { case "required": if isEmptyValue(fieldVal) { return &ValidationError{Field: fieldName, Message: fieldName + " is required"} } case "min": if err := validateMin(fieldName, fieldVal, param); err != nil { return err } case "max": if err := validateMax(fieldName, fieldVal, param); err != nil { return err } case "email": if err := validateEmailTag(fieldName, fieldVal); err != nil { return err } case "url": if err := validateURLTag(fieldName, fieldVal); err != nil { return err } case "oneof": if err := validateOneOf(fieldName, fieldVal, param); err != nil { return err } default: return &ValidationError{Field: fieldName, Message: fieldName + " is invalid"} } return nil } func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.String: return v.String() == "" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Bool: return !v.Bool() case reflect.Pointer, reflect.Interface, reflect.Slice, reflect.Map: return v.IsNil() default: return false } } func validateMin(fieldName string, v reflect.Value, param string) *ValidationError { min, err := strconv.Atoi(param) if err != nil { return &ValidationError{Field: fieldName, Message: fieldName + " has invalid min parameter"} } switch v.Kind() { case reflect.String: if len(v.String()) < min { return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d characters", fieldName, min)} } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if v.Int() < int64(min) { return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d", fieldName, min)} } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if v.Uint() < uint64(min) { return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d", fieldName, min)} } } return nil } func validateMax(fieldName string, v reflect.Value, param string) *ValidationError { max, err := strconv.Atoi(param) if err != nil { return &ValidationError{Field: fieldName, Message: fieldName + " has invalid max parameter"} } switch v.Kind() { case reflect.String: if len(v.String()) > max { return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d characters", fieldName, max)} } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if v.Int() > int64(max) { return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d", fieldName, max)} } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if v.Uint() > uint64(max) { return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d", fieldName, max)} } } return nil } func validateEmailTag(fieldName string, v reflect.Value) *ValidationError { if v.Kind() != reflect.String { return nil } email := strings.TrimSpace(v.String()) if email == "" { return nil } if !EmailRegex.MatchString(email) { return &ValidationError{Field: fieldName, Message: fieldName + " must be a valid email address"} } return nil } func validateURLTag(fieldName string, v reflect.Value) *ValidationError { if v.Kind() != reflect.String { return nil } url := strings.TrimSpace(v.String()) if url == "" { return nil } if len(url) > 2048 { return &ValidationError{Field: fieldName, Message: fieldName + " must be at most 2048 characters"} } if !URLRegex.MatchString(url) { return &ValidationError{Field: fieldName, Message: fieldName + " must be a valid URL"} } return nil } func validateOneOf(fieldName string, v reflect.Value, param string) *ValidationError { if v.Kind() != reflect.String { return nil } value := v.String() allowedValues := strings.Split(param, " ") for _, allowed := range allowedValues { if value == allowed { return nil } } return &ValidationError{Field: fieldName, Message: fieldName + " must be one of: " + param} }