ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
remote_gdb_args.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Validate and serialize remote-GDB public arguments without side effects."""
4
5from __future__ import annotations
6
7import argparse
8import re
9import shlex
10import subprocess
11import sys
12from collections.abc import Callable
13from pathlib import Path
14
15ASCII_MIN = 0x21
16ASCII_MAX = 0x7E
17DNS_MAX = 253
18MAX_HOST_BYTES = 320
19MAX_SELECTOR_BYTES = 256
20PORT_MIN = 1024
21PORT_MAX = 65535
22IPV4_FIELDS = 4
23IPV4_FIELD_BYTES = 3
24IPV4_MAX = 255
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}")
29
30
31class ArgumentError(ValueError):
32 """One public value cannot safely cross the remote shell boundary."""
33
34
35def _ascii(value: str, label: str, maximum: int) -> None:
36 """Require one nonempty bounded printable-ASCII field."""
37 try:
38 encoded = value.encode("ascii", "strict")
39 except UnicodeEncodeError as exc:
40 message = f"{label} must be ASCII"
41 raise ArgumentError(message) from exc
42 if (
43 not encoded
44 or len(encoded) > maximum
45 or any(byte < ASCII_MIN or byte > ASCII_MAX for byte in encoded)
46 ):
47 message = f"{label} contains whitespace, control bytes, or excessive data"
48 raise ArgumentError(message)
49
50
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("@")
58 if not separator:
59 host = value
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
70 for octet in octets
71 ):
72 message = "PI_HOST IPv4 address is invalid"
73 raise ArgumentError(message)
74 return value
75 if len(host) > DNS_MAX or any(
76 DNS_LABEL_RE.fullmatch(label) is None for label in host.split(".")
77 ):
78 message = "PI_HOST DNS name is invalid"
79 raise ArgumentError(message)
80 return value
81
82
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)
88 return value
89
90
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)
96 return value
97
98
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)
108 return port
109
110
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)
114 validate_port(port)
115 fields = [validate_device(device), serial, port]
116 return shlex.join(["/usr/bin/python3", "-I", "-", "--", *fields])
117
118
119def canonical_app(
120 root: Path,
121 selector: str,
122 runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
123) -> str:
124 """Resolve exactly one app through the canonical option-safe CLI."""
125 _ascii(selector, "application selector", MAX_SELECTOR_BYTES)
126 argv = [
127 "/usr/bin/python3",
128 "-I",
129 str(root / "scripts/dev/ra8_apps.py"),
130 "id",
131 "--",
132 selector,
133 ]
134 result = runner(
135 argv,
136 cwd=root,
137 env={
138 "HOME": "/nonexistent",
139 "LC_ALL": "C",
140 "PATH": "/usr/bin:/bin",
141 "PYTHONNOUSERSITE": "1",
142 },
143 capture_output=True,
144 text=True,
145 timeout=15,
146 check=False,
147 )
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)
152 return lines[0]
153
154
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)
171 return parser
172
173
174def main() -> int:
175 """Dispatch validation without importing repository-local code."""
176 args = _parser().parse_args()
177 try:
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))
187 else:
188 print(canonical_app(Path(args.root), args.selector))
189 except ArgumentError as exc:
190 print(f"remote_gdb_args: {exc}", file=sys.stderr)
191 return 2
192 return 0
193
194
195if __name__ == "__main__":
196 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298