openserve 2.0.3

A modern, high-performance, AI-enhanced file server built in Rust
Documentation
//! Validation Utilities
//!
//! This module provides utility functions for data validation,
//! such as checking for valid usernames, passwords, and file paths.
//! These functions help ensure data integrity and security.

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()
});

/// Validates an email address format using a regular expression.
///
/// This function checks if the provided string matches a standard email
/// format pattern. It does not verify if the email address actually exists.
///
/// # Arguments
///
/// * `email` - The email address string to validate.
///
/// # Returns
///
/// `true` if the email format is valid, `false` otherwise.
pub fn is_valid_email(email: &str) -> bool {
    EMAIL_REGEX.is_match(email)
}

/// Validates a filename to ensure it contains only safe characters.
///
/// This function checks that the filename is not empty, does not exceed
/// 255 characters, does not start with a dot (hidden file), and contains
/// only alphanumeric characters, dots, underscores, and hyphens.
///
/// # Arguments
///
/// * `filename` - The filename to validate.
///
/// # Returns
///
/// `true` if the filename is valid, `false` otherwise.
pub fn is_valid_filename(filename: &str) -> bool {
    !filename.is_empty() && 
    filename.len() <= 255 && 
    !filename.starts_with('.') &&
    FILENAME_REGEX.is_match(filename)
}

/// Checks if a file path is safe from directory traversal attacks.
///
/// This function ensures the path does not contain ".." sequences,
/// does not start with an absolute path ("/"), and does not contain
/// double slashes ("//").
///
/// # Arguments
///
/// * `path` - The file path to validate.
///
/// # Returns
///
/// `true` if the path is safe, `false` otherwise.
pub fn is_safe_path(path: &str) -> bool {
    !path.contains("..") && 
    !path.starts_with('/') &&
    !path.contains("//")
}

/// Validates that a file size is within acceptable limits.
///
/// # Arguments
///
/// * `size` - The file size in bytes.
/// * `max_size` - The maximum allowed file size in bytes.
///
/// # Returns
///
/// `true` if the file size is valid (greater than 0 and within limits),
/// `false` otherwise.
pub fn is_valid_file_size(size: u64, max_size: u64) -> bool {
    size > 0 && size <= max_size
}

/// Sanitizes an input string by removing potentially dangerous characters.
///
/// This function keeps only alphanumeric characters, whitespace, and a
/// limited set of safe special characters (-, _, ., @). It also trims
/// leading and trailing whitespace.
///
/// # Arguments
///
/// * `input` - The input string to sanitize.
///
/// # Returns
///
/// A sanitized `String`.
pub fn sanitize_input(input: &str) -> String {
    input
        .chars()
        .filter(|c| c.is_alphanumeric() || c.is_whitespace() || matches!(c, '-' | '_' | '.' | '@'))
        .collect::<String>()
        .trim()
        .to_string()
}

/// Validates a username.
///
/// Usernames must be between 3 and 20 characters long and can
/// only contain alphanumeric characters and underscores.
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(())
}

/// Validates a password.
///
/// Passwords must be at least 8 characters long and contain at least
/// one uppercase letter, one lowercase letter, one number, and one
/// special character.
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(())
}

/// Sanitizes a file path to prevent directory traversal attacks.
///
/// This function removes empty path segments and ".." sequences that
/// could be used for directory traversal attacks.
///
/// # Arguments
///
/// * `path` - The file path to sanitize.
///
/// # Returns
///
/// A sanitized path `String`.
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"));
    }
}