File size: 10,573 Bytes
1041734
 
e7b4937
1041734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41ac444
 
 
 
1041734
 
 
 
41ac444
 
 
 
 
 
 
 
 
 
 
1041734
 
 
 
 
41ac444
 
 
1041734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5890f66
1041734
5890f66
 
 
 
 
 
 
1041734
 
 
5890f66
 
 
 
 
 
 
 
 
 
1041734
5890f66
 
 
 
 
 
 
1041734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
"""
Calculator Tool - Safe mathematical expression evaluation
Author: @mangubee
Date: 2026-01-02

Provides safe evaluation of mathematical expressions with:
- Whitelisted operations and functions
- Timeout protection
- Complexity limits
- No access to dangerous built-ins

Security is prioritized over functionality.
"""

import ast
import math
import operator
import logging
from typing import Any, Dict
import signal
from contextlib import contextmanager

# ============================================================================
# CONFIG
# ============================================================================
MAX_EXPRESSION_LENGTH = 500
MAX_EVAL_TIME_SECONDS = 2
MAX_NUMBER_SIZE = 10**100  # Prevent huge number calculations

# Whitelist of safe operations
SAFE_OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.FloorDiv: operator.floordiv,
    ast.Mod: operator.mod,
    ast.Pow: operator.pow,
    ast.USub: operator.neg,
    ast.UAdd: operator.pos,
}

# Whitelist of safe mathematical functions
SAFE_FUNCTIONS = {
    'abs': abs,
    'round': round,
    'min': min,
    'max': max,
    'sum': sum,
    # Math module functions
    'sqrt': math.sqrt,
    'ceil': math.ceil,
    'floor': math.floor,
    'log': math.log,
    'log10': math.log10,
    'exp': math.exp,
    'sin': math.sin,
    'cos': math.cos,
    'tan': math.tan,
    'asin': math.asin,
    'acos': math.acos,
    'atan': math.atan,
    'degrees': math.degrees,
    'radians': math.radians,
    'factorial': math.factorial,
    # Constants
    'pi': math.pi,
    'e': math.e,
}

# ============================================================================
# Logging Setup
# ============================================================================
logger = logging.getLogger(__name__)


# ============================================================================
# Timeout Context Manager
# ============================================================================

class TimeoutError(Exception):
    """Raised when evaluation exceeds timeout"""
    pass


@contextmanager
def timeout(seconds: int):
    """
    Context manager for timeout protection.

    Args:
        seconds: Maximum execution time

    Raises:
        TimeoutError: If execution exceeds timeout

    Note:
        signal.alarm() only works in main thread. In threaded contexts
        (Gradio, ThreadPoolExecutor), timeout protection is disabled.
    """
    def timeout_handler(signum, frame):
        raise TimeoutError(f"Evaluation exceeded {seconds} second timeout")

    try:
        # Set signal handler (only works in main thread)
        old_handler = signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(seconds)
        _alarm_set = True
    except (ValueError, AttributeError):
        # ValueError: signal.alarm() in non-main thread
        # AttributeError: signal.SIGALRM not available (Windows)
        logger.warning(f"Timeout protection disabled (threading/Windows limitation)")
        _alarm_set = False
        old_handler = None

    try:
        yield
    finally:
        # Restore old handler and cancel alarm
        if _alarm_set and old_handler is not None:
            signal.alarm(0)
            signal.signal(signal.SIGALRM, old_handler)


# ============================================================================
# Safe AST Evaluator
# ============================================================================

