package utils import ( "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "math/big" "regexp" "strings" "time" "unicode" "golang.org/x/crypto/bcrypt" ) // GenerateID génère un ID unique func GenerateID() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) } // GenerateUUID génère un UUID v4 func GenerateUUID() string { b := make([]byte, 16) rand.Read(b) // Version 4 b[6] = (b[6] & 0x0f) | 0x40 // Variant bits b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } // GenerateRandomString génère une chaîne aléatoire de longueur donnée func GenerateRandomString(length int) string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, length) for i := range b { num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) b[i] = charset[num.Int64()] } return string(b) } // GenerateRandomBytes génère des bytes aléatoires func GenerateRandomBytes(length int) ([]byte, error) { b := make([]byte, length) _, err := rand.Read(b) return b, err } // HashPassword hash un mot de passe avec bcrypt func HashPassword(password string) (string, error) { hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("failed to hash password: %w", err) } return string(hashedBytes), nil } // VerifyPassword vérifie un mot de passe contre son hash func VerifyPassword(hashedPassword, password string) error { return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) } // CheckPasswordHash est un alias pour VerifyPassword (compatibilité) func CheckPasswordHash(password, hashedPassword string) error { return VerifyPassword(hashedPassword, password) } // HashSHA256 hash une chaîne avec SHA256 func HashSHA256(input string) string { hash := sha256.Sum256([]byte(input)) return hex.EncodeToString(hash[:]) } // ValidateEmail valide le format d'un email func ValidateEmail(email string) bool { emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) return emailRegex.MatchString(email) } // ValidatePasswordStrength is now in password_validator.go // T0197: Moved to password_validator.go for better organization // ValidateUsername valide le format d'un nom d'utilisateur func ValidateUsername(username string) (bool, []string) { var errors []string if len(username) < 3 { errors = append(errors, "Username must be at least 3 characters long") } if len(username) > 30 { errors = append(errors, "Username must be less than 30 characters") } usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) if !usernameRegex.MatchString(username) { errors = append(errors, "Username can only contain letters, numbers, underscores, and hyphens") } return len(errors) == 0, errors } // SanitizeString nettoie une chaîne de caractères func SanitizeString(input string) string { // Supprimer les caractères de contrôle cleaned := strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 } return r }, input) // Supprimer les espaces en début et fin cleaned = strings.TrimSpace(cleaned) // Limiter la longueur if len(cleaned) > 1000 { cleaned = cleaned[:1000] } return cleaned } // SanitizeHTML is now in sanitizer.go with enhanced functionality // BE-SEC-009: Moved to sanitizer.go for better organization and security // TruncateString tronque une chaîne à la longueur spécifiée func TruncateString(input string, maxLength int) string { if len(input) <= maxLength { return input } return input[:maxLength] + "..." } // ContainsString vérifie si une chaîne contient une sous-chaîne (insensible à la casse) func ContainsString(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) } // IsEmpty vérifie si une chaîne est vide ou ne contient que des espaces func IsEmpty(s string) bool { return strings.TrimSpace(s) == "" } // IsNotEmpty vérifie si une chaîne n'est pas vide func IsNotEmpty(s string) bool { return !IsEmpty(s) } // FormatDuration formate une durée en chaîne lisible func FormatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%.0fs", d.Seconds()) } if d < time.Hour { return fmt.Sprintf("%.0fm", d.Minutes()) } if d < 24*time.Hour { return fmt.Sprintf("%.1fh", d.Hours()) } return fmt.Sprintf("%.1fd", d.Hours()/24) } // FormatFileSize formate une taille de fichier en chaîne lisible func FormatFileSize(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } // FormatNumber formate un nombre avec des séparateurs de milliers func FormatNumber(n int64) string { str := fmt.Sprintf("%d", n) if len(str) <= 3 { return str } var result strings.Builder for i, char := range str { if i > 0 && (len(str)-i)%3 == 0 { result.WriteString(",") } result.WriteRune(char) } return result.String() } // ParseDuration parse une durée depuis une chaîne func ParseDuration(s string) (time.Duration, error) { // Supprimer les espaces s = strings.TrimSpace(s) // Ajouter 's' si pas d'unité spécifiée if !strings.ContainsAny(s, "smhd") { s += "s" } return time.ParseDuration(s) } // IsValidURL vérifie si une chaîne est une URL valide func IsValidURL(url string) bool { urlRegex := regexp.MustCompile(`^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$`) return urlRegex.MatchString(url) } // ExtractDomain extrait le domaine d'une URL func ExtractDomain(url string) string { // Supprimer le protocole if strings.HasPrefix(url, "http://") { url = url[7:] } else if strings.HasPrefix(url, "https://") { url = url[8:] } // Supprimer le chemin if idx := strings.Index(url, "/"); idx != -1 { url = url[:idx] } return url } // GenerateSlug génère un slug à partir d'une chaîne func GenerateSlug(input string) string { // Convertir en minuscules slug := strings.ToLower(input) // Remplacer les espaces par des tirets slug = strings.ReplaceAll(slug, " ", "-") // Supprimer les caractères non alphanumériques sauf les tirets slug = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(slug, "") // Supprimer les tirets multiples slug = regexp.MustCompile(`-+`).ReplaceAllString(slug, "-") // Supprimer les tirets en début et fin slug = strings.Trim(slug, "-") return slug } // ContainsOnlyDigits vérifie si une chaîne ne contient que des chiffres func ContainsOnlyDigits(s string) bool { for _, char := range s { if !unicode.IsDigit(char) { return false } } return true } // ContainsOnlyLetters vérifie si une chaîne ne contient que des lettres func ContainsOnlyLetters(s string) bool { for _, char := range s { if !unicode.IsLetter(char) { return false } } return true } // ContainsOnlyAlphanumeric vérifie si une chaîne ne contient que des caractères alphanumériques func ContainsOnlyAlphanumeric(s string) bool { for _, char := range s { if !unicode.IsLetter(char) && !unicode.IsDigit(char) { return false } } return true } // RemoveDuplicates supprime les doublons d'une slice de chaînes func RemoveDuplicates(slice []string) []string { keys := make(map[string]bool) var result []string for _, item := range slice { if !keys[item] { keys[item] = true result = append(result, item) } } return result } // Contains vérifie si une slice contient un élément func Contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } // IndexOf retourne l'index d'un élément dans une slice func IndexOf(slice []string, item string) int { for i, s := range slice { if s == item { return i } } return -1 } // Reverse inverse l'ordre d'une slice func Reverse(slice []string) []string { for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 { slice[i], slice[j] = slice[j], slice[i] } return slice } // Chunk divise une slice en chunks de taille donnée func Chunk(slice []string, chunkSize int) [][]string { var chunks [][]string for i := 0; i < len(slice); i += chunkSize { end := i + chunkSize if end > len(slice) { end = len(slice) } chunks = append(chunks, slice[i:end]) } return chunks } // Filter filtre une slice selon une condition func Filter(slice []string, predicate func(string) bool) []string { var result []string for _, item := range slice { if predicate(item) { result = append(result, item) } } return result } // Map applique une fonction à chaque élément d'une slice func Map(slice []string, mapper func(string) string) []string { var result []string for _, item := range slice { result = append(result, mapper(item)) } return result } // Reduce réduit une slice à une seule valeur func Reduce(slice []string, initial string, reducer func(string, string) string) string { result := initial for _, item := range slice { result = reducer(result, item) } return result }