use once_cell::sync::Lazy; use regex::Regex; static PHONE_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[6-9]\d{9}$").unwrap()); static EMAIL_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()); static PINCODE_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[1-9]\d{5}$").unwrap()); pub fn validate_email(email: &str) -> Result<(), ValidationError> { if email.is_empty() { return Err(ValidationError::new("Email cannot be empty")); } if email.len() > 255 { return Err(ValidationError::new("Email too long")); } if !EMAIL_REGEX.is_match(email) { return Err(ValidationError::new("Invalid email format")); } Ok(()) } pub fn validate_pincode(pincode: &str) -> Result<(), ValidationError> { if pincode.is_empty() { return Err(ValidationError::new("Pincode cannot be empty")); } if !PINCODE_REGEX.is_match(pincode) { return Err(ValidationError::new( "Invalid Indian pincode format (6 digits starting with 1-9)", )); } Ok(()) } pub fn validate_price(price: f64) -> Result<(), ValidationError> { if price <= 0.0 { return Err(ValidationError::new("Price must be greater than 0")); } if price > 1_000_000.0 { return Err(ValidationError::new("Price exceeds maximum allowed value")); } if price.is_nan() || price.is_infinite() { return Err(ValidationError::new("Invalid price value")); } Ok(()) } pub fn validate_product_name(name: &str) -> Result<(), ValidationError> { if name.trim().is_empty() { return Err(ValidationError::new("Product name cannot be empty")); } if name.len() > 200 { return Err(ValidationError::new( "Product name too long (max 200 characters)", )); } Ok(()) } pub fn validate_weight(weight: f64) -> Result<(), ValidationError> { if weight < 0.0 { return Err(ValidationError::new("Weight cannot be negative")); } if weight > 100_000.0 { // 100kg limit for social checkout return Err(ValidationError::new( "Weight exceeds maximum allowed value (100kg)", )); } if weight.is_nan() || weight.is_infinite() { return Err(ValidationError::new("Invalid weight value")); } Ok(()) } pub fn sanitize_string(input: &str) -> String { input .trim() .chars() .filter(|c| !c.is_control()) .take(1000) .collect() } pub fn normalize_phone(phone: &str) -> Result { if phone.is_empty() { return Err(ValidationError::new("Phone cannot be empty")); } let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect(); let normalized = match digits.len() { 10 => digits, 11 if digits.starts_with('0') => digits[1..].to_string(), 12 if digits.starts_with("91") => digits[2..].to_string(), _ => return Err(ValidationError::new("Invalid Indian phone number format")), }; if !PHONE_REGEX.is_match(&normalized) { return Err(ValidationError::new("Invalid Indian phone number format")); } Ok(normalized) } pub fn validate_base64_payload(data: &str, max_bytes: usize) -> Result, ValidationError> { if data.is_empty() { return Err(ValidationError::new("Base64 payload cannot be empty")); } let raw_data = if let Some(pos) = data.find(',') { &data[pos + 1..] } else { data }; use base64::{engine::general_purpose, Engine as _}; match general_purpose::STANDARD.decode(raw_data) { Ok(bytes) => { if bytes.len() > max_bytes { return Err(ValidationError::new(&format!( "Payload exceeds maximum size of {} bytes", max_bytes ))); } Ok(bytes) } Err(_) => Err(ValidationError::new("Invalid Base64 encoding")), } } pub fn sanitize_filename(filename: &str) -> String { let sanitized: String = filename .replace("..", "") .chars() .filter(|c| c.is_alphanumeric() || *c == '.' || *c == '-' || *c == '_') .collect(); if sanitized.starts_with('.') || sanitized.is_empty() { return "unnamed".to_string(); } sanitized.chars().take(255).collect() } #[derive(Debug, Clone)] pub struct ValidationError { pub message: String, } impl ValidationError { pub fn new(msg: &str) -> Self { Self { message: msg.to_string(), } } } impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl std::error::Error for ValidationError {}