use regex::Regex;
use once_cell::sync::Lazy;
use anyhow::{Result, bail};
static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
});
static FILENAME_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9._-]+$").unwrap()
});
pub fn is_valid_email(email: &str) -> bool {
EMAIL_REGEX.is_match(email)
}
pub fn is_valid_filename(filename: &str) -> bool {
!filename.is_empty() &&
filename.len() <= 255 &&
!filename.starts_with('.') &&
FILENAME_REGEX.is_match(filename)
}
pub fn is_safe_path(path: &str) -> bool {
!path.contains("..") &&
!path.starts_with('/') &&
!path.contains("//")
}
pub fn is_valid_file_size(size: u64, max_size: u64) -> bool {
size > 0 && size <= max_size
}
pub fn sanitize_input(input: &str) -> String {
input
.chars()
.filter(|c| c.is_alphanumeric() || c.is_whitespace() || matches!(c, '-' | '_' | '.' | '@'))
.collect::<String>()
.trim()
.to_string()
}
pub fn validate_username(username: &str) -> Result<()> {
if username.len() < 3 || username.len() > 20 {
bail!("Username must be between 3 and 20 characters.");
}
let re = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
if !re.is_match(username) {
bail!("Username can only contain alphanumeric characters and underscores.");
}
Ok(())
}
pub fn validate_password(password: &str) -> Result<()> {
if password.len() < 8 {
bail!("Password must be at least 8 characters long.");
}
let (has_upper, has_lower, has_digit, has_special) = password.chars().fold(
(false, false, false, false),
|(upper, lower, digit, special), c| {
(
upper || c.is_ascii_uppercase(),
lower || c.is_ascii_lowercase(),
digit || c.is_ascii_digit(),
special || !c.is_ascii_alphanumeric(),
)
},
);
if !has_upper || !has_lower || !has_digit || !has_special {
bail!("Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.");
}
Ok(())
}
pub fn sanitize_path(path: &str) -> String {
path.split('/')
.filter(|s| !s.is_empty() && *s != "..")
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_email_validation() {
assert!(is_valid_email("test@example.com"));
assert!(is_valid_email("user.name+tag@domain.co.uk"));
assert!(!is_valid_email("invalid-email"));
assert!(!is_valid_email("@domain.com"));
assert!(!is_valid_email("user@"));
}
#[test]
fn test_filename_validation() {
assert!(is_valid_filename("file.txt"));
assert!(is_valid_filename("document_v2.pdf"));
assert!(!is_valid_filename(""));
assert!(!is_valid_filename(".hidden"));
assert!(!is_valid_filename("file with spaces.txt"));
assert!(!is_valid_filename("file/with/slashes.txt"));
}
#[test]
fn test_path_safety() {
assert!(is_safe_path("documents/file.txt"));
assert!(is_safe_path("folder/subfolder/file.pdf"));
assert!(!is_safe_path("../../../etc/passwd"));
assert!(!is_safe_path("/absolute/path"));
assert!(!is_safe_path("path//with//double//slashes"));
}
}