Coverage for src / bluetooth_sig / advertising / exceptions.py: 100%
49 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Advertising exceptions for the Bluetooth SIG library.
3Provides exception types for advertising-related errors, following the same
4patterns as GATT exceptions for API consistency.
6Exception Hierarchy:
7 AdvertisingError (base)
8 ├── AdvertisingParseError - General parse failures
9 │ ├── EncryptionRequiredError - Payload encrypted, no bindkey
10 │ ├── DecryptionFailedError - Decryption failed
11 │ └── UnsupportedVersionError - Unknown protocol version
12 ├── ReplayDetectedError - Counter not increasing
13 └── DuplicatePacketError - Same packet_id as previous
14"""
16from __future__ import annotations
18from bluetooth_sig.gatt.exceptions import BluetoothSIGError
21class AdvertisingError(BluetoothSIGError):
22 """Base exception for all advertising-related errors."""
25class AdvertisingParseError(AdvertisingError):
26 """Exception raised when advertising payload parsing fails.
28 Attributes:
29 message: Human-readable error message.
30 raw_data: The raw advertising data that failed to parse.
31 interpreter_name: Name of the interpreter that raised the error.
32 field: Specific field that caused the error (if applicable).
34 """
36 def __init__(
37 self,
38 message: str,
39 raw_data: bytes = b"",
40 interpreter_name: str = "",
41 field: str | None = None,
42 ) -> None:
43 """Initialise AdvertisingParseError.
45 Args:
46 message: Human-readable error message.
47 raw_data: The raw advertising data that failed to parse.
48 interpreter_name: Name of the interpreter that raised the error.
49 field: Specific field that caused the error (if applicable).
51 """
52 self.raw_data = raw_data
53 self.interpreter_name = interpreter_name
54 self.field = field
56 # Build detailed message
57 parts = [message]
58 if interpreter_name:
59 parts.insert(0, f"[{interpreter_name}]")
60 if field:
61 parts.append(f"(field: {field})")
62 if raw_data:
63 max_hex_bytes = 32
64 hex_data = raw_data.hex() if len(raw_data) <= max_hex_bytes else f"{raw_data[:max_hex_bytes].hex()}..."
65 parts.append(f"[data: {hex_data}]")
67 super().__init__(" ".join(parts))
70class EncryptionRequiredError(AdvertisingParseError):
71 """Exception raised when payload is encrypted but no bindkey is available.
73 This exception indicates the payload contains encrypted data that
74 requires a bindkey for decryption. The caller should:
75 1. Prompt the user to provide a bindkey
76 2. Store the bindkey in DeviceAdvertisingState.encryption.bindkey
77 3. Retry interpretation
79 Attributes:
80 mac_address: Device MAC address needing a bindkey.
82 """
84 def __init__(
85 self,
86 mac_address: str,
87 raw_data: bytes = b"",
88 interpreter_name: str = "",
89 ) -> None:
90 """Initialise EncryptionRequiredError.
92 Args:
93 mac_address: Device MAC address needing a bindkey.
94 raw_data: The raw encrypted advertising data.
95 interpreter_name: Name of the interpreter that raised the error.
97 """
98 self.mac_address = mac_address
99 message = f"Encryption required for device {mac_address}"
100 super().__init__(
101 message=message,
102 raw_data=raw_data,
103 interpreter_name=interpreter_name,
104 )
107class DecryptionFailedError(AdvertisingParseError):
108 """Exception raised when decryption fails.
110 This typically indicates:
111 - Wrong bindkey
112 - Corrupted data
113 - Incorrect nonce construction
115 Attributes:
116 mac_address: Device MAC address.
117 reason: Specific reason for decryption failure.
119 """
121 def __init__(
122 self,
123 mac_address: str,
124 reason: str = "decryption failed",
125 raw_data: bytes = b"",
126 interpreter_name: str = "",
127 ) -> None:
128 """Initialise DecryptionFailedError.
130 Args:
131 mac_address: Device MAC address.
132 reason: Specific reason for decryption failure.
133 raw_data: The raw encrypted advertising data.
134 interpreter_name: Name of the interpreter that raised the error.
136 """
137 self.mac_address = mac_address
138 self.reason = reason
139 message = f"Decryption failed for device {mac_address}: {reason}"
140 super().__init__(
141 message=message,
142 raw_data=raw_data,
143 interpreter_name=interpreter_name,
144 )
147class UnsupportedVersionError(AdvertisingParseError):
148 """Exception raised when protocol version is not supported.
150 Attributes:
151 version: The unsupported version identifier.
152 supported_versions: List of supported version identifiers.
154 """
156 def __init__(
157 self,
158 version: str | int,
159 supported_versions: list[str | int] | None = None,
160 raw_data: bytes = b"",
161 interpreter_name: str = "",
162 ) -> None:
163 """Initialise UnsupportedVersionError.
165 Args:
166 version: The unsupported version identifier.
167 supported_versions: List of supported version identifiers.
168 raw_data: The raw advertising data.
169 interpreter_name: Name of the interpreter that raised the error.
171 """
172 self.version = version
173 self.supported_versions = supported_versions or []
174 supported_str = ", ".join(str(v) for v in self.supported_versions) if self.supported_versions else "unknown"
175 message = f"Unsupported protocol version {version} (supported: {supported_str})"
176 super().__init__(
177 message=message,
178 raw_data=raw_data,
179 interpreter_name=interpreter_name,
180 )
183class ReplayDetectedError(AdvertisingError):
184 """Exception raised when a replay attack is detected.
186 This occurs when the encryption counter is not increasing,
187 indicating a potential replay attack.
189 Note: Per Bluetooth Core Specification, replay protection is typically
190 handled at Controller/Link Layer level. This exception is provided
191 for vendor protocols that implement their own replay detection.
193 Attributes:
194 mac_address: Device MAC address.
195 received_counter: Counter value received in the packet.
196 expected_counter: Minimum expected counter value.
198 """
200 def __init__(
201 self,
202 mac_address: str,
203 received_counter: int,
204 expected_counter: int,
205 ) -> None:
206 """Initialise ReplayDetectedError.
208 Args:
209 mac_address: Device MAC address.
210 received_counter: Counter value received in the packet.
211 expected_counter: Minimum expected counter value.
213 """
214 self.mac_address = mac_address
215 self.received_counter = received_counter
216 self.expected_counter = expected_counter
217 message = (
218 f"Replay detected for device {mac_address}: "
219 f"received counter {received_counter}, expected >= {expected_counter}"
220 )
221 super().__init__(message)
224class DuplicatePacketError(AdvertisingError):
225 """Exception raised when a duplicate packet is detected.
227 This occurs when the same packet_id is received twice, indicating
228 the same advertisement was received multiple times. This is typically
229 not an error but may be useful for deduplication.
231 Attributes:
232 mac_address: Device MAC address.
233 packet_id: The duplicate packet ID.
235 """
237 def __init__(
238 self,
239 mac_address: str,
240 packet_id: int,
241 ) -> None:
242 """Initialise DuplicatePacketError.
244 Args:
245 mac_address: Device MAC address.
246 packet_id: The duplicate packet ID.
248 """
249 self.mac_address = mac_address
250 self.packet_id = packet_id
251 message = f"Duplicate packet {packet_id} from device {mac_address}"
252 super().__init__(message)