3"""Validate and serialize remote-GDB public arguments without side effects."""
5from __future__
import annotations
12from collections.abc
import Callable
13from pathlib
import Path
19MAX_SELECTOR_BYTES = 256
25USER_RE = re.compile(
r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}")
26DNS_LABEL_RE = re.compile(
r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?")
27IDENTIFIER_RE = re.compile(
r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}")
28APP_RE = re.compile(
r"[A-Za-z0-9_][A-Za-z0-9_:@.-]{0,255}")
31class ArgumentError(ValueError):
32 """One public value cannot safely cross the remote shell boundary."""
35def _ascii(value: str, label: str, maximum: int) ->
None:
36 """Require one nonempty bounded printable-ASCII field."""
38 encoded = value.encode(
"ascii",
"strict")
39 except UnicodeEncodeError
as exc:
40 message = f
"{label} must be ASCII"
41 raise ArgumentError(message)
from exc
44 or len(encoded) > maximum
45 or any(byte < ASCII_MIN
or byte > ASCII_MAX
for byte
in encoded)
47 message = f
"{label} contains whitespace, control bytes, or excessive data"
48 raise ArgumentError(message)
51def validate_host(value: str) -> str:
52 """Validate the shared rig contract's optional user plus DNS/IPv4 host."""
53 _ascii(value,
"PI_HOST", MAX_HOST_BYTES)
54 if value.startswith(
"-")
or value.count(
"@") > 1:
55 message =
"PI_HOST is not a destination"
56 raise ArgumentError(message)
57 user, separator, host = value.rpartition(
"@")
60 elif USER_RE.fullmatch(user)
is None:
61 message =
"PI_HOST user is invalid"
62 raise ArgumentError(message)
63 if host.startswith(
"-")
or not host:
64 message =
"PI_HOST host is invalid"
65 raise ArgumentError(message)
66 if "." in host
and all(character
in "0123456789." for character
in host):
67 octets = host.split(
".")
68 if len(octets) != IPV4_FIELDS
or any(
69 not octet
or len(octet) > IPV4_FIELD_BYTES
or int(octet, 10) > IPV4_MAX
72 message =
"PI_HOST IPv4 address is invalid"
73 raise ArgumentError(message)
75 if len(host) > DNS_MAX
or any(
76 DNS_LABEL_RE.fullmatch(label)
is None for label
in host.split(
".")
78 message =
"PI_HOST DNS name is invalid"
79 raise ArgumentError(message)
83def validate_serial(value: str) -> str:
84 """Validate the shared rig contract's serial identifier."""
85 if IDENTIFIER_RE.fullmatch(value)
is None:
86 message =
"JLINK_SN has invalid rig identifier syntax"
87 raise ArgumentError(message)
91def validate_device(value: str) -> str:
92 """Validate the shared rig contract's bounded SEGGER device identifier."""
93 if IDENTIFIER_RE.fullmatch(value)
is None:
94 message =
"JLINK_DEVICE has invalid rig identifier syntax"
95 raise ArgumentError(message)
99def validate_port(value: str) -> int:
100 """Validate the existing unprivileged TCP port contract."""
101 if not value.isascii()
or not value.isdecimal():
102 message =
"port must be decimal"
103 raise ArgumentError(message)
104 port = int(value, 10)
105 if not PORT_MIN <= port <= PORT_MAX:
106 message =
"port must be between 1024 and 65535"
107 raise ArgumentError(message)
111def remote_command(serial: str, port: str, *, device: str) -> str:
112 """Build one exact OpenSSH remote-shell command from validated fields."""
113 validate_serial(serial)
115 fields = [validate_device(device), serial, port]
116 return shlex.join([
"/usr/bin/python3",
"-I",
"-",
"--", *fields])
122 runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
124 """Resolve exactly one app through the canonical option-safe CLI."""
125 _ascii(selector,
"application selector", MAX_SELECTOR_BYTES)
129 str(root /
"scripts/dev/ra8_apps.py"),
138 "HOME":
"/nonexistent",
140 "PATH":
"/usr/bin:/bin",
141 "PYTHONNOUSERSITE":
"1",
148 lines = result.stdout.splitlines()
149 if result.returncode != 0
or len(lines) != 1
or APP_RE.fullmatch(lines[0])
is None:
150 message =
"application selector did not resolve to one canonical id"
151 raise ArgumentError(message)
155def _parser() -> argparse.ArgumentParser:
156 """Build the closed argument-only command interface."""
157 parser = argparse.ArgumentParser(description=__doc__)
158 commands = parser.add_subparsers(dest=
"command", required=
True)
159 validate = commands.add_parser(
"validate")
160 for name
in (
"host",
"serial",
"device",
"port"):
161 validate.add_argument(f
"--{name}", required=
True)
162 port = commands.add_parser(
"validate-port")
163 port.add_argument(
"--port", required=
True)
164 remote = commands.add_parser(
"remote-command")
165 remote.add_argument(
"--device", required=
True)
166 remote.add_argument(
"--serial", required=
True)
167 remote.add_argument(
"--port", required=
True)
168 app = commands.add_parser(
"canonical-app")
169 app.add_argument(
"--root", required=
True)
170 app.add_argument(
"--selector", required=
True)
175 """Dispatch validation without importing repository-local code."""
176 args = _parser().parse_args()
178 if args.command ==
"validate":
179 validate_host(args.host)
180 validate_serial(args.serial)
181 validate_device(args.device)
182 validate_port(args.port)
183 elif args.command ==
"validate-port":
184 validate_port(args.port)
185 elif args.command ==
"remote-command":
186 print(remote_command(args.serial, args.port, device=args.device))
188 print(canonical_app(Path(args.root), args.selector))
189 except ArgumentError
as exc:
190 print(f
"remote_gdb_args: {exc}", file=sys.stderr)
195if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.