140 lines
3.6 KiB
Python
140 lines
3.6 KiB
Python
"""Common validation utility functions."""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
# Password validation constants
|
|
MIN_PASSWORD_LENGTH = 8
|
|
|
|
|
|
def validate_email(email: str) -> bool:
|
|
"""Validate email address format.
|
|
|
|
Args:
|
|
email: Email address to validate
|
|
|
|
Returns:
|
|
True if email format is valid, False otherwise
|
|
"""
|
|
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
return bool(re.match(pattern, email))
|
|
|
|
|
|
def validate_password_strength(password: str) -> tuple[bool, str | None]:
|
|
"""Validate password meets security requirements.
|
|
|
|
Args:
|
|
password: Password to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if len(password) < MIN_PASSWORD_LENGTH:
|
|
msg = f"Password must be at least {MIN_PASSWORD_LENGTH} characters long"
|
|
return False, msg
|
|
|
|
if not re.search(r"[A-Z]", password):
|
|
return False, "Password must contain at least one uppercase letter"
|
|
|
|
if not re.search(r"[a-z]", password):
|
|
return False, "Password must contain at least one lowercase letter"
|
|
|
|
if not re.search(r"\d", password):
|
|
return False, "Password must contain at least one number"
|
|
|
|
return True, None
|
|
|
|
|
|
def validate_filename(
|
|
filename: str, allowed_extensions: list[str] | None = None
|
|
) -> bool:
|
|
"""Validate filename format and extension.
|
|
|
|
Args:
|
|
filename: Filename to validate
|
|
allowed_extensions: List of allowed file extensions (with dots)
|
|
|
|
Returns:
|
|
True if filename is valid, False otherwise
|
|
"""
|
|
if not filename or filename.startswith(".") or "/" in filename or "\\" in filename:
|
|
return False
|
|
|
|
if allowed_extensions:
|
|
file_path = Path(filename)
|
|
return file_path.suffix.lower() in [ext.lower() for ext in allowed_extensions]
|
|
|
|
return True
|
|
|
|
|
|
def validate_audio_filename(filename: str) -> bool:
|
|
"""Validate audio filename has allowed extension.
|
|
|
|
Args:
|
|
filename: Audio filename to validate
|
|
|
|
Returns:
|
|
True if filename has valid audio extension, False otherwise
|
|
"""
|
|
audio_extensions = [".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".wma"]
|
|
return validate_filename(filename, audio_extensions)
|
|
|
|
|
|
def sanitize_filename(filename: str) -> str:
|
|
"""Sanitize filename by removing/replacing invalid characters.
|
|
|
|
Args:
|
|
filename: Filename to sanitize
|
|
|
|
Returns:
|
|
Sanitized filename safe for filesystem
|
|
"""
|
|
# Remove or replace problematic characters
|
|
sanitized = re.sub(r'[<>:"/\\|?*]', "_", filename)
|
|
|
|
# Remove leading/trailing whitespace and dots
|
|
sanitized = sanitized.strip(" .")
|
|
|
|
# Ensure not empty
|
|
if not sanitized:
|
|
sanitized = "untitled"
|
|
|
|
return sanitized
|
|
|
|
|
|
def validate_url(url: str) -> bool:
|
|
"""Validate URL format.
|
|
|
|
Args:
|
|
url: URL to validate
|
|
|
|
Returns:
|
|
True if URL format is valid, False otherwise
|
|
"""
|
|
pattern = r"^https?://[^\s/$.?#].[^\s]*$"
|
|
return bool(re.match(pattern, url))
|
|
|
|
|
|
def validate_positive_integer(value: Any, field_name: str = "value") -> int:
|
|
"""Validate and convert value to positive integer.
|
|
|
|
Args:
|
|
value: Value to validate and convert
|
|
field_name: Name of field for error messages
|
|
|
|
Returns:
|
|
Validated positive integer
|
|
|
|
Raises:
|
|
ValueError: If value is not a positive integer
|
|
"""
|
|
try:
|
|
int_value = int(value)
|
|
if int_value <= 0:
|
|
msg = f"{field_name} must be a positive integer"
|
|
raise ValueError(msg)
|
|
return int_value
|
|
except (TypeError, ValueError) as e:
|
|
msg = f"{field_name} must be a positive integer"
|
|
raise ValueError(msg) from e |