pyGdbToolkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Session-cached Arm target inspection shared by the Arm-aware GDB commands.
6
+
7
+ This module is not exported by :mod:`pyGdbToolkit.arch.arm` on purpose: it binds
8
+ the Arm inspection results to the unified session, whereas the ``arch`` package
9
+ stays independent from any session or GDB concern.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+ from ...session import SESSION, SessionSlice, ToolkitSession
17
+ from .coresight import CoreSightDiscovery, discover_rom_tables
18
+ from .cortex_m import CPUID_ADDRESS, CortexMTargetDescription, decode_cpuid
19
+ from .models import DeviceReport
20
+ from .providers import DEFAULT_PROVIDER_REGISTRY
21
+
22
+
23
+ @dataclass
24
+ class ArmInspectionState(SessionSlice):
25
+ """Cache the Arm identity, the ROM-table discovery, and the vendor device report."""
26
+
27
+ target: CortexMTargetDescription | None = None
28
+ discovery: CoreSightDiscovery | None = None
29
+ report: DeviceReport | None = None
30
+
31
+ def reset(self) -> None:
32
+ """Drop every cached Arm inspection result."""
33
+ self.target = None
34
+ self.discovery = None
35
+ self.report = None
36
+
37
+
38
+ def cortex_m_target(session: ToolkitSession = SESSION) -> CortexMTargetDescription:
39
+ """Return the decoded Cortex-M CPUID identity of the session target.
40
+
41
+ Parameters
42
+ ----------
43
+ session : ToolkitSession
44
+ Session owning the target access and the cached inspection results.
45
+
46
+ Returns
47
+ -------
48
+ CortexMTargetDescription
49
+ The decoded CPUID identity.
50
+ """
51
+ state = session.state(ArmInspectionState)
52
+ if state.target is None:
53
+ state.target = decode_cpuid(session.memory.read_uint32(CPUID_ADDRESS))
54
+ return state.target
55
+
56
+
57
+ def rom_table_discovery(session: ToolkitSession = SESSION) -> CoreSightDiscovery:
58
+ """Return the CoreSight ROM-table discovery of the session target.
59
+
60
+ Parameters
61
+ ----------
62
+ session : ToolkitSession
63
+ Session owning the target access and the cached inspection results.
64
+
65
+ Returns
66
+ -------
67
+ CoreSightDiscovery
68
+ The MCU and processor ROM-table discovery results.
69
+ """
70
+ state = session.state(ArmInspectionState)
71
+ if state.discovery is None:
72
+ state.discovery = discover_rom_tables(session.memory)
73
+ return state.discovery
74
+
75
+
76
+ def device_report(session: ToolkitSession = SESSION) -> DeviceReport:
77
+ """Return the manufacturer device report of the session target.
78
+
79
+ Parameters
80
+ ----------
81
+ session : ToolkitSession
82
+ Session owning the target access and the cached inspection results.
83
+
84
+ Returns
85
+ -------
86
+ DeviceReport
87
+ The report of the first provider recognizing the target, or a generic one.
88
+ """
89
+ state = session.state(ArmInspectionState)
90
+ if state.report is None:
91
+ state.report = DEFAULT_PROVIDER_REGISTRY.inspect(
92
+ session.memory,
93
+ cortex_m_target(session),
94
+ rom_table_discovery(session),
95
+ )
96
+ return state.report
@@ -0,0 +1,32 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Shared Arm target-description types."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum, unique
10
+
11
+ from ..base import Architecture, TargetDescription
12
+
13
+
14
+ @unique
15
+ class ArmProfile(StrEnum):
16
+ """Architectural execution profiles implemented by Arm cores."""
17
+
18
+ CORTEX_M = "cortex-m"
19
+ CORTEX_A = "cortex-a"
20
+ CORTEX_R = "cortex-r"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ArmTargetDescription(TargetDescription):
25
+ """Target identity common to all Arm architecture profiles."""
26
+
27
+ profile: ArmProfile
28
+
29
+ def __post_init__(self) -> None:
30
+ """Require Arm target descriptions to retain the Arm architecture tag."""
31
+ if self.architecture is not Architecture.ARM:
32
+ raise ValueError("an Arm target description must use the Arm architecture")
@@ -0,0 +1,143 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Portable architecture target-description and register-reading contracts."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum, unique
10
+ from typing import Protocol
11
+
12
+ from ..target_memory import TargetMemory
13
+
14
+
15
+ @unique
16
+ class Architecture(StrEnum):
17
+ """Architectures supported by the target-description registry."""
18
+
19
+ ARM = "arm"
20
+ RISCV = "riscv"
21
+ XTENSA = "xtensa"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class TargetDescription:
26
+ """Architecture-neutral identity for an inspected target."""
27
+
28
+ architecture: Architecture
29
+ family: str
30
+ core_name: str
31
+ revision: str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class RegisterValue:
36
+ """A system-register value or the explicit reason it could not be read."""
37
+
38
+ name: str
39
+ address: int
40
+ width_bits: int
41
+ value: int | None = None
42
+ unavailable_reason: str | None = None
43
+
44
+ def __post_init__(self) -> None:
45
+ """Validate that a register result has exactly one value state."""
46
+ if (self.value is None) == (self.unavailable_reason is None):
47
+ raise ValueError("a register value must be available or unavailable")
48
+ if self.address < 0:
49
+ raise ValueError("a register address must not be negative")
50
+ if self.width_bits <= 0:
51
+ raise ValueError("a register width must be positive")
52
+
53
+ @classmethod
54
+ def known(cls, name: str, address: int, width_bits: int, value: int) -> RegisterValue:
55
+ """Create an available register value."""
56
+ return cls(name=name, address=address, width_bits=width_bits, value=value)
57
+
58
+ @classmethod
59
+ def unavailable(
60
+ cls,
61
+ name: str,
62
+ address: int,
63
+ width_bits: int,
64
+ reason: str,
65
+ ) -> RegisterValue:
66
+ """Create an unavailable register result."""
67
+ return cls(
68
+ name=name,
69
+ address=address,
70
+ width_bits=width_bits,
71
+ unavailable_reason=reason,
72
+ )
73
+
74
+ @property
75
+ def is_available(self) -> bool:
76
+ """Whether the target supplied a register value."""
77
+ return self.value is not None
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class SystemRegisterSet:
82
+ """One architecture-specific system-register block read through a common API."""
83
+
84
+ architecture: Architecture
85
+ block_name: str
86
+ registers: tuple[RegisterValue, ...]
87
+
88
+ def get(self, name: str) -> RegisterValue | None:
89
+ """Find a register by case-insensitive name."""
90
+ normalized_name = name.upper()
91
+ for register in self.registers:
92
+ if register.name.upper() == normalized_name:
93
+ return register
94
+ return None
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class ProbeResult:
99
+ """A probe result containing a target description or explicit unavailability."""
100
+
101
+ target: TargetDescription | None = None
102
+ unavailable_reason: str | None = None
103
+ access_error: bool = False
104
+
105
+ def __post_init__(self) -> None:
106
+ """Validate that a probe result has exactly one state."""
107
+ if (self.target is None) == (self.unavailable_reason is None):
108
+ raise ValueError("a probe result must contain a target or an unavailable reason")
109
+ if self.target is not None and self.access_error:
110
+ raise ValueError("a successful probe result cannot contain an access error")
111
+
112
+ @classmethod
113
+ def detected(cls, target: TargetDescription) -> ProbeResult:
114
+ """Create a successful probe result."""
115
+ return cls(target=target)
116
+
117
+ @classmethod
118
+ def unavailable(cls, reason: str, *, access_error: bool = False) -> ProbeResult:
119
+ """Create an unsuccessful probe result."""
120
+ return cls(unavailable_reason=reason, access_error=access_error)
121
+
122
+ @property
123
+ def is_available(self) -> bool:
124
+ """Whether an architecture probe identified a target."""
125
+ return self.target is not None
126
+
127
+
128
+ class ArchitectureProbe(Protocol):
129
+ """An architecture implementation that can identify and inspect a target."""
130
+
131
+ architecture: Architecture
132
+
133
+ def probe(self, reader: TargetMemory) -> ProbeResult:
134
+ """Identify a target or return an explicit unsupported result."""
135
+ ...
136
+
137
+ def read_system_registers(
138
+ self,
139
+ reader: TargetMemory,
140
+ target: TargetDescription,
141
+ ) -> SystemRegisterSet:
142
+ """Read the architecture-specific system-register block for a target."""
143
+ ...
@@ -0,0 +1,300 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Portable diagnostic-service contracts, reports, and runtime dispatch."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum, auto, unique
10
+ from typing import Protocol, TypeAlias
11
+
12
+ from ..target_memory import TargetMemory
13
+ from .base import Architecture, TargetDescription
14
+ from .registry import ArchitectureRegistry
15
+
16
+ DiagnosticValue: TypeAlias = bool | int | str | None
17
+
18
+
19
+ @unique
20
+ class DiagnosticServiceName(StrEnum):
21
+ """Architecture-neutral diagnostics offered by command entry points."""
22
+
23
+ SECURITY_AUDIT = "security-audit"
24
+ FAULT_ANALYSIS = "fault-analysis"
25
+
26
+
27
+ @unique
28
+ class DiagnosticSeverity(StrEnum):
29
+ """Common severity levels used by diagnostic findings."""
30
+
31
+ PASS = auto()
32
+ INFO = auto()
33
+ WARNING = auto()
34
+ ERROR = auto()
35
+ CRITICAL = auto()
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class DiagnosticField:
40
+ """One named, scalar diagnostic value suitable for a renderer."""
41
+
42
+ name: str
43
+ value: DiagnosticValue
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class DiagnosticTableRow:
48
+ """One renderer-independent row in a diagnostic table."""
49
+
50
+ values: tuple[DiagnosticValue, ...]
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class DiagnosticTable:
55
+ """One renderer-independent table in a diagnostic report."""
56
+
57
+ title: str
58
+ columns: tuple[str, ...]
59
+ rows: tuple[DiagnosticTableRow, ...]
60
+
61
+ def __post_init__(self) -> None:
62
+ """Ensure every row matches the declared table column count."""
63
+ if not self.columns:
64
+ raise ValueError("a diagnostic table must have at least one column")
65
+ if any(len(row.values) != len(self.columns) for row in self.rows):
66
+ raise ValueError("diagnostic table rows must match the column count")
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class DiagnosticPanel:
71
+ """One renderer-independent, ordered group of diagnostic lines."""
72
+
73
+ title: str
74
+ lines: tuple[str, ...]
75
+
76
+
77
+ DiagnosticContent: TypeAlias = DiagnosticTable | DiagnosticPanel
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class DiagnosticFinding:
82
+ """One portable diagnostic observation."""
83
+
84
+ category: str
85
+ severity: DiagnosticSeverity
86
+ title: str
87
+ detail: str
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class DiagnosticSection:
92
+ """One renderer-independent grouping of diagnostic values."""
93
+
94
+ title: str
95
+ fields: tuple[DiagnosticField, ...]
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class DiagnosticReport:
100
+ """A complete architecture-specific diagnostic report."""
101
+
102
+ service: DiagnosticServiceName
103
+ target: TargetDescription
104
+ sections: tuple[DiagnosticSection, ...] = ()
105
+ findings: tuple[DiagnosticFinding, ...] = ()
106
+ tables: tuple[DiagnosticTable, ...] = ()
107
+ panels: tuple[DiagnosticPanel, ...] = ()
108
+ blocks: tuple[DiagnosticContent, ...] = ()
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class DiagnosticResult:
113
+ """A completed report or a reason the requested diagnostic is unavailable."""
114
+
115
+ report: DiagnosticReport | None = None
116
+ target: TargetDescription | None = None
117
+ unavailable_reason: str | None = None
118
+ access_error: bool = False
119
+
120
+ def __post_init__(self) -> None:
121
+ """Require exactly one completed or unavailable result state."""
122
+ if (self.report is None) == (self.unavailable_reason is None):
123
+ raise ValueError("a diagnostic result must contain a report or an unavailable reason")
124
+ if self.report is not None and self.target != self.report.target:
125
+ raise ValueError("a completed diagnostic result must retain its report target")
126
+ if self.report is not None and self.access_error:
127
+ raise ValueError("a completed diagnostic result cannot contain an access error")
128
+
129
+ @classmethod
130
+ def completed(cls, report: DiagnosticReport) -> DiagnosticResult:
131
+ """Create a completed diagnostic result."""
132
+ return cls(report=report, target=report.target)
133
+
134
+ @classmethod
135
+ def unavailable(
136
+ cls,
137
+ reason: str,
138
+ target: TargetDescription | None = None,
139
+ *,
140
+ access_error: bool = False,
141
+ ) -> DiagnosticResult:
142
+ """Create an explicitly unavailable diagnostic result."""
143
+ return cls(target=target, unavailable_reason=reason, access_error=access_error)
144
+
145
+ @property
146
+ def is_available(self) -> bool:
147
+ """Whether the requested diagnostic completed."""
148
+ return self.report is not None
149
+
150
+
151
+ class DiagnosticService(Protocol):
152
+ """An architecture-specific collector behind a portable diagnostic service."""
153
+
154
+ architecture: Architecture
155
+ service: DiagnosticServiceName
156
+
157
+ def supports(self, target: TargetDescription) -> bool:
158
+ """Return whether the service can collect this target description."""
159
+ ...
160
+
161
+ def collect(
162
+ self,
163
+ reader: TargetMemory,
164
+ target: TargetDescription,
165
+ access: DiagnosticRuntimeAccess | None = None,
166
+ ) -> DiagnosticReport:
167
+ """Collect one report for a compatible target."""
168
+ ...
169
+
170
+
171
+ class DiagnosticRegisterReader(Protocol):
172
+ """Read a runtime register from any of a service-provided set of names."""
173
+
174
+ def read_first(self, names: tuple[str, ...]) -> int | None:
175
+ """Return the first available named register, or ``None``."""
176
+ ...
177
+
178
+
179
+ class DiagnosticSymbolResolver(Protocol):
180
+ """Resolve a runtime address for architecture-neutral report renderers."""
181
+
182
+ def resolve(self, address: int) -> str:
183
+ """Return a symbol description or ``"?"`` when no symbol is available."""
184
+ ...
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class DiagnosticRuntimeAccess:
189
+ """Optional neutral runtime facilities needed by a diagnostic collector."""
190
+
191
+ registers: DiagnosticRegisterReader | None = None
192
+ symbols: DiagnosticSymbolResolver | None = None
193
+
194
+
195
+ class DiagnosticRuntime(Protocol):
196
+ """Portable runtime entry point used by architecture-neutral commands."""
197
+
198
+ def diagnose(
199
+ self,
200
+ reader: TargetMemory,
201
+ service: DiagnosticServiceName,
202
+ access: DiagnosticRuntimeAccess | None = None,
203
+ ) -> DiagnosticResult:
204
+ """Identify the target and run one requested diagnostic service."""
205
+ ...
206
+
207
+
208
+ class DiagnosticServiceRegistry:
209
+ """Resolve diagnostic collectors by requested service and target architecture."""
210
+
211
+ def __init__(self, services: tuple[DiagnosticService, ...] = ()) -> None:
212
+ """Create a registry and register its initial service implementations."""
213
+ self._services: dict[tuple[Architecture, DiagnosticServiceName], DiagnosticService] = {}
214
+ for service in services:
215
+ self.register(service)
216
+
217
+ def register(self, service: DiagnosticService) -> None:
218
+ """Register one service implementation.
219
+
220
+ Raises
221
+ ------
222
+ ValueError
223
+ If a service is already registered for the same architecture and name.
224
+ """
225
+ key = (service.architecture, service.service)
226
+ if key in self._services:
227
+ raise ValueError(
228
+ f"diagnostic service '{service.service}' is already registered "
229
+ f"for architecture '{service.architecture}'"
230
+ )
231
+ self._services[key] = service
232
+
233
+ def resolve(
234
+ self,
235
+ service: DiagnosticServiceName,
236
+ target: TargetDescription,
237
+ ) -> DiagnosticService | None:
238
+ """Return the registered service implementation compatible with a target."""
239
+ implementation = self._services.get((target.architecture, service))
240
+ if implementation is None or not implementation.supports(target):
241
+ return None
242
+ return implementation
243
+
244
+ def dispatch(
245
+ self,
246
+ reader: TargetMemory,
247
+ service: DiagnosticServiceName,
248
+ target: TargetDescription,
249
+ access: DiagnosticRuntimeAccess | None = None,
250
+ ) -> DiagnosticResult:
251
+ """Run a compatible service or return an explicit unsupported result."""
252
+ implementation = self._services.get((target.architecture, service))
253
+ if implementation is None:
254
+ return DiagnosticResult.unavailable(
255
+ (
256
+ f"diagnostic service '{service}' is not registered "
257
+ f"for architecture '{target.architecture}'"
258
+ ),
259
+ target,
260
+ )
261
+ if not implementation.supports(target):
262
+ return DiagnosticResult.unavailable(
263
+ f"diagnostic service '{service}' does not support target '{target.core_name}'",
264
+ target,
265
+ )
266
+
267
+ report = implementation.collect(reader, target, access)
268
+ if report.service is not service:
269
+ raise ValueError("diagnostic service returned a report for a different service")
270
+ if report.target != target:
271
+ raise ValueError("diagnostic service returned a report for a different target")
272
+ return DiagnosticResult.completed(report)
273
+
274
+
275
+ class ArchitectureDiagnosticRuntime:
276
+ """Adapt architecture probing and service dispatch to the diagnostic runtime protocol."""
277
+
278
+ def __init__(
279
+ self,
280
+ architecture_registry: ArchitectureRegistry,
281
+ service_registry: DiagnosticServiceRegistry,
282
+ ) -> None:
283
+ """Create a runtime using the supplied architecture and service registries."""
284
+ self._architecture_registry = architecture_registry
285
+ self._service_registry = service_registry
286
+
287
+ def diagnose(
288
+ self,
289
+ reader: TargetMemory,
290
+ service: DiagnosticServiceName,
291
+ access: DiagnosticRuntimeAccess | None = None,
292
+ ) -> DiagnosticResult:
293
+ """Identify the target, then dispatch its implementation of a diagnostic."""
294
+ probe_result = self._architecture_registry.probe(reader)
295
+ if probe_result.target is None:
296
+ return DiagnosticResult.unavailable(
297
+ probe_result.unavailable_reason or "target unavailable",
298
+ access_error=probe_result.access_error,
299
+ )
300
+ return self._service_registry.dispatch(reader, service, probe_result.target, access)
@@ -0,0 +1,43 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Architecture probe registry."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Sequence
9
+
10
+ from ..target_memory import TargetMemory
11
+ from .base import ArchitectureProbe, ProbeResult
12
+
13
+
14
+ class ArchitectureRegistry:
15
+ """Run registered architecture probes in their declared priority order."""
16
+
17
+ def __init__(self, probes: Sequence[ArchitectureProbe]) -> None:
18
+ """Create a registry from an immutable probe sequence."""
19
+ self._probes = tuple(probes)
20
+
21
+ def probe(self, reader: TargetMemory) -> ProbeResult:
22
+ """Return the first successful architecture probe result.
23
+
24
+ Parameters
25
+ ----------
26
+ reader
27
+ Target-memory reader used by each architecture probe.
28
+
29
+ Returns
30
+ -------
31
+ ProbeResult
32
+ The first detected target or an explicit unsupported result.
33
+ """
34
+ access_error: ProbeResult | None = None
35
+ for probe in self._probes:
36
+ result = probe.probe(reader)
37
+ if result.is_available:
38
+ return result
39
+ if result.access_error and access_error is None:
40
+ access_error = result
41
+ if access_error is not None:
42
+ return access_error
43
+ return ProbeResult.unavailable("no registered architecture probe recognized the target")