umsh_core/
packet.rs

1use core::ops::Range;
2
3use crate::{EncodeError, ParseError, options::OptionDecoder};
4
5/// Current UMSH packet version encoded in the FCF high bits.
6pub const UMSH_VERSION: u8 = 0b11;
7
8/// Packet class encoded in the frame-control field.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10#[repr(u8)]
11pub enum PacketType {
12    Broadcast = 0,
13    MacAck = 1,
14    Unicast = 2,
15    UnicastAckReq = 3,
16    Multicast = 4,
17    Reserved5 = 5,
18    BlindUnicast = 6,
19    BlindUnicastAckReq = 7,
20}
21
22impl PacketType {
23    /// Decode a packet type from the three packet-type bits in the FCF.
24    pub const fn from_bits(value: u8) -> Self {
25        match value & 0x07 {
26            0 => Self::Broadcast,
27            1 => Self::MacAck,
28            2 => Self::Unicast,
29            3 => Self::UnicastAckReq,
30            4 => Self::Multicast,
31            5 => Self::Reserved5,
32            6 => Self::BlindUnicast,
33            _ => Self::BlindUnicastAckReq,
34        }
35    }
36
37    /// Return whether packets of this type carry SECINFO and a MIC.
38    pub fn is_secure(self) -> bool {
39        matches!(
40            self,
41            Self::Unicast
42                | Self::UnicastAckReq
43                | Self::Multicast
44                | Self::BlindUnicast
45                | Self::BlindUnicastAckReq
46        )
47    }
48
49    /// Return whether this packet type requests a MAC ACK.
50    pub fn ack_requested(self) -> bool {
51        matches!(self, Self::UnicastAckReq | Self::BlindUnicastAckReq)
52    }
53
54    /// Return whether this packet type participates in mesh routing/forwarding.
55    pub fn is_routable(self) -> bool {
56        !matches!(self, Self::Reserved5)
57    }
58}
59
60/// Application payload type carried inside the MAC body.
61///
62/// `Empty` is a special out-of-band value used when the frame carries no
63/// application payload bytes at all, meaning there is no payload-type byte on
64/// the wire. All other variants correspond to the leading typed-payload byte.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66#[repr(u8)]
67pub enum PayloadType {
68    /// No application payload and no payload-type byte on the wire.
69    Empty = 0xFF,
70    /// Explicit application-agnostic payload type byte `0`.
71    Unspecified = 0,
72    /// Node-identity payload.
73    NodeIdentity = 1,
74    /// MAC command payload.
75    MacCommand = 2,
76    /// Text-message payload.
77    TextMessage = 3,
78    /// Chat-room management payload.
79    ChatRoomMessage = 5,
80    /// CoAP-over-UMSH payload.
81    CoapOverUmsh = 7,
82    /// Node-management payload.
83    NodeManagement = 8,
84}
85
86impl PayloadType {
87    /// Convert a raw payload-type byte into a known payload type.
88    pub fn from_byte(byte: u8) -> Option<Self> {
89        match byte {
90            0 => Some(Self::Unspecified),
91            1 => Some(Self::NodeIdentity),
92            2 => Some(Self::MacCommand),
93            3 => Some(Self::TextMessage),
94            5 => Some(Self::ChatRoomMessage),
95            7 => Some(Self::CoapOverUmsh),
96            8 => Some(Self::NodeManagement),
97            _ => None,
98        }
99    }
100
101    /// Return whether this payload type is valid for the given MAC packet type.
102    pub fn allowed_for(self, packet_type: PacketType) -> bool {
103        match self {
104            Self::Empty | Self::Unspecified | Self::NodeIdentity => {
105                !matches!(packet_type, PacketType::MacAck)
106            }
107            Self::MacCommand => matches!(
108                packet_type,
109                PacketType::Unicast
110                    | PacketType::UnicastAckReq
111                    | PacketType::BlindUnicast
112                    | PacketType::BlindUnicastAckReq
113                    | PacketType::Multicast
114            ),
115            Self::TextMessage | Self::CoapOverUmsh | Self::NodeManagement => matches!(
116                packet_type,
117                PacketType::Unicast
118                    | PacketType::UnicastAckReq
119                    | PacketType::BlindUnicast
120                    | PacketType::BlindUnicastAckReq
121                    | PacketType::Multicast
122            ),
123            Self::ChatRoomMessage => matches!(
124                packet_type,
125                PacketType::Unicast
126                    | PacketType::UnicastAckReq
127                    | PacketType::BlindUnicast
128                    | PacketType::BlindUnicastAckReq
129            ),
130        }
131    }
132}
133
134/// Frame-control field wrapper.
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub struct Fcf(pub u8);
137
138impl Fcf {
139    /// Build an FCF from structured flags.
140    pub const fn new(
141        packet_type: PacketType,
142        full_source: bool,
143        flood_hops_present: bool,
144    ) -> Self {
145        Self(
146            (UMSH_VERSION << 6)
147                | ((packet_type as u8) << 3)
148                | ((full_source as u8) << 2)
149                | flood_hops_present as u8,
150        )
151    }
152
153    /// Return the encoded protocol version.
154    pub const fn version(self) -> u8 {
155        self.0 >> 6
156    }
157
158    /// Return the encoded packet type.
159    pub const fn packet_type(self) -> PacketType {
160        PacketType::from_bits((self.0 >> 3) & 0x07)
161    }
162
163    /// Return whether the source address is the full 32-byte public key.
164    pub const fn full_source(self) -> bool {
165        self.0 & 0x04 != 0
166    }
167
168    /// Return whether the reserved bit is clear as required by the spec.
169    pub const fn reserved_valid(self) -> bool {
170        self.0 & 0x02 == 0
171    }
172
173    /// Return whether a flood-hop byte is present.
174    pub const fn flood_hops_present(self) -> bool {
175        self.0 & 0x01 != 0
176    }
177}
178
179/// Security-control field wrapper.
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub struct Scf(pub u8);
182
183impl Scf {
184    /// Build an SCF from structured flags.
185    pub const fn new(encrypted: bool, mic_size: MicSize, salt_present: bool) -> Self {
186        Self(((encrypted as u8) << 7) | ((mic_size as u8) << 5) | ((salt_present as u8) << 4))
187    }
188
189    /// Return whether the body is encrypted in place.
190    pub const fn encrypted(self) -> bool {
191        self.0 & 0x80 != 0
192    }
193
194    /// Decode the configured MIC size.
195    pub fn mic_size(self) -> Result<MicSize, ParseError> {
196        MicSize::from_bits((self.0 >> 5) & 0x03)
197    }
198
199    /// Return whether a salt field follows the frame counter.
200    pub const fn salt_present(self) -> bool {
201        self.0 & 0x10 != 0
202    }
203
204    /// Return whether the reserved low nibble is valid for the current spec.
205    pub const fn reserved_valid(self) -> bool {
206        self.0 & 0x0F == 0
207    }
208}
209
210/// Authenticator size carried by secured packets.
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum MicSize {
213    Mic4 = 0,
214    Mic8 = 1,
215    Mic12 = 2,
216    Mic16 = 3,
217}
218
219impl MicSize {
220    /// Return the on-wire byte length of this MIC size.
221    pub const fn byte_len(self) -> usize {
222        match self {
223            Self::Mic4 => 4,
224            Self::Mic8 => 8,
225            Self::Mic12 => 12,
226            Self::Mic16 => 16,
227        }
228    }
229
230    /// Decode a MIC size from its two SCF bits.
231    pub fn from_bits(value: u8) -> Result<Self, ParseError> {
232        match value {
233            0 => Ok(Self::Mic4),
234            1 => Ok(Self::Mic8),
235            2 => Ok(Self::Mic12),
236            3 => Ok(Self::Mic16),
237            other => Err(ParseError::InvalidMicSize(other)),
238        }
239    }
240}
241
242/// Combined remaining/accumulated flood-hop counters.
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub struct FloodHops(pub u8);
245
246impl FloodHops {
247    /// Construct a flood-hop value if both nibbles fit in four bits.
248    pub fn new(remaining: u8, accumulated: u8) -> Option<Self> {
249        if remaining <= 0x0F && accumulated <= 0x0F {
250            Some(Self((remaining << 4) | accumulated))
251        } else {
252            None
253        }
254    }
255
256    /// Remaining forward-hop budget.
257    pub const fn remaining(self) -> u8 {
258        self.0 >> 4
259    }
260
261    /// Number of hops already consumed.
262    pub const fn accumulated(self) -> u8 {
263        self.0 & 0x0F
264    }
265
266    /// Return the next forwarded hop count.
267    pub fn decremented(self) -> Self {
268        let remaining = self.remaining();
269        if remaining == 0 {
270            self
271        } else {
272            Self::new(remaining - 1, self.accumulated().saturating_add(1)).unwrap_or(self)
273        }
274    }
275}
276
277/// Three-byte node hint derived from a public key.
278#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
279pub struct NodeHint(pub [u8; 3]);
280
281impl NodeHint {
282    /// Derive the hint from the first three public-key bytes.
283    pub fn from_public_key(key: &PublicKey) -> Self {
284        Self([key.0[0], key.0[1], key.0[2]])
285    }
286}
287
288/// Two-byte router hint used in learned routes.
289#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
290pub struct RouterHint(pub [u8; 2]);
291
292impl RouterHint {
293    /// Derive the router hint from the first two public-key bytes.
294    pub fn from_public_key(key: &PublicKey) -> Self {
295        Self([key.0[0], key.0[1]])
296    }
297}
298
299/// Two-byte multicast channel identifier.
300#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
301pub struct ChannelId(pub [u8; 2]);
302
303/// Node public key, which also acts as the full network address.
304#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
305pub struct PublicKey(pub [u8; 32]);
306
307impl PublicKey {
308    /// Return the node hint associated with this key.
309    pub fn hint(&self) -> NodeHint {
310        NodeHint::from_public_key(self)
311    }
312
313    /// Return the router hint associated with this key.
314    pub fn router_hint(&self) -> RouterHint {
315        RouterHint::from_public_key(self)
316    }
317}
318
319/// Raw 32-byte multicast channel secret.
320#[derive(Clone, Copy, zeroize::Zeroize)]
321pub struct ChannelKey(pub [u8; 32]);
322
323impl PartialEq for ChannelKey {
324    fn eq(&self, other: &Self) -> bool {
325        self.0 == other.0
326    }
327}
328
329impl Eq for ChannelKey {}
330
331impl core::fmt::Debug for ChannelKey {
332    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
333        f.write_str("ChannelKey([redacted])")
334    }
335}
336
337/// Source address supplied while constructing packets.
338#[derive(Clone, Copy, Debug, PartialEq, Eq)]
339pub enum SourceAddr<'a> {
340    Hint(NodeHint),
341    Full(&'a PublicKey),
342}
343
344impl SourceAddr<'_> {
345    /// Return the hint form of this address.
346    pub fn hint(&self) -> NodeHint {
347        match self {
348            Self::Hint(hint) => *hint,
349            Self::Full(key) => key.hint(),
350        }
351    }
352}
353
354/// Decoded SECINFO structure.
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub struct SecInfo {
357    pub scf: Scf,
358    pub frame_counter: u32,
359    pub salt: Option<u16>,
360}
361
362impl SecInfo {
363    /// Return the SECINFO on-wire length.
364    pub fn wire_len(&self) -> usize {
365        if self.salt.is_some() { 7 } else { 5 }
366    }
367
368    /// Encode SECINFO into `buf` and return the number of bytes written.
369    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
370        let needed = self.wire_len();
371        if buf.len() < needed {
372            return Err(EncodeError::BufferTooSmall);
373        }
374        buf[0] = self.scf.0;
375        buf[1..5].copy_from_slice(&self.frame_counter.to_be_bytes());
376        if let Some(salt) = self.salt {
377            buf[5..7].copy_from_slice(&salt.to_be_bytes());
378        }
379        Ok(needed)
380    }
381
382    /// Decode SECINFO from the start of `buf`.
383    pub fn decode(buf: &[u8]) -> Result<Self, ParseError> {
384        if buf.len() < 5 {
385            return Err(ParseError::Truncated);
386        }
387        let scf = Scf(buf[0]);
388        if !scf.reserved_valid() {
389            return Err(ParseError::InvalidScfReserved);
390        }
391        let salt = if scf.salt_present() {
392            if buf.len() < 7 {
393                return Err(ParseError::Truncated);
394            }
395            Some(u16::from_be_bytes([buf[5], buf[6]]))
396        } else {
397            None
398        };
399        Ok(Self {
400            scf,
401            frame_counter: u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]),
402            salt,
403        })
404    }
405}
406
407/// Known packet-option numbers.
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum OptionNumber {
410    RegionCode,
411    TraceRoute,
412    SourceRoute,
413    OperatorCallsign,
414    MinRssi,
415    RouteRetry,
416    StationCallsign,
417    MinSnr,
418    Unknown(u16),
419}
420
421impl OptionNumber {
422    /// Return the numeric option number used on the wire.
423    pub fn as_u16(self) -> u16 {
424        match self {
425            Self::RegionCode => 11,
426            Self::TraceRoute => 2,
427            Self::SourceRoute => 3,
428            Self::OperatorCallsign => 4,
429            Self::MinRssi => 5,
430            Self::RouteRetry => 6,
431            Self::StationCallsign => 7,
432            Self::MinSnr => 9,
433            Self::Unknown(value) => value,
434        }
435    }
436
437    /// Return whether the option is critical when unknown.
438    pub fn is_critical(self) -> bool {
439        self.as_u16() & 1 != 0
440    }
441
442    /// Return whether the option is considered dynamic for AAD purposes.
443    pub fn is_dynamic(self) -> bool {
444        self.as_u16() & 2 != 0
445    }
446}
447
448impl From<u16> for OptionNumber {
449    fn from(value: u16) -> Self {
450        match value {
451            2 => Self::TraceRoute,
452            3 => Self::SourceRoute,
453            4 => Self::OperatorCallsign,
454            5 => Self::MinRssi,
455            6 => Self::RouteRetry,
456            7 => Self::StationCallsign,
457            9 => Self::MinSnr,
458            11 => Self::RegionCode,
459            other => Self::Unknown(other),
460        }
461    }
462}
463
464/// Parsed source-address location used by zero-copy packet processing.
465#[derive(Clone, Copy, Debug, PartialEq, Eq)]
466pub enum SourceAddrRef {
467    Hint(NodeHint),
468    FullKeyAt { offset: usize },
469    Encrypted { offset: usize, len: usize },
470    None,
471}
472
473/// Parsed packet header with borrowed ranges into the original frame.
474#[derive(Clone, Debug, PartialEq, Eq)]
475pub struct PacketHeader {
476    pub fcf: Fcf,
477    pub options_range: Range<usize>,
478    pub flood_hops: Option<FloodHops>,
479    pub dst: Option<NodeHint>,
480    pub channel: Option<ChannelId>,
481    pub ack_dst: Option<NodeHint>,
482    pub source: SourceAddrRef,
483    pub sec_info: Option<SecInfo>,
484    pub body_range: Range<usize>,
485    pub mic_range: Range<usize>,
486    pub total_len: usize,
487}
488
489impl PacketHeader {
490    /// Parse a complete on-wire packet header and compute payload/MIC ranges.
491    pub fn parse(buf: &[u8]) -> Result<Self, ParseError> {
492        if buf.is_empty() {
493            return Err(ParseError::Truncated);
494        }
495
496        let fcf = Fcf(buf[0]);
497        if fcf.version() != UMSH_VERSION {
498            return Err(ParseError::InvalidVersion(fcf.version()));
499        }
500        if !fcf.reserved_valid() {
501            return Err(ParseError::InvalidFcfReserved);
502        }
503
504        let mut cursor = 1;
505        let flood_hops = if fcf.flood_hops_present() {
506            if cursor >= buf.len() {
507                return Err(ParseError::Truncated);
508            }
509            let fh = FloodHops(buf[cursor]);
510            cursor += 1;
511            Some(fh)
512        } else {
513            None
514        };
515
516        let packet_type = fcf.packet_type();
517        let mut dst = None;
518        let mut channel = None;
519        let mut ack_dst = None;
520        let mut source = SourceAddrRef::None;
521        let mut sec_info = None;
522
523        match packet_type {
524            PacketType::Broadcast => {
525                let src_len = source_len(fcf.full_source());
526                source = if fcf.full_source() {
527                    ensure_len(buf, cursor, 32)?;
528                    SourceAddrRef::FullKeyAt { offset: cursor }
529                } else {
530                    ensure_len(buf, cursor, 3)?;
531                    SourceAddrRef::Hint(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]))
532                };
533                cursor += src_len;
534                let options_start = cursor;
535                let options_end = buf.len();
536                let (consumed, has_marker) =
537                    scan_options_bounded(&buf[options_start..options_end])?;
538                let options_range = options_start..options_start + consumed;
539                let body_start = options_start + consumed;
540                let body_end = if has_marker { buf.len() } else { body_start };
541                Ok(Self {
542                    fcf,
543                    options_range,
544                    flood_hops,
545                    dst,
546                    channel,
547                    ack_dst,
548                    source,
549                    sec_info,
550                    body_range: body_start..body_end,
551                    mic_range: buf.len()..buf.len(),
552                    total_len: buf.len(),
553                })
554            }
555            PacketType::MacAck => {
556                ensure_len(buf, cursor, 3)?;
557                ack_dst = Some(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]));
558                cursor += 3;
559                let options_start = cursor;
560                let options_end = buf
561                    .len()
562                    .checked_sub(8)
563                    .ok_or(ParseError::Truncated)?;
564                if options_end < options_start {
565                    return Err(ParseError::Truncated);
566                }
567                let (consumed, _has_marker) =
568                    scan_options_bounded(&buf[options_start..options_end])?;
569                let options_range = options_start..options_start + consumed;
570                let ack_tag_start = options_end;
571                Ok(Self {
572                    fcf,
573                    options_range,
574                    flood_hops,
575                    dst,
576                    channel,
577                    ack_dst,
578                    source,
579                    sec_info,
580                    body_range: ack_tag_start..ack_tag_start + 8,
581                    mic_range: ack_tag_start..ack_tag_start + 8,
582                    total_len: ack_tag_start + 8,
583                })
584            }
585            PacketType::Unicast | PacketType::UnicastAckReq => {
586                ensure_len(buf, cursor, 3)?;
587                dst = Some(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]));
588                cursor += 3;
589                let src_len = source_len(fcf.full_source());
590                source = if fcf.full_source() {
591                    ensure_len(buf, cursor, 32)?;
592                    SourceAddrRef::FullKeyAt { offset: cursor }
593                } else {
594                    ensure_len(buf, cursor, 3)?;
595                    SourceAddrRef::Hint(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]))
596                };
597                cursor += src_len;
598                let parsed_sec = SecInfo::decode(&buf[cursor..])?;
599                let sec_len = parsed_sec.wire_len();
600                sec_info = Some(parsed_sec);
601                cursor += sec_len;
602                let mic_len = parsed_sec.scf.mic_size()?.byte_len();
603                let mic_start = buf
604                    .len()
605                    .checked_sub(mic_len)
606                    .ok_or(ParseError::Truncated)?;
607                if mic_start < cursor {
608                    return Err(ParseError::Truncated);
609                }
610                let options_start = cursor;
611                let (consumed, has_marker) =
612                    scan_options_bounded(&buf[options_start..mic_start])?;
613                let options_range = options_start..options_start + consumed;
614                let body_start = options_start + consumed;
615                let body_end = if has_marker { mic_start } else { body_start };
616                Ok(Self {
617                    fcf,
618                    options_range,
619                    flood_hops,
620                    dst,
621                    channel,
622                    ack_dst,
623                    source,
624                    sec_info,
625                    body_range: body_start..body_end,
626                    mic_range: mic_start..buf.len(),
627                    total_len: buf.len(),
628                })
629            }
630            PacketType::Multicast => {
631                ensure_len(buf, cursor, 2)?;
632                channel = Some(ChannelId([buf[cursor], buf[cursor + 1]]));
633                cursor += 2;
634                let parsed_sec = SecInfo::decode(&buf[cursor..])?;
635                let sec_len = parsed_sec.wire_len();
636                sec_info = Some(parsed_sec);
637                cursor += sec_len;
638                let mic_len = parsed_sec.scf.mic_size()?.byte_len();
639                let mic_start = buf
640                    .len()
641                    .checked_sub(mic_len)
642                    .ok_or(ParseError::Truncated)?;
643                if mic_start < cursor {
644                    return Err(ParseError::Truncated);
645                }
646                let options_start = cursor;
647                let (consumed, has_marker) =
648                    scan_options_bounded(&buf[options_start..mic_start])?;
649                let options_range = options_start..options_start + consumed;
650                cursor = options_start + consumed;
651                if parsed_sec.scf.encrypted() {
652                    let src_len = source_len(fcf.full_source());
653                    source = SourceAddrRef::Encrypted {
654                        offset: cursor,
655                        len: src_len,
656                    };
657                    Ok(Self {
658                        fcf,
659                        options_range,
660                        flood_hops,
661                        dst,
662                        channel,
663                        ack_dst,
664                        source,
665                        sec_info,
666                        body_range: cursor..mic_start,
667                        mic_range: mic_start..buf.len(),
668                        total_len: buf.len(),
669                    })
670                } else {
671                    let src_len = source_len(fcf.full_source());
672                    source = if fcf.full_source() {
673                        ensure_len(buf, cursor, 32)?;
674                        SourceAddrRef::FullKeyAt { offset: cursor }
675                    } else {
676                        ensure_len(buf, cursor, 3)?;
677                        SourceAddrRef::Hint(NodeHint([
678                            buf[cursor],
679                            buf[cursor + 1],
680                            buf[cursor + 2],
681                        ]))
682                    };
683                    cursor += src_len;
684                    let body_start = cursor;
685                    let body_end = if has_marker { mic_start } else { body_start };
686                    Ok(Self {
687                        fcf,
688                        options_range,
689                        flood_hops,
690                        dst,
691                        channel,
692                        ack_dst,
693                        source,
694                        sec_info,
695                        body_range: body_start..body_end,
696                        mic_range: mic_start..buf.len(),
697                        total_len: buf.len(),
698                    })
699                }
700            }
701            PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {
702                ensure_len(buf, cursor, 2)?;
703                channel = Some(ChannelId([buf[cursor], buf[cursor + 1]]));
704                cursor += 2;
705                let parsed_sec = SecInfo::decode(&buf[cursor..])?;
706                let sec_len = parsed_sec.wire_len();
707                sec_info = Some(parsed_sec);
708                cursor += sec_len;
709                let mic_len = parsed_sec.scf.mic_size()?.byte_len();
710                let mic_start = buf
711                    .len()
712                    .checked_sub(mic_len)
713                    .ok_or(ParseError::Truncated)?;
714                if mic_start < cursor {
715                    return Err(ParseError::Truncated);
716                }
717                let options_start = cursor;
718                let (consumed, has_marker) =
719                    scan_options_bounded(&buf[options_start..mic_start])?;
720                let options_range = options_start..options_start + consumed;
721                cursor = options_start + consumed;
722                let src_len = source_len(fcf.full_source());
723                ensure_len(buf, cursor, 3 + src_len)?;
724                if parsed_sec.scf.encrypted() {
725                    source = SourceAddrRef::Encrypted {
726                        offset: cursor + 3,
727                        len: src_len,
728                    };
729                    cursor += 3 + src_len;
730                } else {
731                    dst = Some(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]));
732                    cursor += 3;
733                    source = if fcf.full_source() {
734                        ensure_len(buf, cursor, 32)?;
735                        SourceAddrRef::FullKeyAt { offset: cursor }
736                    } else {
737                        ensure_len(buf, cursor, 3)?;
738                        SourceAddrRef::Hint(NodeHint([
739                            buf[cursor],
740                            buf[cursor + 1],
741                            buf[cursor + 2],
742                        ]))
743                    };
744                    cursor += src_len;
745                }
746                let body_start = cursor;
747                let body_end = if has_marker { mic_start } else { body_start };
748                Ok(Self {
749                    fcf,
750                    options_range,
751                    flood_hops,
752                    dst,
753                    channel,
754                    ack_dst,
755                    source,
756                    sec_info,
757                    body_range: body_start..body_end,
758                    mic_range: mic_start..buf.len(),
759                    total_len: buf.len(),
760                })
761            }
762            PacketType::Reserved5 => Err(ParseError::MalformedOption),
763        }
764    }
765
766    /// Convenience accessor for the decoded packet type.
767    pub fn packet_type(&self) -> PacketType {
768        self.fcf.packet_type()
769    }
770
771    /// Return whether the packet requests a MAC ACK.
772    pub fn ack_requested(&self) -> bool {
773        self.packet_type().ack_requested()
774    }
775
776    /// Return whether the parsed packet is a beacon broadcast with empty body.
777    pub fn is_beacon(&self) -> bool {
778        self.packet_type() == PacketType::Broadcast && self.body_range.is_empty()
779    }
780}
781
782#[derive(Clone, Debug, Default, PartialEq, Eq)]
783pub struct ParsedOptions {
784    pub region_code: Option<[u8; 2]>,
785    pub source_route: Option<Range<usize>>,
786    pub trace_route: Option<Range<usize>>,
787    pub min_rssi: Option<i16>,
788    pub min_snr: Option<i8>,
789    pub route_retry: bool,
790    pub has_unknown_critical: bool,
791}
792
793impl ParsedOptions {
794    pub fn extract(buf: &[u8], range: Range<usize>) -> Result<Self, ParseError> {
795        let mut parsed = Self::default();
796        if range.is_empty() {
797            return Ok(parsed);
798        }
799        let options = &buf[range.clone()];
800        for entry in OptionDecoder::new(options) {
801            let (number, value) = entry?;
802            let relative_start = unsafe { value.as_ptr().offset_from(options.as_ptr()) } as usize;
803            let value_start = range.start + relative_start;
804            let value_range = value_start..value_start + value.len();
805            match OptionNumber::from(number) {
806                OptionNumber::RegionCode if value.len() == 2 => {
807                    parsed.region_code = Some([value[0], value[1]]);
808                }
809                OptionNumber::TraceRoute => parsed.trace_route = Some(value_range),
810                OptionNumber::SourceRoute => parsed.source_route = Some(value_range),
811                OptionNumber::RouteRetry if value.is_empty() => parsed.route_retry = true,
812                OptionNumber::MinRssi if value.len() == 2 => {
813                    parsed.min_rssi = Some(i16::from_be_bytes([value[0], value[1]]));
814                }
815                OptionNumber::MinSnr if value.len() == 1 => parsed.min_snr = Some(value[0] as i8),
816                OptionNumber::Unknown(raw) if raw & 1 != 0 => parsed.has_unknown_critical = true,
817                _ => {}
818            }
819        }
820        Ok(parsed)
821    }
822}
823
824pub fn iter_options<'a>(buf: &'a [u8], range: Range<usize>) -> OptionDecoder<'a> {
825    OptionDecoder::new(&buf[range])
826}
827
828pub fn feed_aad(header: &PacketHeader, packet_buf: &[u8], mut sink: impl FnMut(&[u8])) {
829    sink(&packet_buf[..1]);
830    for option in iter_options(packet_buf, header.options_range.clone()) {
831        let Ok((number, value)) = option else {
832            return;
833        };
834        let option_number = OptionNumber::from(number);
835        if option_number.is_dynamic() {
836            continue;
837        }
838        let mut tl = [0u8; 4];
839        tl[..2].copy_from_slice(&number.to_be_bytes());
840        tl[2..].copy_from_slice(&(value.len() as u16).to_be_bytes());
841        sink(&tl);
842        sink(value);
843    }
844
845    if let Some(dst) = header.dst {
846        sink(&dst.0);
847    }
848    if let Some(channel) = header.channel {
849        sink(&channel.0);
850    }
851    match header.source {
852        SourceAddrRef::Hint(hint) => sink(&hint.0),
853        SourceAddrRef::FullKeyAt { offset } => sink(&packet_buf[offset..offset + 32]),
854        SourceAddrRef::Encrypted { .. } | SourceAddrRef::None => {}
855    }
856    if let Some(sec_info) = header.sec_info {
857        let mut buf = [0u8; 7];
858        let Ok(len) = sec_info.encode(&mut buf) else {
859            return;
860        };
861        sink(&buf[..len]);
862    }
863}
864
865fn ensure_len(buf: &[u8], offset: usize, len: usize) -> Result<(), ParseError> {
866    if buf.len() < offset + len {
867        Err(ParseError::Truncated)
868    } else {
869        Ok(())
870    }
871}
872
873/// Scan an options region that may end with an explicit `0xFF` marker or with
874/// the end of the bounded slice.
875///
876/// Returns the number of bytes consumed (including the terminator byte when
877/// present) and a flag indicating whether the terminator was observed. When
878/// the terminator is absent, the caller can infer that no payload follows.
879fn scan_options_bounded(data: &[u8]) -> Result<(usize, bool), ParseError> {
880    let mut pos = 0;
881    let mut last_number: u16 = 0;
882    while pos < data.len() {
883        let first = data[pos];
884        if first == 0xFF {
885            return Ok((pos + 1, true));
886        }
887        pos += 1;
888        let delta_nibble = first >> 4;
889        let len_nibble = first & 0x0F;
890        let (delta, delta_len) = read_extended(&data[pos..], delta_nibble)?;
891        pos += delta_len;
892        let (len, len_len) = read_extended(&data[pos..], len_nibble)?;
893        pos += len_len;
894        if pos + len as usize > data.len() {
895            return Err(ParseError::Truncated);
896        }
897        let number = last_number
898            .checked_add(delta)
899            .ok_or(ParseError::MalformedOption)?;
900        pos += len as usize;
901        last_number = number;
902    }
903    Ok((pos, false))
904}
905
906fn read_extended(data: &[u8], nibble: u8) -> Result<(u16, usize), ParseError> {
907    match nibble {
908        0..=12 => Ok((nibble as u16, 0)),
909        13 => {
910            if data.is_empty() {
911                return Err(ParseError::Truncated);
912            }
913            Ok((data[0] as u16 + 13, 1))
914        }
915        14 => {
916            if data.len() < 2 {
917                return Err(ParseError::Truncated);
918            }
919            Ok((u16::from_be_bytes([data[0], data[1]]) + 269, 2))
920        }
921        _ => Err(ParseError::InvalidOptionNibble),
922    }
923}
924
925pub(crate) fn source_len(full_source: bool) -> usize {
926    if full_source { 32 } else { 3 }
927}
928
929#[derive(Debug, PartialEq, Eq)]
930pub struct UnsealedPacket<'a> {
931    buf: &'a mut [u8],
932    total_len: usize,
933    body_range: Range<usize>,
934    blind_addr_range: Option<Range<usize>>,
935    mic_range: Range<usize>,
936    sec_info_range: Range<usize>,
937    aad_static_options: Range<usize>,
938}
939
940impl<'a> UnsealedPacket<'a> {
941    pub fn new(
942        buf: &'a mut [u8],
943        total_len: usize,
944        body_range: Range<usize>,
945        blind_addr_range: Option<Range<usize>>,
946        mic_range: Range<usize>,
947        sec_info_range: Range<usize>,
948        aad_static_options: Range<usize>,
949    ) -> Self {
950        Self {
951            buf,
952            total_len,
953            body_range,
954            blind_addr_range,
955            mic_range,
956            sec_info_range,
957            aad_static_options,
958        }
959    }
960
961    pub fn header(&self) -> Result<PacketHeader, ParseError> {
962        PacketHeader::parse(self.as_bytes())
963    }
964
965    pub fn body(&self) -> &[u8] {
966        &self.buf[self.body_range.clone()]
967    }
968
969    pub fn body_mut(&mut self) -> &mut [u8] {
970        &mut self.buf[self.body_range.clone()]
971    }
972
973    pub fn blind_addr_range(&self) -> Option<Range<usize>> {
974        self.blind_addr_range.clone()
975    }
976
977    pub fn blind_addr(&self) -> Option<&[u8]> {
978        let range = self.blind_addr_range.clone()?;
979        Some(&self.buf[range])
980    }
981
982    pub fn mic_slot(&mut self) -> &mut [u8] {
983        &mut self.buf[self.mic_range.clone()]
984    }
985
986    pub fn as_bytes(&self) -> &[u8] {
987        &self.buf[..self.total_len]
988    }
989
990    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
991        &mut self.buf[..self.total_len]
992    }
993
994    pub fn total_len(&self) -> usize {
995        self.total_len
996    }
997
998    pub fn sec_info_range(&self) -> Range<usize> {
999        self.sec_info_range.clone()
1000    }
1001
1002    pub fn aad_static_options(&self) -> Range<usize> {
1003        self.aad_static_options.clone()
1004    }
1005}