iotsploit-protocols 0.0.9__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.
- iotsploit_protocols/__init__.py +20 -0
- iotsploit_protocols/autosar/__init__.py +12 -0
- iotsploit_protocols/autosar/arxml.py +765 -0
- iotsploit_protocols/canbus/__init__.py +56 -0
- iotsploit_protocols/canbus/bus_match.py +121 -0
- iotsploit_protocols/canbus/catalog.py +425 -0
- iotsploit_protocols/canbus/codec.py +455 -0
- iotsploit_protocols/canbus/definitions.py +252 -0
- iotsploit_protocols/canbus/errorframes.py +154 -0
- iotsploit_protocols/canbus/errors.py +47 -0
- iotsploit_protocols/canbus/logfile.py +753 -0
- iotsploit_protocols/canbus/socketcan.py +385 -0
- iotsploit_protocols/doip/__init__.py +22 -0
- iotsploit_protocols/doip/client.py +267 -0
- iotsploit_protocols/doip/facet.py +51 -0
- iotsploit_protocols/doip/uds.py +262 -0
- iotsploit_protocols/errors.py +45 -0
- iotsploit_protocols/someip/__init__.py +19 -0
- iotsploit_protocols/someip/client.py +322 -0
- iotsploit_protocols/someip/codec.py +11 -0
- iotsploit_protocols/someip/facet.py +71 -0
- iotsploit_protocols/someip/sd.py +355 -0
- iotsploit_protocols-0.0.9.dist-info/METADATA +118 -0
- iotsploit_protocols-0.0.9.dist-info/RECORD +25 -0
- iotsploit_protocols-0.0.9.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""What a target says about one CAN frame, frozen and free of storage detail.
|
|
2
|
+
|
|
3
|
+
A target holds CAN frames in two places for historical reasons: ARXML frames
|
|
4
|
+
and transmitter-less DBC frames sit under ``bus.properties.messages``, while
|
|
5
|
+
transmitter-attributed DBC frames sit under ``component.facets.can.messages``.
|
|
6
|
+
Those are different JSON shapes describing the same wire, and every consumer
|
|
7
|
+
that reads them directly grows its own opinion about which fields exist.
|
|
8
|
+
|
|
9
|
+
These types are the single shape everything downstream works in. The catalogue
|
|
10
|
+
converts both storage forms into them once; the codec, the composer plugin, the
|
|
11
|
+
live capture, and the UI all read them and never touch the raw target again.
|
|
12
|
+
|
|
13
|
+
Nothing here validates a bit layout. That is the codec's job, because the
|
|
14
|
+
authority on whether a signal fits is ``cantools``, and duplicating its rules
|
|
15
|
+
here would produce a second answer to the same question.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
|
|
22
|
+
|
|
23
|
+
#: Frame ids wider than these cannot be put on a wire. Standard CAN carries an
|
|
24
|
+
#: 11-bit identifier and extended CAN a 29-bit one.
|
|
25
|
+
MAX_STANDARD_FRAME_ID = 0x7FF
|
|
26
|
+
MAX_EXTENDED_FRAME_ID = 0x1FFFFFFF
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SignalDefinition:
|
|
31
|
+
"""One field packed into a frame's payload.
|
|
32
|
+
|
|
33
|
+
Carries everything needed to rebuild a ``cantools`` signal and nothing that
|
|
34
|
+
only describes where it was stored. ``factor`` and ``offset`` convert raw
|
|
35
|
+
to physical: ``physical = raw * factor + offset``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
start_bit: int
|
|
40
|
+
length: int
|
|
41
|
+
#: ``"little"`` (Intel, DBC ``@1``) or ``"big"`` (Motorola, DBC ``@0``).
|
|
42
|
+
byte_order: str = "little"
|
|
43
|
+
signed: bool = False
|
|
44
|
+
factor: float = 1.0
|
|
45
|
+
offset: float = 0.0
|
|
46
|
+
minimum: Optional[float] = None
|
|
47
|
+
maximum: Optional[float] = None
|
|
48
|
+
unit: str = ""
|
|
49
|
+
#: ``"M"`` for the switch, ``"m3"`` or ``"m3,m4"`` for a branch signal.
|
|
50
|
+
multiplexer: Optional[str] = None
|
|
51
|
+
#: Which switch selects this branch signal. Only load-bearing when a frame
|
|
52
|
+
#: has more than one multiplexer, but stored always so it never has to be
|
|
53
|
+
#: inferred from position.
|
|
54
|
+
multiplexer_signal: Optional[str] = None
|
|
55
|
+
choices: Optional[Mapping[int, str]] = None
|
|
56
|
+
is_float: bool = False
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_multiplexer(self) -> bool:
|
|
60
|
+
"""Whether this signal selects which branch signals are present."""
|
|
61
|
+
return self.multiplexer == "M"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def multiplexer_ids(self) -> Tuple[int, ...]:
|
|
65
|
+
"""The switch values this signal is present for.
|
|
66
|
+
|
|
67
|
+
Empty for ordinary signals and for the switch itself. ARXML joins
|
|
68
|
+
several with commas because one signal can belong to more than one
|
|
69
|
+
branch; DBC only ever writes a single value.
|
|
70
|
+
"""
|
|
71
|
+
if not self.multiplexer or self.multiplexer == "M":
|
|
72
|
+
return ()
|
|
73
|
+
ids = []
|
|
74
|
+
for token in self.multiplexer.split(","):
|
|
75
|
+
token = token.strip()
|
|
76
|
+
if token.startswith("m") and token[1:].lstrip("-").isdigit():
|
|
77
|
+
ids.append(int(token[1:]))
|
|
78
|
+
return tuple(ids)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class FrameDefinition:
|
|
83
|
+
"""One frame on one bus, with where it came from attached.
|
|
84
|
+
|
|
85
|
+
``(bus_id, frame_id, is_extended)`` is the identity. A name is not: the
|
|
86
|
+
same name legitimately appears on two buses, and 0x123 standard and 0x123
|
|
87
|
+
extended are different frames sharing a wire.
|
|
88
|
+
|
|
89
|
+
``unsupported_reason`` is set rather than raised so that a frame the
|
|
90
|
+
composer cannot encode is still listable. The UI shows it as a disabled row
|
|
91
|
+
explaining itself, which is more useful than omitting it and leaving the
|
|
92
|
+
operator to wonder where it went.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
bus_id: str
|
|
96
|
+
frame_id: int
|
|
97
|
+
is_extended: bool
|
|
98
|
+
name: str
|
|
99
|
+
dlc: int
|
|
100
|
+
signals: Tuple[SignalDefinition, ...] = ()
|
|
101
|
+
is_fd: bool = False
|
|
102
|
+
senders: Tuple[str, ...] = ()
|
|
103
|
+
cycle_time_ms: Optional[int] = None
|
|
104
|
+
#: ``"bus"`` or ``"component"`` -- which storage location this came from.
|
|
105
|
+
owner_kind: str = "bus"
|
|
106
|
+
component_id: Optional[str] = None
|
|
107
|
+
component_name: Optional[str] = None
|
|
108
|
+
#: Index in the list it was read from, so a diagnostic can point at the row.
|
|
109
|
+
source_index: int = 0
|
|
110
|
+
#: AUTOSAR container payloads, kept only to detect and refuse one.
|
|
111
|
+
contained_messages: Tuple[Mapping[str, Any], ...] = ()
|
|
112
|
+
unsupported_reason: Optional[str] = None
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_supported(self) -> bool:
|
|
116
|
+
return self.unsupported_reason is None
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def frame_id_hex(self) -> str:
|
|
120
|
+
return f"0x{self.frame_id:X}"
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def multiplexer_signal_name(self) -> Optional[str]:
|
|
124
|
+
"""The name of this frame's switch signal, if it has one."""
|
|
125
|
+
for signal in self.signals:
|
|
126
|
+
if signal.is_multiplexer:
|
|
127
|
+
return signal.name
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def signal(self, name: str) -> Optional[SignalDefinition]:
|
|
131
|
+
for signal in self.signals:
|
|
132
|
+
if signal.name == name:
|
|
133
|
+
return signal
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def encoding_key(self) -> Tuple[Any, ...]:
|
|
137
|
+
"""Everything that changes the bytes on the wire, and nothing else.
|
|
138
|
+
|
|
139
|
+
Two rows describing one frame are duplicates when this matches. Where
|
|
140
|
+
they came from, who sends them, and how often deliberately do not
|
|
141
|
+
appear: two components declaring the same frame identically is
|
|
142
|
+
ordinary, and treating that as a conflict would make half the DBCs in
|
|
143
|
+
the world unusable.
|
|
144
|
+
"""
|
|
145
|
+
return (
|
|
146
|
+
self.frame_id,
|
|
147
|
+
self.is_extended,
|
|
148
|
+
self.is_fd,
|
|
149
|
+
self.name,
|
|
150
|
+
self.dlc,
|
|
151
|
+
tuple(
|
|
152
|
+
(
|
|
153
|
+
s.name,
|
|
154
|
+
s.start_bit,
|
|
155
|
+
s.length,
|
|
156
|
+
s.byte_order,
|
|
157
|
+
s.signed,
|
|
158
|
+
s.factor,
|
|
159
|
+
s.offset,
|
|
160
|
+
s.unit,
|
|
161
|
+
s.multiplexer,
|
|
162
|
+
s.multiplexer_signal,
|
|
163
|
+
s.is_float,
|
|
164
|
+
tuple(sorted((s.choices or {}).items())),
|
|
165
|
+
)
|
|
166
|
+
for s in self.signals
|
|
167
|
+
),
|
|
168
|
+
bool(self.contained_messages),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class BusDefinition:
|
|
174
|
+
"""A CAN bus and the frames resolved onto it."""
|
|
175
|
+
|
|
176
|
+
bus_id: str
|
|
177
|
+
name: str
|
|
178
|
+
frames: Tuple[FrameDefinition, ...] = ()
|
|
179
|
+
#: Identities that resolved to more than one incompatible definition.
|
|
180
|
+
#: Listed separately because they are findings about the target, not
|
|
181
|
+
#: frames anyone can use.
|
|
182
|
+
conflicts: Tuple[str, ...] = ()
|
|
183
|
+
|
|
184
|
+
def frame(self, frame_id: int, is_extended: bool) -> Optional[FrameDefinition]:
|
|
185
|
+
for candidate in self.frames:
|
|
186
|
+
if candidate.frame_id == frame_id and candidate.is_extended == is_extended:
|
|
187
|
+
return candidate
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class EncodedFrame:
|
|
193
|
+
"""The bytes a frame encodes to, with the flags needed to put them on a wire."""
|
|
194
|
+
|
|
195
|
+
frame_id: int
|
|
196
|
+
is_extended: bool
|
|
197
|
+
is_fd: bool
|
|
198
|
+
dlc: int
|
|
199
|
+
data: bytes
|
|
200
|
+
name: str
|
|
201
|
+
#: Values after normalization: choice labels resolved, numeric text parsed,
|
|
202
|
+
#: nothing echoed back as the operator typed it.
|
|
203
|
+
signals: Dict[str, Any] = field(default_factory=dict)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def data_hex(self) -> str:
|
|
207
|
+
return self.data.hex().upper()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class DecodedFrame:
|
|
212
|
+
"""The result of reading bytes against a definition.
|
|
213
|
+
|
|
214
|
+
Never raised, always returned, because the bytes come off a wire and a
|
|
215
|
+
capture that dies on one malformed frame is not a capture. ``ok`` says
|
|
216
|
+
which half of this object is meaningful.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
ok: bool
|
|
220
|
+
name: str
|
|
221
|
+
signals: Dict[str, Any] = field(default_factory=dict)
|
|
222
|
+
#: Raw integers behind any named choice, so a code missing from the value
|
|
223
|
+
#: table is reported as the number it was rather than lost.
|
|
224
|
+
raw_values: Dict[str, int] = field(default_factory=dict)
|
|
225
|
+
reason: Optional[str] = None
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def failed(cls, name: str, reason: str) -> "DecodedFrame":
|
|
229
|
+
return cls(ok=False, name=name, reason=reason)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def canonical_frame_id(frame_id: int, is_extended: bool = False) -> str:
|
|
233
|
+
"""The string form of a frame id, matching the CAN facet's own function.
|
|
234
|
+
|
|
235
|
+
Duplicated from ``iotsploit_django.tools.can_facet`` on purpose: this
|
|
236
|
+
package must not import Django, and an observation's ``subject_id`` has to
|
|
237
|
+
join across both. Uppercase hex, no ``0x``, zero-padded to the width of the
|
|
238
|
+
id space -- three digits standard, eight extended -- so a standard 0x123
|
|
239
|
+
and an extended 0x123 stay distinct keys.
|
|
240
|
+
"""
|
|
241
|
+
return f"{frame_id:0{8 if is_extended else 3}X}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def frame_id_is_valid(frame_id: int, is_extended: bool) -> bool:
|
|
245
|
+
"""Whether a frame id fits the identifier width it claims."""
|
|
246
|
+
if not isinstance(frame_id, int) or isinstance(frame_id, bool) or frame_id < 0:
|
|
247
|
+
return False
|
|
248
|
+
return frame_id <= (MAX_EXTENDED_FRAME_ID if is_extended else MAX_STANDARD_FRAME_ID)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def signals_by_name(signals: Sequence[SignalDefinition]) -> Dict[str, SignalDefinition]:
|
|
252
|
+
return {signal.name: signal for signal in signals}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Telling a bus fault apart from a frame, before anything reads an address.
|
|
2
|
+
|
|
3
|
+
A SocketCAN socket delivers bus faults as *error frames*, and python-can turns
|
|
4
|
+
them on by default (``CAN_RAW_ERR_FILTER``). They are not messages. The
|
|
5
|
+
``CAN_ERR_FLAG`` lives in the CAN ID, and python-can masks it off before it
|
|
6
|
+
exposes ``arbitration_id``, so what is left there is an error *class*, not an
|
|
7
|
+
address: ``CAN_ERR_CRTL`` arrives looking exactly like a frame with id ``0x004``
|
|
8
|
+
whose ``data[1]`` happens to be the controller status.
|
|
9
|
+
|
|
10
|
+
Anything that keys on ``(arbitration_id, is_extended)`` without testing
|
|
11
|
+
``is_error_frame`` first therefore invents a frame ``0x004`` no ECU ever sent
|
|
12
|
+
and counts bus faults as traffic. For a capture that writes observations the
|
|
13
|
+
consequence is worse than a wrong number on a screen: it records that phantom
|
|
14
|
+
frame as a documented-versus-observed finding against the target.
|
|
15
|
+
|
|
16
|
+
Why this exists twice
|
|
17
|
+
---------------------
|
|
18
|
+
``iotsploit_drivers.socketcan.can_errors`` holds the same constants, and the
|
|
19
|
+
duplication is deliberate rather than an oversight. ``iotsploit-drivers``
|
|
20
|
+
depends on ``iotsploit-core`` alone, and the dependency direction both CAN plans
|
|
21
|
+
fix is ``exploits -> protocols -> core``; importing the driver package from here
|
|
22
|
+
would add an edge neither plan sanctions, to reach nine integers out of
|
|
23
|
+
``linux/can/error.h`` that have not changed since 2007. The precedent is
|
|
24
|
+
``canonical_frame_id``, duplicated for the same reason.
|
|
25
|
+
|
|
26
|
+
The two are not redundant in what they produce. The driver formats a
|
|
27
|
+
``candump -e`` style description for a live stream; a capture needs a per-class
|
|
28
|
+
tally it can total over a window. They share the vocabulary, not the output.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from typing import Any, Dict, List, Mapping, Tuple
|
|
35
|
+
|
|
36
|
+
# Error class, carried in the masked arbitration id. linux/can/error.h.
|
|
37
|
+
CAN_ERR_TX_TIMEOUT = 0x001
|
|
38
|
+
CAN_ERR_LOSTARB = 0x002
|
|
39
|
+
CAN_ERR_CRTL = 0x004
|
|
40
|
+
CAN_ERR_PROT = 0x008
|
|
41
|
+
CAN_ERR_TRX = 0x010
|
|
42
|
+
CAN_ERR_ACK = 0x020
|
|
43
|
+
CAN_ERR_BUSOFF = 0x040
|
|
44
|
+
CAN_ERR_BUSERROR = 0x080
|
|
45
|
+
CAN_ERR_RESTARTED = 0x100
|
|
46
|
+
|
|
47
|
+
ERROR_CLASSES: Dict[int, str] = {
|
|
48
|
+
CAN_ERR_TX_TIMEOUT: "tx-timeout",
|
|
49
|
+
CAN_ERR_LOSTARB: "lost-arbitration",
|
|
50
|
+
CAN_ERR_CRTL: "controller-problem",
|
|
51
|
+
CAN_ERR_PROT: "protocol-violation",
|
|
52
|
+
CAN_ERR_TRX: "transceiver-status",
|
|
53
|
+
CAN_ERR_ACK: "no-acknowledgement-on-tx",
|
|
54
|
+
CAN_ERR_BUSOFF: "bus-off",
|
|
55
|
+
CAN_ERR_BUSERROR: "bus-error",
|
|
56
|
+
CAN_ERR_RESTARTED: "controller-restarted",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#: Controller status, ``data[1]``. These are the ones worth surfacing to an
|
|
60
|
+
#: operator: a bus sitting at error-passive is saying the bitrate is wrong, the
|
|
61
|
+
#: FD configuration is wrong, or the wiring is wrong -- which is the actual
|
|
62
|
+
#: explanation for a capture that "sees nothing".
|
|
63
|
+
CONTROLLER_STATUS: Dict[int, str] = {
|
|
64
|
+
0x01: "rx-overflow",
|
|
65
|
+
0x02: "tx-overflow",
|
|
66
|
+
0x04: "rx-error-warning",
|
|
67
|
+
0x08: "tx-error-warning",
|
|
68
|
+
0x10: "rx-error-passive",
|
|
69
|
+
0x20: "tx-error-passive",
|
|
70
|
+
0x40: "back-to-error-active",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#: Statuses that mean the link is degraded rather than merely noisy. A capture
|
|
74
|
+
#: whose data-frame count is low and whose faults are these must say so instead
|
|
75
|
+
#: of reporting an empty bus.
|
|
76
|
+
DEGRADED_STATUSES = frozenset(
|
|
77
|
+
{"rx-error-passive", "tx-error-passive", "rx-error-warning", "tx-error-warning"}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class ErrorFrame:
|
|
83
|
+
"""One classified fault, reduced to what a tally needs."""
|
|
84
|
+
|
|
85
|
+
classes: Tuple[str, ...]
|
|
86
|
+
controller_status: Tuple[str, ...] = ()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def labels(self) -> Tuple[str, ...]:
|
|
90
|
+
"""Every name this fault should be counted under."""
|
|
91
|
+
return self.classes + self.controller_status
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_degraded(self) -> bool:
|
|
95
|
+
return "bus-off" in self.classes or any(
|
|
96
|
+
status in DEGRADED_STATUSES for status in self.controller_status
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _flag_names(mapping: Mapping[int, str], value: int) -> List[str]:
|
|
101
|
+
return [name for bit, name in sorted(mapping.items()) if value & bit]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def classify_error_frame(arbitration_id: int, data: Any = None) -> ErrorFrame:
|
|
105
|
+
"""Read an error frame's class, and the controller status when it has one.
|
|
106
|
+
|
|
107
|
+
``arbitration_id`` is what python-can exposes after masking off
|
|
108
|
+
``CAN_ERR_FLAG``: a bitmask of error classes, never an address.
|
|
109
|
+
|
|
110
|
+
Never raises. A live capture is a long-running loop, so a message-shaped
|
|
111
|
+
object carrying something other than an ``int`` and ``bytes`` degrades to
|
|
112
|
+
``unknown-error-class`` rather than ending the capture.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
flags = int(arbitration_id or 0)
|
|
116
|
+
except (TypeError, ValueError):
|
|
117
|
+
flags = 0
|
|
118
|
+
|
|
119
|
+
classes = tuple(_flag_names(ERROR_CLASSES, flags))
|
|
120
|
+
if not classes:
|
|
121
|
+
classes = ("unknown-error-class",)
|
|
122
|
+
|
|
123
|
+
status: Tuple[str, ...] = ()
|
|
124
|
+
if flags & CAN_ERR_CRTL:
|
|
125
|
+
# An int is excluded rather than coerced: bytes(n) would allocate n
|
|
126
|
+
# zero bytes, which is both a wrong reading and a way to exhaust a
|
|
127
|
+
# long-running capture's memory from one malformed message.
|
|
128
|
+
try:
|
|
129
|
+
payload = b"" if isinstance(data, int) else bytes(data or b"")
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
payload = b""
|
|
132
|
+
if len(payload) > 1:
|
|
133
|
+
status = tuple(_flag_names(CONTROLLER_STATUS, payload[1]))
|
|
134
|
+
|
|
135
|
+
return ErrorFrame(classes=classes, controller_status=status)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_error_frame(message: Any) -> bool:
|
|
139
|
+
"""Whether this is a fault rather than traffic.
|
|
140
|
+
|
|
141
|
+
Read before identity, always. A message object that does not carry the
|
|
142
|
+
attribute is treated as traffic, which is the safe default for a scripted
|
|
143
|
+
frame source in a test.
|
|
144
|
+
"""
|
|
145
|
+
return bool(getattr(message, "is_error_frame", False))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_remote_frame(message: Any) -> bool:
|
|
149
|
+
"""Whether this is a remote request rather than a data frame.
|
|
150
|
+
|
|
151
|
+
Its own category: a remote frame carries no payload, and counting it as a
|
|
152
|
+
zero-length data frame would report a frame whose signals are all zero.
|
|
153
|
+
"""
|
|
154
|
+
return bool(getattr(message, "is_remote_frame", False))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Errors the CAN catalogue and codec raise.
|
|
2
|
+
|
|
3
|
+
Two types, split the way :mod:`iotsploit_protocols.errors` splits its three:
|
|
4
|
+
by what the caller can do about it.
|
|
5
|
+
|
|
6
|
+
``CanDefinitionError`` the target is wrong -- fix the ARXML, DBC, or facet.
|
|
7
|
+
``CanValueError`` the operator's input is wrong -- fix the value.
|
|
8
|
+
|
|
9
|
+
The difference is not cosmetic. A definition error means no value would have
|
|
10
|
+
worked and the form should say so once, at the top; a value error belongs
|
|
11
|
+
against the signal row that caused it, which is why it carries a field map.
|
|
12
|
+
|
|
13
|
+
Transport failures are deliberately absent. The house rule in
|
|
14
|
+
:mod:`iotsploit_protocols.errors` is that a connection failure stays
|
|
15
|
+
``OSError`` rather than being re-spelled. ``python-can`` does not follow it: a
|
|
16
|
+
SocketCAN interface that is down surfaces as ``can.CanOperationError``, which
|
|
17
|
+
is not an ``OSError``, so :mod:`iotsploit_protocols.canbus.socketcan` wraps it
|
|
18
|
+
in ``CanTransportError`` rather than let a caller's ``except OSError`` miss it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Dict, Optional
|
|
24
|
+
|
|
25
|
+
from iotsploit_protocols.errors import ProtocolError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CanDefinitionError(ProtocolError):
|
|
29
|
+
"""The target cannot describe the requested frame.
|
|
30
|
+
|
|
31
|
+
Covers absent, ambiguous, conflicting, and unsupported definitions. The
|
|
32
|
+
caller cannot fix any of them by changing a signal value, which is what
|
|
33
|
+
separates this from :class:`CanValueError`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CanValueError(ProtocolError):
|
|
38
|
+
"""A supplied signal value is missing, unknown, or out of range.
|
|
39
|
+
|
|
40
|
+
``field_errors`` maps a dotted path (``signals.VehicleSpeed``) to a message
|
|
41
|
+
about that one field, so an editor can put the text next to the input that
|
|
42
|
+
caused it instead of showing one opaque failure for the whole frame.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None) -> None:
|
|
46
|
+
self.field_errors: Dict[str, str] = dict(field_errors or {})
|
|
47
|
+
super().__init__(message)
|