class SafeEvaluator(ast.NodeVisitor):
    """
    AST visitor that evaluates mathematical expressions safely.

    Only allows whitelisted operations and functions.
    Prevents code execution, attribute access, and other dangerous operations.
    """

    def visit_Expression(self, node):
        """Visit Expression node (root of parse tree)"""
        return self.visit(node.body)

    def visit_Constant(self, node):
        """Visit Constant node (numbers, strings)"""
        value = node.value

        # Only allow numbers
        if not isinstance(value, (int, float, complex)):
            raise ValueError(f"Unsupported constant type: {type(value).__name__}")

        # Prevent huge numbers
        if isinstance(value, (int, float)) and abs(value) > MAX_NUMBER_SIZE:
            raise ValueError(f"Number too large: {value}")

        return value

    def visit_BinOp(self, node):
        """Visit binary operation node (+, -, *, /, etc.)"""
        op_type = type(node.op)

        if op_type not in SAFE_OPERATORS:
            raise ValueError(f"Unsupported operation: {op_type.__name__}")

        left = self.visit(node.left)
        right = self.visit(node.right)

        op_func = SAFE_OPERATORS[op_type]

        # Check for division by zero
        if op_type in (ast.Div, ast.FloorDiv, ast.Mod) and right == 0:
            raise ZeroDivisionError("Division by zero")

        # Prevent huge exponentiations
        if op_type == ast.Pow and abs(right) > 1000:
            raise ValueError(f"Exponent too large: {right}")

        return op_func(left, right)

    def visit_UnaryOp(self, node):
        """Visit unary operation node (-, +)"""
        op_type = type(node.op)

        if op_type not in SAFE_OPERATORS:
            raise ValueError(f"Unsupported unary operation: {op_type.__name__}")

        operand = self.visit(node.operand)
        op_func = SAFE_OPERATORS[op_type]

        return op_func(operand)

    def visit_Call(self, node):
        """Visit function call node"""
        # Only allow simple function names, not attribute access
        if not isinstance(node.func, ast.Name):
            raise ValueError("Only direct function calls are allowed")

        func_name = node.func.id

        if func_name not in SAFE_FUNCTIONS:
            raise ValueError(f"Unsupported function: {func_name}")

        # Evaluate arguments
        args = [self.visit(arg) for arg in node.args]

        # No keyword arguments allowed
        if node.keywords:
            raise ValueError("Keyword arguments not allowed")

        func = SAFE_FUNCTIONS[func_name]

        try:
            return func(*args)
        except Exception as e:
            raise ValueError(f"Error calling {func_name}: {str(e)}")

    def visit_Name(self, node):
        """Visit name node (variable/constant reference)"""
        # Only allow whitelisted constants
        if node.id in SAFE_FUNCTIONS:
            value = SAFE_FUNCTIONS[node.id]
            # If it's a constant (not a function), return it
            if not callable(value):
                return value

        raise ValueError(f"Undefined name: {node.id}")

    def visit_List(self, node):
        """Visit list node"""
        return [self.visit(element) for element in node.elts]

    def visit_Tuple(self, node):
        """Visit tuple node"""
        return tuple(self.visit(element) for element in node.elts)

    def generic_visit(self, node):
        """Catch-all for unsupported node types"""
        raise ValueError(f"Unsupported expression type: {type(node).__name__}")


# ============================================================================
# Public API
# ============================================================================

def safe_eval(expression: str) -> Dict[str, Any]:
    """
    Safely evaluate a mathematical expression.

    Args:
        expression: Mathematical expression string

    Returns:
        Dict with structure: {
            "result": float or int,  # Evaluation result
            "expression": str,       # Original expression
            "success": bool          # True if evaluation succeeded
        }

    Raises:
        ValueError: For invalid or unsafe expressions
        ZeroDivisionError: For division by zero
        TimeoutError: If evaluation exceeds timeout
        SyntaxError: For malformed expressions

    Examples:
        >>> safe_eval("2 + 2")
        {"result": 4, "expression": "2 + 2", "success": True}

        >>> safe_eval("sqrt(16) + 3")
        {"result": 7.0, "expression": "sqrt(16) + 3", "success": True}

        >>> safe_eval("import os")  # Raises ValueError
    """
    # Input validation - relaxed to avoid crashes
    if not expression or not isinstance(expression, str):
        logger.warning("Calculator received empty or non-string expression - returning graceful error")
        return {
            "result": None,
            "expression": str(expression) if expression else "",
            "success": False,
            "error": "Empty expression provided. Calculator requires a mathematical expression string."
        }

    expression = expression.strip()

    # Handle case where expression becomes empty after stripping whitespace
    if not expression:
        logger.warning("Calculator expression was only whitespace - returning graceful error")
        return {
            "result": None,
            "expression": "",
            "success": False,
            "error": "Expression was only whitespace. Provide a valid mathematical expression."
        }

    if len(expression) > MAX_EXPRESSION_LENGTH:
        logger.warning(f"Expression too long ({len(expression)} chars) - returning graceful error")
        return {
            "result": None,
            "expression": expression[:100] + "...",
            "success": False,
            "error": f"Expression too long ({len(expression)} chars). Maximum: {MAX_EXPRESSION_LENGTH} chars"
        }

    logger.info(f"Evaluating expression: {expression}")

    try:
        # Parse expression into AST
        tree = ast.parse(expression, mode='eval')

        # Evaluate with timeout protection
        with timeout(MAX_EVAL_TIME_SECONDS):
            evaluator = SafeEvaluator()
            result = evaluator.visit(tree)

        logger.info(f"Evaluation successful: {result}")

        return {
            "result": result,
            "expression": expression,
            "success": True,
        }

    except SyntaxError as e:
        logger.error(f"Syntax error in expression: {e}")
        raise SyntaxError(f"Invalid expression syntax: {str(e)}")
    except ZeroDivisionError as e:
        logger.error(f"Division by zero: {expression}")
        raise
    except TimeoutError as e:
        logger.error(f"Evaluation timeout: {expression}")
        raise
    except ValueError as e:
        logger.error(f"Invalid expression: {e}")
        raise
    except Exception as e:
        logger.error(f"Unexpected error evaluating expression: {e}")
        raise ValueError(f"Evaluation error: {str(e)}")