hidpp/receiver/bolt/event.rs
1//! The notifications a Bolt receiver broadcasts, and their decoding.
2//!
3//! The receiver reports device arrivals, discovery results, and pairing
4//! progress as unsolicited HID++1.0 messages. Their layout is not publicly
5//! documented — it comes from reading other implementations (primarily Solaar)
6//! and from fuzzing registers — so every offset and mask here is pinned by a
7//! test rather than by a specification.
8
9use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
10
11use crate::{protocol::v10, receiver::RECEIVER_DEVICE_INDEX};
12
13/// The notification sub-ids this module decodes.
14///
15/// Modelling the sub-id as an enum rather than matching bare bytes makes the
16/// dispatch below exhaustive: a new notification cannot be added here without
17/// the compiler demanding a decode for it.
18#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
19#[repr(u8)]
20enum Notification {
21 /// A device connected to, or disconnected from, the receiver.
22 DeviceConnection = 0x41,
23
24 /// The receiver asks for a passkey to authenticate a device being paired.
25 PairingPasskeyRequest = 0x4d,
26
27 /// The user pressed a key while entering a pairing passkey.
28 PairingPasskeyPressed = 0x4e,
29
30 /// Details or the name of a device found while discovering.
31 DeviceDiscovery = 0x4f,
32
33 /// Device discovery was enabled or disabled.
34 DeviceDiscoveryStatus = 0x53,
35
36 /// A pairing attempt progressed, succeeded, or failed.
37 PairingStatus = 0x54,
38}
39
40/// The two payload kinds a [`Notification::DeviceDiscovery`] report carries,
41/// selected by `payload[2]`.
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
43#[repr(u8)]
44enum DiscoveryPart {
45 /// Address, kind, and product id of the discovered device.
46 Details = 0,
47
48 /// The discovered device's advertised name.
49 Name = 1,
50}
51
52/// Decodes an unsolicited receiver message into the event it carries.
53///
54/// Returns `None` for a report this crate does not model, one addressed
55/// elsewhere, or one whose payload does not parse — all of which the listener
56/// drops.
57///
58/// Kept separate from the message listener in [`super::Receiver::new`] so the
59/// wire layout is reachable from tests without a HID channel behind it.
60pub(super) fn decode(msg: &v10::Message) -> Option<Event> {
61 let header = msg.header();
62 let payload = msg.extend_payload();
63
64 let notification = Notification::try_from(header.sub_id).ok()?;
65
66 // Every notification but a device connection is addressed to the receiver
67 // itself. A connection notification instead carries the device's slot in
68 // the header — the only place that index is reported.
69 if notification != Notification::DeviceConnection
70 && header.device_index != RECEIVER_DEVICE_INDEX
71 {
72 return None;
73 }
74
75 match notification {
76 Notification::DeviceConnection => Some(Event::DeviceConnection(DeviceConnection {
77 index: header.device_index,
78 // Kind is identity-only; an unrecognised nibble folds to `Unknown`
79 // instead of dropping the event, which would hide the device
80 // entirely.
81 kind: DeviceKind::from(payload[1] & 0x0f),
82 encrypted: payload[1] & (1 << 5) != 0,
83 online: payload[1] & (1 << 6) == 0,
84 wpid: u16::from_le_bytes([payload[2], payload[3]]),
85 })),
86
87 Notification::DeviceDiscovery => match DiscoveryPart::try_from(payload[2]).ok()? {
88 DiscoveryPart::Details => Some(Event::DeviceDiscoveryDeviceDetails {
89 counter: discovery_counter(&payload),
90 kind: DeviceKind::from(payload[4] & 0x0f),
91 wpid: u16::from_le_bytes([payload[5], payload[6]]),
92 address: address6(&payload, 7),
93 authentication: payload[15],
94 }),
95 DiscoveryPart::Name => {
96 let name = discovery_name(&payload)?;
97 Some(Event::DeviceDiscoveryDeviceName {
98 counter: discovery_counter(&payload),
99 name: name.to_string(),
100 })
101 }
102 },
103
104 Notification::DeviceDiscoveryStatus => Some(Event::DeviceDiscoveryStatus {
105 discovery_enabled: payload[0] == 0x00,
106 }),
107
108 Notification::PairingStatus => Some(Event::PairingStatus {
109 device_address: address6(&payload, 2),
110 // `payload[0]` carries some further status this crate does not
111 // model. An unrecognised error code still means "pairing failed" —
112 // dropping it would turn the failure into a session timeout, so
113 // carry the raw code instead.
114 pairing_error: (payload[1] != 0x00).then(|| PairingError::from(payload[1])),
115 slot: (payload[8] != 0x00).then_some(payload[8]),
116 }),
117
118 Notification::PairingPasskeyRequest => Some(Event::PairingPasskeyRequest {
119 device_address: address6(&payload, 7),
120 passkey: passkey(&payload)?.to_string(),
121 }),
122
123 Notification::PairingPasskeyPressed => Some(Event::PairingPasskeyPressed {
124 device_address: address6(&payload, 1),
125 press_type: PairingPasskeyPressType::from(payload[0]),
126 }),
127 }
128}
129
130/// The little-endian counter that pairs a discovery details report with the
131/// name report describing the same device.
132fn discovery_counter(payload: &[u8; 17]) -> u16 {
133 u16::from_le_bytes([payload[0], payload[1]])
134}
135
136/// Extracts 6 contiguous bytes starting at `start` from a receiver-event
137/// payload into a BTLE device-address array.
138///
139/// Every call site above passes a compile-time-fixed `start` (1, 2, or 7)
140/// comfortably within the fixed 17-byte payload, so this never panics in
141/// practice.
142fn address6(payload: &[u8; 17], start: usize) -> [u8; 6] {
143 [
144 payload[start],
145 payload[start + 1],
146 payload[start + 2],
147 payload[start + 3],
148 payload[start + 4],
149 payload[start + 5],
150 ]
151}
152
153/// Reads the name out of a device-discovery name notification.
154///
155/// `payload[3]` is the device-reported name length. The byte comes straight
156/// off the radio, so it must never index past the report: a length that does
157/// not fit the packet (or non-UTF-8 bytes) drops the event instead of
158/// panicking the listener.
159fn discovery_name(payload: &[u8; 17]) -> Option<&str> {
160 let end = 4usize.checked_add(usize::from(payload[3]))?;
161 str::from_utf8(payload.get(4..end)?).ok()
162}
163
164/// Reads the passkey out of a passkey-request notification.
165///
166/// The passkey occupies 6 bytes and is NUL-padded when it is shorter.
167fn passkey(payload: &[u8; 17]) -> Option<&str> {
168 let digits = &payload[1..=6];
169 let len = digits.iter().position(|&b| b == 0).unwrap_or(digits.len());
170 str::from_utf8(&digits[..len]).ok()
171}
172
173/// Represents an event emitted by the receiver.
174///
175/// You can listen to these events using [`super::Receiver::listen`]. Only
176/// enabled notifications as indicated by
177/// [`super::Receiver::get_notification_state`] are emitted.
178#[derive(Clone, PartialEq, Eq, Hash, Debug)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize))]
180#[non_exhaustive]
181pub enum Event {
182 /// Is emitted whenever a device connects to or disconnects from the
183 /// receiver, but only if
184 /// [`NotificationState::wireless_notifications`](super::NotificationState::wireless_notifications)
185 /// is enabled.
186 ///
187 /// Can be triggered for all paired devices using
188 /// [`Receiver::trigger_device_arrival`](super::Receiver::trigger_device_arrival)
189 /// to allow easy device enumeration.
190 ///
191 /// [`Receiver::collect_paired_devices`](super::Receiver::collect_paired_devices)
192 /// implements a simple mechanism to collect all paired devices.
193 DeviceConnection(DeviceConnection),
194
195 /// Is emitted whenever the device discovery status changes.
196 DeviceDiscoveryStatus {
197 /// Whether discovery mode is enabled.
198 discovery_enabled: bool,
199 },
200
201 /// Is emitted many times for every device discovered using
202 /// [`Receiver::discover_devices`](super::Receiver::discover_devices).
203 ///
204 /// This event contains device details, including its address required to
205 /// start pairing. The [`Event::DeviceDiscoveryDeviceName`] event will also
206 /// be emitted and contains the device name.
207 DeviceDiscoveryDeviceDetails {
208 /// The incrementing event counter. This can be used to map
209 /// [`Event::DeviceDiscoveryDeviceDetails`] and
210 /// [`Event::DeviceDiscoveryDeviceName`] events.
211 counter: u16,
212
213 /// Device kind reported by discovery.
214 kind: DeviceKind,
215
216 /// Wireless product ID of the discovered device.
217 wpid: u16,
218
219 /// The address of the device required to pair it using
220 /// [`Receiver::pair_device`](super::Receiver::pair_device).
221 ///
222 /// This can also be used as the unique device identifier when
223 /// collecting discovered devices.
224 address: [u8; 6],
225
226 /// The authentication type(s) the device supports. Unfortunately, there
227 /// is not much information about this value and whether it is a
228 /// single value or a bitfield.
229 authentication: u8,
230 },
231
232 /// Is emitted many times for every device discovered using
233 /// [`Receiver::discover_devices`](super::Receiver::discover_devices).
234 ///
235 /// This event only contains the device name. Device details will be
236 /// provided using the [`Event::DeviceDiscoveryDeviceDetails`] event.
237 DeviceDiscoveryDeviceName {
238 /// The incrementing event counter. This can be used to map
239 /// [`Event::DeviceDiscoveryDeviceDetails`] and
240 /// [`Event::DeviceDiscoveryDeviceName`] events.
241 counter: u16,
242
243 /// Discovered device name.
244 name: String,
245 },
246
247 /// Is emitted whenever the status of a pairing process changes.
248 PairingStatus {
249 /// BTLE address of the device being paired.
250 device_address: [u8; 6],
251
252 /// Optional pairing error reported by the receiver.
253 pairing_error: Option<PairingError>,
254
255 /// The receiver slot the newly paired device was paired to. This can be
256 /// used as the device index for subsequent operations.
257 slot: Option<u8>,
258 },
259
260 /// Is emitted once the receiver requests a passkey to be entered on a
261 /// device that should be paired to it.
262 PairingPasskeyRequest {
263 /// BTLE address of the device being paired.
264 device_address: [u8; 6],
265
266 /// The passkey the user has to enter in order to pair the device.
267 ///
268 /// Depending on the device and authentication type, this value has
269 /// different implications.
270 ///
271 /// For mice, this value will be a valid 6-digit number. After parsing
272 /// this into an integer, the (least significant) bits represent
273 /// the sequence of mouse presses (`0` = left, `1` = right) the
274 /// user has to perform, with an additional press of both mouse
275 /// buttons simultaneously.
276 ///
277 /// The amount of bits significant to this equals to the `entropy`
278 /// passed to [`Receiver::pair_device`](super::Receiver::pair_device).
279 passkey: String,
280 },
281
282 /// Is emitted for every keypress a user performs while entering a pairing
283 /// passkey.
284 PairingPasskeyPressed {
285 /// BTLE address of the device being paired.
286 device_address: [u8; 6],
287
288 /// The type of the keypress the user performed.
289 ///
290 /// Every passkey sequence starts with an event where this value is set
291 /// to [`PairingPasskeyPressType::Initialization`]. Each time the user
292 /// presses a key, an event with a press type of
293 /// [`PairingPasskeyPressType::Keypress`] is emitted. Once the user
294 /// submits their passkey, this value will be
295 /// [`PairingPasskeyPressType::Submit`].
296 press_type: PairingPasskeyPressType,
297 },
298}
299
300/// Represents a device connected to a Bolt receiver.
301///
302/// This information is emitted by the [`Event::DeviceConnection`] event and can
303/// be conveniently collected using
304/// [`Receiver::collect_paired_devices`](super::Receiver::collect_paired_devices).
305#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
306#[cfg_attr(feature = "serde", derive(serde::Serialize))]
307#[non_exhaustive]
308pub struct DeviceConnection {
309 /// Slot index (1-based) of the device.
310 pub index: u8,
311
312 /// Device kind reported by the receiver.
313 pub kind: DeviceKind,
314
315 /// Whether the link is encrypted.
316 pub encrypted: bool,
317
318 /// Whether the device is currently online.
319 pub online: bool,
320
321 /// Wireless product ID of the device.
322 pub wpid: u16,
323}
324
325/// Represents the kind of a device paired to a Bolt receiver.
326#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, FromPrimitive)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize))]
328#[non_exhaustive]
329#[repr(u8)]
330pub enum DeviceKind {
331 /// Unknown device kind — also the fold target for values this crate
332 /// does not model (kind is identity-only and must never drop an event).
333 #[num_enum(default)]
334 Unknown = 0x00,
335 /// Keyboard device.
336 Keyboard = 0x01,
337 /// Mouse device.
338 Mouse = 0x02,
339 /// Numeric keypad device.
340 Numpad = 0x03,
341 /// Presenter device.
342 Presenter = 0x04,
343 /// Remote-control device.
344 Remote = 0x07,
345 /// Trackball device.
346 Trackball = 0x08,
347 /// Touchpad device.
348 Touchpad = 0x09,
349 /// Tablet device.
350 Tablet = 0x0a,
351 /// Gamepad device.
352 Gamepad = 0x0b,
353 /// Joystick device.
354 Joystick = 0x0c,
355 /// Headset device.
356 Headset = 0x0d,
357}
358
359/// Represents an error during device pairing.
360///
361/// This is reported by the [`Event::PairingStatus`] event.
362#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, FromPrimitive, IntoPrimitive)]
363#[cfg_attr(feature = "serde", derive(serde::Serialize))]
364#[non_exhaustive]
365#[repr(u8)]
366pub enum PairingError {
367 /// Device timed out during pairing.
368 DeviceTimeout = 0x01,
369 /// Pairing failed.
370 Failed = 0x02,
371 /// An error code this crate does not model; carries the raw byte.
372 #[num_enum(catch_all)]
373 Other(u8),
374}
375
376/// Represents the type of a single passkey press.
377///
378/// This is reported by the [`Event::PairingPasskeyPressed`] event, which also
379/// includes some further information about the context of these values.
380#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, FromPrimitive, IntoPrimitive)]
381#[cfg_attr(feature = "serde", derive(serde::Serialize))]
382#[non_exhaustive]
383#[repr(u8)]
384pub enum PairingPasskeyPressType {
385 /// Passkey entry has started.
386 Initialization = 0x00,
387 /// A passkey keypress was entered.
388 Keypress = 0x01,
389 /// Passkey entry was submitted.
390 Submit = 0x04,
391 /// A press type this crate does not model; carries the raw byte.
392 #[num_enum(catch_all)]
393 Other(u8),
394}
395
396#[cfg(test)]
397#[allow(
398 clippy::unwrap_used,
399 clippy::expect_used,
400 reason = "expect/unwrap are idiomatic in tests"
401)]
402mod tests {
403 use super::{
404 DeviceConnection, DeviceKind, Event, PairingError, PairingPasskeyPressType, decode,
405 discovery_name,
406 };
407 use crate::{
408 protocol::v10::{Message, MessageHeader},
409 receiver::RECEIVER_DEVICE_INDEX,
410 };
411
412 /// Builds the long notification the receiver broadcasts, with `payload`
413 /// laid out exactly as the 17 bytes following the header.
414 fn notification(device_index: u8, sub_id: u8, payload: [u8; 17]) -> Message {
415 Message::Long(
416 MessageHeader {
417 device_index,
418 sub_id,
419 },
420 payload,
421 )
422 }
423
424 /// A receiver-addressed notification.
425 fn from_receiver(sub_id: u8, payload: [u8; 17]) -> Message {
426 notification(RECEIVER_DEVICE_INDEX, sub_id, payload)
427 }
428
429 #[test]
430 fn device_connection_reads_slot_from_the_header_not_the_payload() {
431 // A connection notification is the only one addressed to the device's
432 // own slot rather than to the receiver, and that header byte is the
433 // only place the slot is reported.
434 let mut payload = [0u8; 17];
435 payload[1] = 0x02; // mouse, not encrypted, online
436 payload[2] = 0x0b;
437 payload[3] = 0x40;
438
439 let event = decode(¬ification(3, 0x41, payload)).unwrap();
440
441 assert_eq!(
442 event,
443 Event::DeviceConnection(DeviceConnection {
444 index: 3,
445 kind: DeviceKind::Mouse,
446 encrypted: false,
447 online: true,
448 wpid: 0x400b,
449 })
450 );
451 }
452
453 #[test]
454 fn device_connection_decodes_its_status_bits() {
455 // Bit 5 is the link encryption flag and bit 6 is *inverted*: it is set
456 // when the device is offline. Bolt puts encryption on a different bit
457 // than Unifying does (bit 4), so this is not a shared layout.
458 let connection = |status: u8| {
459 let mut payload = [0u8; 17];
460 payload[1] = status;
461 match decode(¬ification(1, 0x41, payload)) {
462 Some(Event::DeviceConnection(connection)) => connection,
463 other => panic!("expected a device connection, got {other:?}"),
464 }
465 };
466
467 let encrypted_online = connection(1 << 5);
468 assert!(encrypted_online.encrypted);
469 assert!(encrypted_online.online);
470
471 let plain_offline = connection(1 << 6);
472 assert!(!plain_offline.encrypted);
473 assert!(!plain_offline.online);
474 }
475
476 #[test]
477 fn unmodelled_device_kind_folds_to_unknown_instead_of_dropping_the_event() {
478 // Losing the event would hide the device from enumeration entirely,
479 // which is far worse than reporting an unknown kind.
480 let mut payload = [0u8; 17];
481 payload[1] = 0x0e;
482
483 let Some(Event::DeviceConnection(connection)) = decode(¬ification(1, 0x41, payload))
484 else {
485 panic!("an unknown kind must still produce an event");
486 };
487 assert_eq!(connection.kind, DeviceKind::Unknown);
488 }
489
490 #[test]
491 fn discovery_details_and_name_share_a_counter() {
492 // The counter is what lets a caller join the two halves of one
493 // discovered device, so both must read it from the same little-endian
494 // pair.
495 let mut details = [0u8; 17];
496 details[0] = 0x34;
497 details[1] = 0x12;
498 details[2] = 0; // details part
499 details[4] = 0x01; // keyboard
500 details[5] = 0xcd;
501 details[6] = 0xab;
502 details[7..13].copy_from_slice(&[1, 2, 3, 4, 5, 6]);
503 details[15] = 0x20;
504
505 assert_eq!(
506 decode(&from_receiver(0x4f, details)).unwrap(),
507 Event::DeviceDiscoveryDeviceDetails {
508 counter: 0x1234,
509 kind: DeviceKind::Keyboard,
510 wpid: 0xabcd,
511 address: [1, 2, 3, 4, 5, 6],
512 authentication: 0x20,
513 }
514 );
515
516 let mut name = [0u8; 17];
517 name[0] = 0x34;
518 name[1] = 0x12;
519 name[2] = 1; // name part
520 name[3] = 4;
521 name[4..8].copy_from_slice(b"Casa");
522
523 assert_eq!(
524 decode(&from_receiver(0x4f, name)).unwrap(),
525 Event::DeviceDiscoveryDeviceName {
526 counter: 0x1234,
527 name: "Casa".to_string(),
528 }
529 );
530 }
531
532 #[test]
533 fn unmodelled_discovery_part_is_dropped() {
534 let mut payload = [0u8; 17];
535 payload[2] = 9;
536
537 assert_eq!(decode(&from_receiver(0x4f, payload)), None);
538 }
539
540 #[test]
541 fn discovery_status_is_inverted_on_the_wire() {
542 let enabled = |byte: u8| {
543 let mut payload = [0u8; 17];
544 payload[0] = byte;
545 decode(&from_receiver(0x53, payload)).unwrap()
546 };
547
548 assert_eq!(
549 enabled(0x00),
550 Event::DeviceDiscoveryStatus {
551 discovery_enabled: true
552 }
553 );
554 assert_eq!(
555 enabled(0x01),
556 Event::DeviceDiscoveryStatus {
557 discovery_enabled: false
558 }
559 );
560 }
561
562 #[test]
563 fn pairing_status_carries_an_unmodelled_error_code_rather_than_dropping_it() {
564 // Dropping an unrecognised code would turn a reported failure into a
565 // silent session timeout.
566 let mut payload = [0u8; 17];
567 payload[1] = 0x7f;
568 payload[2..8].copy_from_slice(&[9, 8, 7, 6, 5, 4]);
569 payload[8] = 2;
570
571 assert_eq!(
572 decode(&from_receiver(0x54, payload)).unwrap(),
573 Event::PairingStatus {
574 device_address: [9, 8, 7, 6, 5, 4],
575 pairing_error: Some(PairingError::Other(0x7f)),
576 slot: Some(2),
577 }
578 );
579 }
580
581 #[test]
582 fn pairing_status_reports_success_as_no_error_and_slot_zero_as_none() {
583 let payload = [0u8; 17];
584
585 assert_eq!(
586 decode(&from_receiver(0x54, payload)).unwrap(),
587 Event::PairingStatus {
588 device_address: [0; 6],
589 pairing_error: None,
590 slot: None,
591 }
592 );
593 }
594
595 #[test]
596 fn passkey_request_stops_at_the_nul_padding() {
597 let mut payload = [0u8; 17];
598 payload[1..5].copy_from_slice(b"1234");
599 payload[7..13].copy_from_slice(&[0xaa; 6]);
600
601 assert_eq!(
602 decode(&from_receiver(0x4d, payload)).unwrap(),
603 Event::PairingPasskeyRequest {
604 device_address: [0xaa; 6],
605 passkey: "1234".to_string(),
606 }
607 );
608 }
609
610 #[test]
611 fn passkey_request_uses_all_six_digits_when_unpadded() {
612 let mut payload = [0u8; 17];
613 payload[1..7].copy_from_slice(b"951753");
614
615 let Some(Event::PairingPasskeyRequest { passkey, .. }) =
616 decode(&from_receiver(0x4d, payload))
617 else {
618 panic!("expected a passkey request");
619 };
620 assert_eq!(passkey, "951753");
621 }
622
623 #[test]
624 fn passkey_request_with_invalid_utf8_is_dropped() {
625 let mut payload = [0u8; 17];
626 payload[1] = 0xff;
627 payload[2] = 0xfe;
628
629 assert_eq!(decode(&from_receiver(0x4d, payload)), None);
630 }
631
632 #[test]
633 fn passkey_press_carries_an_unmodelled_press_type() {
634 let mut payload = [0u8; 17];
635 payload[0] = 0x33;
636 payload[1..7].copy_from_slice(&[1, 2, 3, 4, 5, 6]);
637
638 assert_eq!(
639 decode(&from_receiver(0x4e, payload)).unwrap(),
640 Event::PairingPasskeyPressed {
641 device_address: [1, 2, 3, 4, 5, 6],
642 press_type: PairingPasskeyPressType::Other(0x33),
643 }
644 );
645 }
646
647 #[test]
648 fn notifications_addressed_elsewhere_are_dropped_except_device_connections() {
649 // A device-addressed report is only ever a connection notification;
650 // anything else at a device index belongs to that device, not to us.
651 assert_eq!(decode(¬ification(2, 0x53, [0u8; 17])), None);
652 assert!(decode(¬ification(2, 0x41, [0u8; 17])).is_some());
653 }
654
655 #[test]
656 fn unmodelled_sub_id_is_dropped() {
657 assert_eq!(decode(&from_receiver(0x42, [0u8; 17])), None);
658 }
659
660 #[test]
661 fn short_notifications_decode_from_the_zero_padded_payload() {
662 // The receiver may answer in either report width; a short report is
663 // widened with zeroes, which must not change the decoded event.
664 let short = Message::Short(
665 MessageHeader {
666 device_index: 4,
667 sub_id: 0x41,
668 },
669 [0x00, 0x02, 0x0b, 0x40],
670 );
671
672 assert_eq!(
673 decode(&short).unwrap(),
674 Event::DeviceConnection(DeviceConnection {
675 index: 4,
676 kind: DeviceKind::Mouse,
677 encrypted: false,
678 online: true,
679 wpid: 0x400b,
680 })
681 );
682 }
683
684 #[test]
685 fn discovery_name_with_oversized_length_is_dropped() {
686 let mut payload = [0u8; 17];
687 payload[3] = 200;
688
689 assert_eq!(discovery_name(&payload), None);
690 }
691
692 #[test]
693 fn discovery_name_within_bounds_parses() {
694 let mut payload = [0u8; 17];
695 payload[3] = 4;
696 payload[4..8].copy_from_slice(b"Casa");
697
698 assert_eq!(discovery_name(&payload), Some("Casa"));
699 }
700
701 #[test]
702 fn discovery_name_rejects_invalid_utf8() {
703 let mut payload = [0u8; 17];
704 payload[3] = 2;
705 payload[4] = 0xff;
706 payload[5] = 0xfe;
707
708 assert_eq!(discovery_name(&payload), None);
709 }
710}