File size: 2,850 Bytes
7cc1e9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from __future__ import annotations

"""
Input validation and sanitisation utilities.
Applied at the API boundary to reject malformed or dangerous input.
"""

import re
import logging

logger = logging.getLogger(__name__)

# Maximum allowed length for user chat input
MAX_INPUT_LENGTH = 500

# Maximum allowed length for city name
MAX_CITY_LENGTH = 100


def sanitise_text(text: str, max_length: int = MAX_INPUT_LENGTH) -> str:
    """
    Sanitise user-provided text:
    - Strip leading / trailing whitespace
    - Remove control characters and null bytes
    - Enforce length limit
    """
    if not isinstance(text, str):
        return ""

    # Remove null bytes and control characters (keep newlines, tabs)
    text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text)
    text = text.strip()

    if len(text) > max_length:
        logger.warning(
            "Input truncated: %d chars → %d chars", len(text), max_length
        )
        text = text[:max_length]

    return text


def validate_location(location: dict) -> tuple:
    """
    Validate location payload from /weather/probability.

    Returns
    -------
    (is_valid: bool, error_message: str | None)
    """
    if not isinstance(location, dict):
        return False, "location must be an object"

    lat = location.get("latitude")
    lng = location.get("longitude")

    try:
        lat = float(lat) if lat is not None else None
        lng = float(lng) if lng is not None else None
    except (TypeError, ValueError):
        return False, "latitude and longitude must be numbers"

    if lat is None or lng is None:
        return False, "latitude and longitude are required"

    if not (-90 <= lat <= 90):
        return False, f"latitude {lat} out of range [-90, 90]"

    if not (-180 <= lng <= 180):
        return False, f"longitude {lng} out of range [-180, 180]"

    city_name = location.get("city_name", "")
    if city_name and len(str(city_name)) > MAX_CITY_LENGTH:
        location["city_name"] = str(city_name)[:MAX_CITY_LENGTH]

    return True, None


def validate_date_range(date_range: dict) -> tuple:
    """
    Validate date_range payload.

    Returns
    -------
    (is_valid: bool, error_message: str | None)
    """
    if not isinstance(date_range, dict):
        return False, "date_range must be an object"

    start_date = date_range.get("start_date")
    if not start_date:
        return False, "date_range.start_date is required"

    # Basic ISO date format check
    iso_pattern = re.compile(r"^\d{4}-\d{2}-\d{2}")
    if not iso_pattern.match(str(start_date)):
        return False, "start_date must be in ISO format (YYYY-MM-DD)"

    end_date = date_range.get("end_date")
    if end_date and not iso_pattern.match(str(end_date)):
        return False, "end_date must be in ISO format (YYYY-MM-DD)"

    return True, None