File size: 3,938 Bytes
e526aa8
 
76e192b
e526aa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76e192b
e526aa8
76e192b
e526aa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from multi_agent_sdlc.runtime.paths import normalise_relative_path
from multi_agent_sdlc.tools.coder.validation import MODULE_PATTERN
from collections.abc import Callable
from packaging.requirements import InvalidRequirement, Requirement
from packaging.utils import canonicalize_name


def validate_application_arguments(
    arguments: list[str],
) -> list[str]:
    """Validate arguments passed to an application or module."""

    validated: list[str] = []

    for argument in arguments:
        if not isinstance(argument, str):
            raise TypeError("Every application argument must be a string.")

        if "\n" in argument or "\r" in argument:
            raise PermissionError("Multiline application arguments are not allowed.")

        if "\x00" in argument:
            raise PermissionError(
                "Null bytes are not allowed in application arguments."
            )

        validated.append(argument)

    return validated


def validate_project_relative_path(path: str) -> str:
    candidate = normalise_relative_path(path.strip())
    return candidate.as_posix()


def validate_file_content(content: str) -> str:
    if "\x00" in content:
        raise ValueError("File content cannot contain null bytes.")

    return content


def create_entry_point_validator(
    role: str,
    prohibited_entry_points: set[str],
) -> Callable[[str], str]:
    def validate_entry_point(entry_point: str) -> str:
        cleaned = entry_point.strip()

        if not cleaned:
            raise ValueError("Entry point cannot be empty.")

        if cleaned.casefold() in prohibited_entry_points:
            raise ValueError(f"The {role} cannot execute {cleaned}.")

        return cleaned

    return validate_entry_point


def create_module_name_validator(
    role: str,
    prohibited_python_modules: set[str],
) -> Callable[[str], str]:

    def validate_module_name(module: str) -> str:
        cleaned = module.strip()

        if not cleaned:
            raise ValueError("Module cannot be empty.")

        if not MODULE_PATTERN.fullmatch(cleaned):
            raise ValueError(f"Invalid Python module name: {cleaned}")

        root_module = cleaned.split(".", maxsplit=1)[0].lower()

        if root_module in prohibited_python_modules:
            raise PermissionError(
                f"The {role} cannot execute Python module `{cleaned}`."
            )

        return cleaned

    return validate_module_name


def create_testing_dependency_validator(
    role: str,
    prohibited_tester_dependencies: set[str],
) -> Callable[[str], str]:
    canonical_prohibited_dependencies = set(
        canonicalize_name(name) for name in prohibited_tester_dependencies
    )

    def validate_testing_dependency(
        dependency: str,
    ) -> str:
        cleaned = dependency.strip()

        if not cleaned:
            raise ValueError("Dependency specification cannot be empty.")

        if cleaned.startswith("-"):
            raise PermissionError(
                "Dependency command-line options are not allowed: " f"{cleaned!r}."
            )

        requirement = parse_dependency(cleaned)

        if requirement.url is not None:
            raise PermissionError(
                "Git, URL, SSH, and local-file dependencies are not "
                f"allowed: {cleaned!r}."
            )

        dependency_name = canonicalize_name(requirement.name)

        if dependency_name in canonical_prohibited_dependencies:
            raise PermissionError(f"The {role} cannot add dependency {cleaned!r}.")

        return cleaned

    return validate_testing_dependency


def parse_dependency(
    specification: str,
) -> Requirement:
    """Parse a Python dependency specification."""

    try:
        return Requirement(specification)
    except InvalidRequirement as error:
        raise ValueError(
            f"Invalid dependency specification: {specification!r}."
        ) from error