Research-Stack/4-Infrastructure/NoDupeLabs/nodupe/tools/security_audit/validator_logic.py

418 lines
12 KiB
Python

"""Validators Module.
Input validation utilities using standard library only.
Key Features:
- Type validation
- Range validation
- String validation (length, pattern)
- Path validation
- Collection validation
- Standard library only (no external dependencies)
Dependencies:
- typing (standard library)
- re (standard library)
"""
from typing import Any, Type, Optional, List, Union
import re
from pathlib import Path
class ValidationError(Exception):
"""Validation error"""
class Validators:
"""Handle input validation.
Provides comprehensive validation utilities for type checking,
range validation, string validation, and more.
"""
@staticmethod
def validate_type(value: Any, expected_type: Type, allow_none: bool = False) -> bool:
"""Validate type.
Args:
value: Value to validate
expected_type: Expected type
allow_none: If True, None is allowed
Returns:
True if type is valid
Raises:
ValidationError: If type is invalid
"""
if value is None and allow_none:
return True
if not isinstance(value, expected_type):
raise ValidationError(
f"Expected type {expected_type.__name__}, got {type(value).__name__}"
)
return True
@staticmethod
def validate_range(
value: Union[int, float],
min_val: Optional[Union[int, float]] = None,
max_val: Optional[Union[int, float]] = None,
inclusive: bool = True
) -> bool:
"""Validate range.
Args:
value: Value to validate
min_val: Minimum value (None = no minimum)
max_val: Maximum value (None = no maximum)
inclusive: If True, range is inclusive
Returns:
True if value is in range
Raises:
ValidationError: If value is out of range
"""
# Validate type
if not isinstance(value, (int, float)):
raise ValidationError(f"Expected numeric type, got {type(value).__name__}")
# Check minimum
if min_val is not None:
if inclusive:
if value < min_val:
raise ValidationError(f"Value {value} < minimum {min_val}")
else:
if value <= min_val:
raise ValidationError(f"Value {value} <= minimum {min_val}")
# Check maximum
if max_val is not None:
if inclusive:
if value > max_val:
raise ValidationError(f"Value {value} > maximum {max_val}")
else:
if value >= max_val:
raise ValidationError(f"Value {value} >= maximum {max_val}")
return True
@staticmethod
def validate_string_length(
value: str,
min_length: Optional[int] = None,
max_length: Optional[int] = None
) -> bool:
"""Validate string length.
Args:
value: String to validate
min_length: Minimum length (None = no minimum)
max_length: Maximum length (None = no maximum)
Returns:
True if string length is valid
Raises:
ValidationError: If string length is invalid
"""
# Validate type
if not isinstance(value, str):
raise ValidationError(f"Expected string, got {type(value).__name__}")
length = len(value)
# Check minimum length
if min_length is not None and length < min_length:
raise ValidationError(
f"String length {length} < minimum {min_length}"
)
# Check maximum length
if max_length is not None and length > max_length:
raise ValidationError(
f"String length {length} > maximum {max_length}"
)
return True
@staticmethod
def validate_pattern(value: str, pattern: str) -> bool:
"""Validate string against regex pattern.
Args:
value: String to validate
pattern: Regex pattern
Returns:
True if string matches pattern
Raises:
ValidationError: If string doesn't match pattern
"""
# Validate type
if not isinstance(value, str):
raise ValidationError(f"Expected string, got {type(value).__name__}")
# Compile and match pattern
try:
regex = re.compile(pattern)
if not regex.match(value):
raise ValidationError(f"String '{value}' doesn't match pattern '{pattern}'")
except re.error as e:
raise ValidationError(f"Invalid regex pattern '{pattern}': {e}") from e
return True
@staticmethod
def validate_email(email: str) -> bool:
"""Validate email address.
Args:
email: Email address to validate
Returns:
True if email is valid
Raises:
ValidationError: If email is invalid
"""
# Simple email validation regex
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return Validators.validate_pattern(email, email_pattern)
@staticmethod
def validate_path(
file_path: Union[str, Path],
must_exist: bool = False,
must_be_file: bool = False,
must_be_dir: bool = False
) -> bool:
"""Validate file path.
Args:
file_path: Path to validate
must_exist: If True, path must exist
must_be_file: If True, path must be a file
must_be_dir: If True, path must be a directory
Returns:
True if path is valid
Raises:
ValidationError: If path is invalid
"""
# Convert to Path
if isinstance(file_path, str):
file_path = Path(file_path)
if not isinstance(file_path, Path):
raise ValidationError(f"Expected Path or str, got {type(file_path).__name__}")
# Check existence
if must_exist and not file_path.exists():
raise ValidationError(f"Path does not exist: {file_path}")
# Check if file
if must_be_file:
if not file_path.exists():
raise ValidationError(f"File does not exist: {file_path}")
if not file_path.is_file():
raise ValidationError(f"Path is not a file: {file_path}")
# Check if directory
if must_be_dir:
if not file_path.exists():
raise ValidationError(f"Directory does not exist: {file_path}")
if not file_path.is_dir():
raise ValidationError(f"Path is not a directory: {file_path}")
return True
@staticmethod
def validate_enum(value: Any, allowed_values: List[Any]) -> bool:
"""Validate value is in allowed list.
Args:
value: Value to validate
allowed_values: List of allowed values
Returns:
True if value is allowed
Raises:
ValidationError: If value is not allowed
"""
if value not in allowed_values:
raise ValidationError(
f"Value '{value}' not in allowed values: {allowed_values}"
)
return True
@staticmethod
def validate_dict_keys(
data: dict,
required_keys: Optional[List[str]] = None,
allowed_keys: Optional[List[str]] = None
) -> bool:
"""Validate dictionary keys.
Args:
data: Dictionary to validate
required_keys: List of required keys
allowed_keys: List of allowed keys (None = any keys allowed)
Returns:
True if dictionary keys are valid
Raises:
ValidationError: If dictionary keys are invalid
"""
# Validate type
if not isinstance(data, dict):
raise ValidationError(f"Expected dict, got {type(data).__name__}")
# Check required keys
if required_keys is not None:
missing_keys = set(required_keys) - set(data.keys())
if missing_keys:
raise ValidationError(f"Missing required keys: {missing_keys}")
# Check allowed keys
if allowed_keys is not None:
extra_keys = set(data.keys()) - set(allowed_keys)
if extra_keys:
raise ValidationError(f"Unexpected keys: {extra_keys}")
return True
@staticmethod
def validate_list_items(
items: list,
item_type: Type,
min_items: Optional[int] = None,
max_items: Optional[int] = None
) -> bool:
"""Validate list items.
Args:
items: List to validate
item_type: Expected type of items
min_items: Minimum number of items
max_items: Maximum number of items
Returns:
True if list items are valid
Raises:
ValidationError: If list items are invalid
"""
# Validate type
if not isinstance(items, list):
raise ValidationError(f"Expected list, got {type(items).__name__}")
# Check item count
item_count = len(items)
if min_items is not None and item_count < min_items:
raise ValidationError(
f"List has {item_count} items, minimum is {min_items}"
)
if max_items is not None and item_count > max_items:
raise ValidationError(
f"List has {item_count} items, maximum is {max_items}"
)
# Check item types
for i, item in enumerate(items):
if not isinstance(item, item_type):
raise ValidationError(
f"Item {i} has type {type(item).__name__}, "
f"expected {item_type.__name__}"
)
return True
@staticmethod
def validate_boolean(value: Any) -> bool:
"""Validate boolean value.
Args:
value: Value to validate
Returns:
True if value is boolean
Raises:
ValidationError: If value is not boolean
"""
if not isinstance(value, bool):
raise ValidationError(f"Expected bool, got {type(value).__name__}")
return True
@staticmethod
def validate_positive(value: Union[int, float]) -> bool:
"""Validate value is positive.
Args:
value: Value to validate
Returns:
True if value is positive
Raises:
ValidationError: If value is not positive
"""
if not isinstance(value, (int, float)):
raise ValidationError(f"Expected numeric type, got {type(value).__name__}")
if value <= 0:
raise ValidationError(f"Value {value} is not positive")
return True
@staticmethod
def validate_non_negative(value: Union[int, float]) -> bool:
"""Validate value is non-negative.
Args:
value: Value to validate
Returns:
True if value is non-negative
Raises:
ValidationError: If value is negative
"""
if not isinstance(value, (int, float)):
raise ValidationError(f"Expected numeric type, got {type(value).__name__}")
if value < 0:
raise ValidationError(f"Value {value} is negative")
return True
@staticmethod
def validate_non_empty(value: Union[str, list, dict, set, tuple]) -> bool:
"""Validate value is not empty.
Args:
value: Value to validate
Returns:
True if value is not empty
Raises:
ValidationError: If value is empty
"""
if not value:
raise ValidationError(f"Value is empty")
return True