File size: 2,303 Bytes
639a496
 
ea655e4
 
 
 
639a496
 
 
 
 
 
ea655e4
 
 
 
 
639a496
ea655e4
639a496
ea655e4
 
639a496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea655e4
 
639a496
 
 
9ae008a
ea655e4
 
 
 
 
 
639a496
ea655e4
 
 
 
 
 
639a496
 
 
 
 
 
ea655e4
 
 
639a496
 
 
 
 
 
 
 
 
 
 
 
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
import sys
from typing import Any, Callable, Dict, Tuple


SAFE_BUILTINS = {
    "abs": abs,
    "bool": bool,
    "dict": dict,
    "enumerate": enumerate,
    "float": float,
    "int": int,
    "list": list,
    "len": len,
    "max": max,
    "min": min,
    "print": print,
    "range": range,
    "str": str,
    "sum": sum,
    "tuple": tuple,
}

MAX_SOURCE_CHARS = 20_000
DEFAULT_MAX_EXECUTION_LINES = 4_000


class ExecutionLimitExceeded(RuntimeError):
    pass


def _run_with_line_limit(fn: Callable[[], Any], max_executed_lines: int) -> Any:
    counter = {"lines": 0}

    def tracer(frame, event, arg):  # noqa: ANN001
        if event == "line":
            counter["lines"] += 1
            if counter["lines"] > max_executed_lines:
                raise ExecutionLimitExceeded(
                    f"Execution limit exceeded ({max_executed_lines} lines)."
                )
        return tracer

    previous_tracer = sys.gettrace()
    sys.settrace(tracer)
    try:
        return fn()
    finally:
        sys.settrace(previous_tracer)


def compile_code(code: str) -> Tuple[bool, str]:
    if len(code) > MAX_SOURCE_CHARS:
        return False, f"Code exceeds max length of {MAX_SOURCE_CHARS} characters."

    try:
        compile(code, "<candidate>", "exec")
        return True, ""
    except SyntaxError as exc:
        return False, f"SyntaxError: {exc.msg} (line {exc.lineno})"


def execute_code(code: str, max_executed_lines: int = DEFAULT_MAX_EXECUTION_LINES) -> Tuple[bool, Dict[str, Any], str]:
    syntax_ok, syntax_error = compile_code(code)
    if not syntax_ok:
        return False, {}, syntax_error

    namespace: Dict[str, Any] = {"__builtins__": SAFE_BUILTINS}
    try:
        _run_with_line_limit(
            lambda: exec(code, namespace, namespace),
            max_executed_lines=max_executed_lines,
        )
    except ExecutionLimitExceeded as exc:
        return False, {}, str(exc)
    except Exception as exc:
        return False, {}, f"{exc.__class__.__name__}: {exc}"

    return True, namespace, ""


def invoke_callable(
    fn: Callable[..., Any],
    *args: Any,
    max_executed_lines: int = DEFAULT_MAX_EXECUTION_LINES,
) -> Any:
    return _run_with_line_limit(
        lambda: fn(*args),
        max_executed_lines=max_executed_lines,
    )