Source code for nostress.utils.validation
"""Input validation utilities."""
import re
from collections.abc import Callable
from pathlib import Path
from typing import Any
import typer
from ..exceptions import ValidationError
[docs]
def validate_file_path(
path: str, must_exist: bool = False, must_not_exist: bool = False
) -> Path:
"""Validate file path.
Args:
path: File path to validate
must_exist: Whether file must exist
must_not_exist: Whether file must not exist
Returns:
Path: Validated path object
Raises:
ValidationError: If validation fails
"""
try:
path_obj = Path(path).expanduser().resolve()
if must_exist and not path_obj.exists():
raise ValidationError(f"File does not exist: {path}")
if must_not_exist and path_obj.exists():
raise ValidationError(f"File already exists: {path}")
# Check parent directory exists
if not path_obj.parent.exists():
raise ValidationError(f"Directory does not exist: {path_obj.parent}")
return path_obj
except (OSError, ValueError) as e:
raise ValidationError(f"Invalid file path: {e}") from e
[docs]
def validate_hex_string(hex_str: str, expected_length: int | None = None) -> str:
"""Validate hexadecimal string.
Args:
hex_str: Hex string to validate
expected_length: Expected length in characters
Returns:
str: Validated hex string
Raises:
ValidationError: If validation fails
"""
# Remove any whitespace
hex_str = hex_str.strip()
# Check if it's a valid hex string
if not re.match(r"^[0-9a-fA-F]+$", hex_str):
raise ValidationError(f"Invalid hexadecimal string: {hex_str}")
# Check length if specified
if expected_length is not None and len(hex_str) != expected_length:
raise ValidationError(
f"Hex string must be {expected_length} characters long, got {len(hex_str)}"
)
return hex_str.lower()
[docs]
def validate_bech32_string(bech32_str: str, expected_prefix: str | None = None) -> str:
"""Validate bech32 string.
Args:
bech32_str: Bech32 string to validate
expected_prefix: Expected prefix (nsec, npub)
Returns:
str: Validated bech32 string
Raises:
ValidationError: If validation fails
"""
bech32_str = bech32_str.strip()
# Basic format check
if not re.match(r"^[a-z0-9]+$", bech32_str):
raise ValidationError(f"Invalid bech32 format: {bech32_str}")
# Check prefix if specified
if expected_prefix is not None and not bech32_str.startswith(expected_prefix):
prefix_part = bech32_str[: len(expected_prefix)]
raise ValidationError(
f"Expected prefix '{expected_prefix}', got: {prefix_part}"
)
return bech32_str
[docs]
def create_validator(validation_func: Callable[[str], Any]) -> Callable[[str], Any]:
"""Create a typer-compatible validator function.
Args:
validation_func: Function that validates and returns the value
Returns:
Callable: Typer-compatible validator
"""
def validator(value: str) -> Any:
try:
return validation_func(value)
except ValidationError as e:
raise typer.BadParameter(str(e)) from e
return validator
# Pre-built validators for common use cases
validate_hex_private_key = create_validator(
lambda x: validate_hex_string(x, expected_length=64)
)
validate_hex_public_key = create_validator(
lambda x: validate_hex_string(x, expected_length=64)
)
validate_bech32_private_key = create_validator(
lambda x: validate_bech32_string(x, expected_prefix="nsec")
)
validate_bech32_public_key = create_validator(
lambda x: validate_bech32_string(x, expected_prefix="npub")
)
validate_output_file = create_validator(
lambda x: validate_file_path(x, must_exist=False)
)
[docs]
def validate_password_strength(password: str, min_length: int = 8) -> str:
"""Validate password strength.
Args:
password: Password to validate
min_length: Minimum required length
Returns:
str: Validated password
Raises:
ValidationError: If password is too weak
"""
if len(password) < min_length:
raise ValidationError(f"Password must be at least {min_length} characters long")
# Check for at least one digit and one letter
has_digit = any(c.isdigit() for c in password)
has_letter = any(c.isalpha() for c in password)
if not (has_digit and has_letter):
raise ValidationError("Password must contain at least one letter and one digit")
return password