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            // Broadcast admission is per-command: this gate only says a MAC
108            // command may ride a broadcast at all; receivers act only on the
109            // commands whose definitions permit broadcast carriage (currently
110            // the Identity Request, with its own flood-management rules).
111            Self::MacCommand => matches!(
112                packet_type,
113                PacketType::Unicast
114                    | PacketType::UnicastAckReq
115                    | PacketType::BlindUnicast
116                    | PacketType::BlindUnicastAckReq
117                    | PacketType::Multicast
118                    | PacketType::Broadcast
119            ),
120            Self::TextMessage | Self::CoapOverUmsh => matches!(
121                packet_type,
122                PacketType::Unicast
123                    | PacketType::UnicastAckReq
124                    | PacketType::BlindUnicast
125                    | PacketType::BlindUnicastAckReq
126                    | PacketType::Multicast
127            ),
128            Self::NodeManagement | Self::ChatRoomMessage => matches!(
129                packet_type,
130                PacketType::Unicast
131                    | PacketType::UnicastAckReq
132                    | PacketType::BlindUnicast
133                    | PacketType::BlindUnicastAckReq
134            ),
135        }
136    }
137}
138
139/// Frame-control field wrapper.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub struct Fcf(pub u8);
142
143impl Fcf {
144    /// Build an FCF from structured flags.
145    pub const fn new(packet_type: PacketType, full_source: bool, flood_hops_present: bool) -> Self {
146        Self(
147            (UMSH_VERSION << 6)
148                | ((packet_type as u8) << 3)
149                | ((full_source as u8) << 2)
150                | flood_hops_present as u8,
151        )
152    }
153
154    /// Return the encoded protocol version.
155    pub const fn version(self) -> u8 {
156        self.0 >> 6
157    }
158
159    /// Return the encoded packet type.
160    pub const fn packet_type(self) -> PacketType {
161        PacketType::from_bits((self.0 >> 3) & 0x07)
162    }
163
164    /// Return whether the source address is the full 32-byte public key.
165    pub const fn full_source(self) -> bool {
166        self.0 & 0x04 != 0
167    }
168
169    /// Return whether the reserved bit is clear as required by the spec.
170    pub const fn reserved_valid(self) -> bool {
171        self.0 & 0x02 == 0
172    }
173
174    /// Return whether a flood-hop byte is present.
175    pub const fn flood_hops_present(self) -> bool {
176        self.0 & 0x01 != 0
177    }
178
179    /// Return the FCF as it enters the AAD, with the flood-hops-present bit
180    /// cleared.
181    ///
182    /// `FHOPS` is a forwarding budget, excluded from the AAD because repeaters
183    /// rewrite it. Whether the byte is present at all is the same budget
184    /// expressed one bit up, and a sender that abandons a source route for a
185    /// flood adds it to a packet already sealed. Clearing the bit is not a
186    /// hole: flipping it on the wire shifts every field the parser reads after
187    /// it, so `DST`/`SRC`/`SECINFO` reach the AAD as different values and the
188    /// MIC still fails.
189    pub const fn aad_byte(self) -> u8 {
190        self.0 & !0x01
191    }
192}
193
194/// Security-control field wrapper.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct Scf(pub u8);
197
198impl Scf {
199    /// Build an SCF from structured flags.
200    pub const fn new(encrypted: bool, mic_size: MicSize, salt_present: bool) -> Self {
201        Self(((encrypted as u8) << 7) | ((mic_size as u8) << 5) | ((salt_present as u8) << 4))
202    }
203
204    /// Return whether the body is encrypted in place.
205    pub const fn encrypted(self) -> bool {
206        self.0 & 0x80 != 0
207    }
208
209    /// Decode the configured MIC size.
210    pub fn mic_size(self) -> Result<MicSize, ParseError> {
211        MicSize::from_bits((self.0 >> 5) & 0x03)
212    }
213
214    /// Return whether a salt field follows the frame counter.
215    pub const fn salt_present(self) -> bool {
216        self.0 & 0x10 != 0
217    }
218
219    /// Return whether the reserved low nibble is valid for the current spec.
220    pub const fn reserved_valid(self) -> bool {
221        self.0 & 0x0F == 0
222    }
223}
224
225/// Authenticator size carried by secured packets.
226#[derive(Clone, Copy, Debug, PartialEq, Eq)]
227pub enum MicSize {
228    Mic4 = 0,
229    Mic8 = 1,
230    Mic12 = 2,
231    Mic16 = 3,
232}
233
234impl MicSize {
235    /// Return the on-wire byte length of this MIC size.
236    pub const fn byte_len(self) -> usize {
237        match self {
238            Self::Mic4 => 4,
239            Self::Mic8 => 8,
240            Self::Mic12 => 12,
241            Self::Mic16 => 16,
242        }
243    }
244
245    /// Decode a MIC size from its two SCF bits.
246    pub fn from_bits(value: u8) -> Result<Self, ParseError> {
247        match value {
248            0 => Ok(Self::Mic4),
249            1 => Ok(Self::Mic8),
250            2 => Ok(Self::Mic12),
251            3 => Ok(Self::Mic16),
252            other => Err(ParseError::InvalidMicSize(other)),
253        }
254    }
255}
256
257/// Combined remaining/accumulated flood-hop counters.
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub struct FloodHops(pub u8);
260
261impl FloodHops {
262    /// Construct a flood-hop value if both nibbles fit in four bits.
263    pub fn new(remaining: u8, accumulated: u8) -> Option<Self> {
264        if remaining <= 0x0F && accumulated <= 0x0F {
265            Some(Self((remaining << 4) | accumulated))
266        } else {
267            None
268        }
269    }
270
271    /// Remaining forward-hop budget.
272    pub const fn remaining(self) -> u8 {
273        self.0 >> 4
274    }
275
276    /// Number of hops already consumed.
277    pub const fn accumulated(self) -> u8 {
278        self.0 & 0x0F
279    }
280
281    /// Return the next forwarded hop count.
282    pub fn decremented(self) -> Self {
283        let remaining = self.remaining();
284        if remaining == 0 {
285            self
286        } else {
287            Self::new(remaining - 1, self.accumulated().saturating_add(1)).unwrap_or(self)
288        }
289    }
290}
291
292/// Three-byte node hint derived from a public key.
293#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
294pub struct NodeHint(pub [u8; 3]);
295
296impl NodeHint {
297    /// Derive the hint from the first three public-key bytes.
298    pub fn from_public_key(key: &PublicKey) -> Self {
299        Self([key.0[0], key.0[1], key.0[2]])
300    }
301}
302
303impl core::fmt::Display for NodeHint {
304    /// Renders up to four base58 characters of the matching address space,
305    /// star-truncated where the hint no longer pins down the encoding.
306    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307        crate::base58::fmt_hint(f, &self.0, 4)
308    }
309}
310
311/// Two-byte router hint used in learned routes.
312#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
313pub struct RouterHint(pub [u8; 2]);
314
315impl RouterHint {
316    /// Derive the router hint from the first two public-key bytes.
317    pub fn from_public_key(key: &PublicKey) -> Self {
318        Self([key.0[0], key.0[1]])
319    }
320}
321
322impl core::fmt::Display for RouterHint {
323    /// Renders up to three base58 characters of the matching address space,
324    /// star-truncated where the hint no longer pins down the encoding.
325    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
326        crate::base58::fmt_hint(f, &self.0, 3)
327    }
328}
329
330/// Two-byte multicast channel identifier.
331#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
332pub struct ChannelId(pub [u8; 2]);
333
334/// Sixteen-byte channel tag: a local identity for a channel, wide enough to
335/// distinguish channels that a [`ChannelId`] cannot.
336///
337/// The two-byte identifier is a hint, and collisions between distinct channel
338/// keys are legal — a frame belongs to whichever channel key authenticates it,
339/// not to whichever key derives the same identifier. Bookkeeping that has no
340/// key at hand to authenticate against therefore needs a wider name; this is
341/// that name. Nothing on the wire carries it.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
343pub struct ChannelTag(pub [u8; 16]);
344
345impl ChannelTag {
346    /// The channel identifier this tag begins with.
347    ///
348    /// Both come from the same derivation, so the identifier is the tag's own
349    /// first two bytes rather than a separate computation.
350    pub fn channel_id(&self) -> ChannelId {
351        ChannelId([self.0[0], self.0[1]])
352    }
353}
354
355/// Node public key, which also acts as the full network address.
356#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
357pub struct PublicKey(pub [u8; 32]);
358
359impl PublicKey {
360    /// Return the node hint associated with this key.
361    pub fn hint(&self) -> NodeHint {
362        NodeHint::from_public_key(self)
363    }
364
365    /// Return the router hint associated with this key.
366    pub fn router_hint(&self) -> RouterHint {
367        RouterHint::from_public_key(self)
368    }
369}
370
371impl core::fmt::Display for PublicKey {
372    /// Renders the canonical fixed-width base58 address (always 44 digits).
373    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
374        let encoded = crate::base58::encode(&self.0);
375        f.write_str(core::str::from_utf8(&encoded).map_err(|_| core::fmt::Error)?)
376    }
377}
378
379impl core::fmt::LowerHex for PublicKey {
380    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
381        for byte in self.0 {
382            write!(f, "{byte:02x}")?;
383        }
384        Ok(())
385    }
386}
387
388impl core::fmt::UpperHex for PublicKey {
389    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
390        for byte in self.0 {
391            write!(f, "{byte:02X}")?;
392        }
393        Ok(())
394    }
395}
396
397impl core::str::FromStr for PublicKey {
398    type Err = crate::AddressParseError;
399
400    /// Parses either address form: 44 base58 characters or 64 base16
401    /// characters (either case), distinguished by length.
402    fn from_str(s: &str) -> Result<Self, Self::Err> {
403        fn hex_value(digit: u8) -> Result<u8, crate::AddressParseError> {
404            match digit {
405                b'0'..=b'9' => Ok(digit - b'0'),
406                b'a'..=b'f' => Ok(digit - b'a' + 10),
407                b'A'..=b'F' => Ok(digit - b'A' + 10),
408                _ => Err(crate::AddressParseError::InvalidCharacter),
409            }
410        }
411
412        let bytes = s.as_bytes();
413        match bytes.len() {
414            crate::base58::ENCODED_LEN => crate::base58::decode(bytes).map(Self),
415            64 => {
416                let mut key = [0u8; 32];
417                for (slot, pair) in key.iter_mut().zip(bytes.chunks_exact(2)) {
418                    *slot = (hex_value(pair[0])? << 4) | hex_value(pair[1])?;
419                }
420                Ok(Self(key))
421            }
422            _ => Err(crate::AddressParseError::InvalidLength),
423        }
424    }
425}
426
427/// Raw 32-byte multicast channel secret.
428#[derive(Clone, Copy, zeroize::Zeroize)]
429pub struct ChannelKey(pub [u8; 32]);
430
431impl PartialEq for ChannelKey {
432    fn eq(&self, other: &Self) -> bool {
433        self.0 == other.0
434    }
435}
436
437impl Eq for ChannelKey {}
438
439impl core::fmt::Debug for ChannelKey {
440    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
441        f.write_str("ChannelKey([redacted])")
442    }
443}
444
445/// Source address supplied while constructing packets.
446#[derive(Clone, Copy, Debug, PartialEq, Eq)]
447pub enum SourceAddr<'a> {
448    Hint(NodeHint),
449    Full(&'a PublicKey),
450}
451
452impl SourceAddr<'_> {
453    /// Return the hint form of this address.
454    pub fn hint(&self) -> NodeHint {
455        match self {
456            Self::Hint(hint) => *hint,
457            Self::Full(key) => key.hint(),
458        }
459    }
460}
461
462/// Decoded SECINFO structure.
463#[derive(Clone, Copy, Debug, PartialEq, Eq)]
464pub struct SecInfo {
465    pub scf: Scf,
466    pub frame_counter: u32,
467    pub salt: Option<u16>,
468}
469
470impl SecInfo {
471    /// Return the SECINFO on-wire length.
472    pub fn wire_len(&self) -> usize {
473        if self.salt.is_some() { 7 } else { 5 }
474    }
475
476    /// Encode SECINFO into `buf` and return the number of bytes written.
477    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
478        let needed = self.wire_len();
479        if buf.len() < needed {
480            return Err(EncodeError::BufferTooSmall);
481        }
482        buf[0] = self.scf.0;
483        buf[1..5].copy_from_slice(&self.frame_counter.to_be_bytes());
484        if let Some(salt) = self.salt {
485            buf[5..7].copy_from_slice(&salt.to_be_bytes());
486        }
487        Ok(needed)
488    }
489
490    /// Decode SECINFO from the start of `buf`.
491    pub fn decode(buf: &[u8]) -> Result<Self, ParseError> {
492        if buf.len() < 5 {
493            return Err(ParseError::Truncated);
494        }
495        let scf = Scf(buf[0]);
496        if !scf.reserved_valid() {
497            return Err(ParseError::InvalidScfReserved);
498        }
499        let salt = if scf.salt_present() {
500            if buf.len() < 7 {
501                return Err(ParseError::Truncated);
502            }
503            Some(u16::from_be_bytes([buf[5], buf[6]]))
504        } else {
505            None
506        };
507        Ok(Self {
508            scf,
509            frame_counter: u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]),
510            salt,
511        })
512    }
513}
514
515/// Known packet-option numbers.
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517pub enum OptionNumber {
518    RegionCode,
519    TraceRoute,
520    SourceRoute,
521    OperatorCallsign,
522    MinRssi,
523    RouteRetry,
524    StationCallsign,
525    AckMic,
526    MinSnr,
527    TraceSignal,
528    Unknown(u16),
529}
530
531impl OptionNumber {
532    /// Return the numeric option number used on the wire.
533    pub fn as_u16(self) -> u16 {
534        match self {
535            Self::RegionCode => 11,
536            Self::TraceRoute => 2,
537            Self::SourceRoute => 3,
538            Self::OperatorCallsign => 4,
539            Self::MinRssi => 5,
540            Self::RouteRetry => 6,
541            Self::StationCallsign => 7,
542            Self::AckMic => 8,
543            Self::MinSnr => 9,
544            Self::TraceSignal => 10,
545            Self::Unknown(value) => value,
546        }
547    }
548
549    /// Return whether the option is critical when unknown.
550    pub fn is_critical(self) -> bool {
551        self.as_u16() & 1 != 0
552    }
553
554    /// Return whether the option is considered dynamic for AAD purposes.
555    pub fn is_dynamic(self) -> bool {
556        self.as_u16() & 2 != 0
557    }
558}
559
560impl From<u16> for OptionNumber {
561    fn from(value: u16) -> Self {
562        match value {
563            2 => Self::TraceRoute,
564            3 => Self::SourceRoute,
565            4 => Self::OperatorCallsign,
566            5 => Self::MinRssi,
567            6 => Self::RouteRetry,
568            7 => Self::StationCallsign,
569            8 => Self::AckMic,
570            9 => Self::MinSnr,
571            10 => Self::TraceSignal,
572            11 => Self::RegionCode,
573            other => Self::Unknown(other),
574        }
575    }
576}
577
578/// Parsed source-address location used by zero-copy packet processing.
579#[derive(Clone, Copy, Debug, PartialEq, Eq)]
580pub enum SourceAddrRef {
581    Hint(NodeHint),
582    FullKeyAt { offset: usize },
583    Encrypted { offset: usize, len: usize },
584    None,
585}
586
587/// Parsed packet header with borrowed ranges into the original frame.
588#[derive(Clone, Debug, PartialEq, Eq)]
589pub struct PacketHeader {
590    pub fcf: Fcf,
591    pub options_range: Range<usize>,
592    pub flood_hops: Option<FloodHops>,
593    pub dst: Option<NodeHint>,
594    pub channel: Option<ChannelId>,
595    pub source: SourceAddrRef,
596    pub sec_info: Option<SecInfo>,
597    pub body_range: Range<usize>,
598    pub mic_range: Range<usize>,
599    pub total_len: usize,
600}
601
602impl PacketHeader {
603    /// Parse a complete on-wire packet header and compute payload/MIC ranges.
604    pub fn parse(buf: &[u8]) -> Result<Self, ParseError> {
605        if buf.is_empty() {
606            return Err(ParseError::Truncated);
607        }
608
609        let fcf = Fcf(buf[0]);
610        if fcf.version() != UMSH_VERSION {
611            return Err(ParseError::InvalidVersion(fcf.version()));
612        }
613        if !fcf.reserved_valid() {
614            return Err(ParseError::InvalidFcfReserved);
615        }
616
617        let mut cursor = 1;
618        let flood_hops = if fcf.flood_hops_present() {
619            if cursor >= buf.len() {
620                return Err(ParseError::Truncated);
621            }
622            let fh = FloodHops(buf[cursor]);
623            cursor += 1;
624            Some(fh)
625        } else {
626            None
627        };
628
629        let packet_type = fcf.packet_type();
630        let mut dst = None;
631        let mut channel = None;
632        let mut source = SourceAddrRef::None;
633        let mut sec_info = None;
634
635        match packet_type {
636            PacketType::Broadcast => {
637                let src_len = source_len(fcf.full_source());
638                source = if fcf.full_source() {
639                    ensure_len(buf, cursor, 32)?;
640                    SourceAddrRef::FullKeyAt { offset: cursor }
641                } else {
642                    ensure_len(buf, cursor, 3)?;
643                    SourceAddrRef::Hint(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]))
644                };
645                cursor += src_len;
646                let options_start = cursor;
647                let options_end = buf.len();
648                let (consumed, has_marker) =
649                    scan_options_bounded(&buf[options_start..options_end])?;
650                let options_range = options_start..options_start + consumed;
651                let body_start = options_start + consumed;
652                let body_end = if has_marker { buf.len() } else { body_start };
653                Ok(Self {
654                    fcf,
655                    options_range,
656                    flood_hops,
657                    dst,
658                    channel,
659                    source,
660                    sec_info,
661                    body_range: body_start..body_end,
662                    mic_range: buf.len()..buf.len(),
663                    total_len: buf.len(),
664                })
665            }
666            PacketType::MacAck => {
667                // MAC acks carry no destination hint: options follow the
668                // header directly, then a fixed 8-byte ack trailer
669                // (`ack_mic` || `ack_tag`).
670                let options_start = cursor;
671                let options_end = buf.len().checked_sub(8).ok_or(ParseError::Truncated)?;
672                if options_end < options_start {
673                    return Err(ParseError::Truncated);
674                }
675                // MAC ACK has no payload — the options region is bounded by
676                // the fixed 8-byte ack trailer. The scan must consume the
677                // entire region: either the marker is absent (and the scan
678                // exhausts the region), or the marker is the last byte (and
679                // `consumed` includes it). Any other case means there are
680                // bytes between an end-of-options marker and the trailer,
681                // which the wire format does not assign meaning to.
682                let region = &buf[options_start..options_end];
683                let (consumed, _has_marker) = scan_options_bounded(region)?;
684                if consumed != region.len() {
685                    return Err(ParseError::MalformedOption);
686                }
687                let options_range = options_start..options_start + consumed;
688                let trailer_start = options_end;
689                Ok(Self {
690                    fcf,
691                    options_range,
692                    flood_hops,
693                    dst,
694                    channel,
695                    source,
696                    sec_info,
697                    body_range: trailer_start..trailer_start + 8,
698                    mic_range: trailer_start..trailer_start + 8,
699                    total_len: trailer_start + 8,
700                })
701            }
702            PacketType::Unicast | PacketType::UnicastAckReq => {
703                ensure_len(buf, cursor, 3)?;
704                dst = Some(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]));
705                cursor += 3;
706                let src_len = source_len(fcf.full_source());
707                source = if fcf.full_source() {
708                    ensure_len(buf, cursor, 32)?;
709                    SourceAddrRef::FullKeyAt { offset: cursor }
710                } else {
711                    ensure_len(buf, cursor, 3)?;
712                    SourceAddrRef::Hint(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]))
713                };
714                cursor += src_len;
715                let parsed_sec = SecInfo::decode(&buf[cursor..])?;
716                let sec_len = parsed_sec.wire_len();
717                sec_info = Some(parsed_sec);
718                cursor += sec_len;
719                let mic_len = parsed_sec.scf.mic_size()?.byte_len();
720                let mic_start = buf
721                    .len()
722                    .checked_sub(mic_len)
723                    .ok_or(ParseError::Truncated)?;
724                if mic_start < cursor {
725                    return Err(ParseError::Truncated);
726                }
727                let options_start = cursor;
728                let (consumed, has_marker) = scan_options_bounded(&buf[options_start..mic_start])?;
729                let options_range = options_start..options_start + consumed;
730                let body_start = options_start + consumed;
731                let body_end = if has_marker { mic_start } else { body_start };
732                Ok(Self {
733                    fcf,
734                    options_range,
735                    flood_hops,
736                    dst,
737                    channel,
738                    source,
739                    sec_info,
740                    body_range: body_start..body_end,
741                    mic_range: mic_start..buf.len(),
742                    total_len: buf.len(),
743                })
744            }
745            PacketType::Multicast => {
746                ensure_len(buf, cursor, 2)?;
747                channel = Some(ChannelId([buf[cursor], buf[cursor + 1]]));
748                cursor += 2;
749                let parsed_sec = SecInfo::decode(&buf[cursor..])?;
750                let sec_len = parsed_sec.wire_len();
751                sec_info = Some(parsed_sec);
752                cursor += sec_len;
753                let mic_len = parsed_sec.scf.mic_size()?.byte_len();
754                let mic_start = buf
755                    .len()
756                    .checked_sub(mic_len)
757                    .ok_or(ParseError::Truncated)?;
758                if mic_start < cursor {
759                    return Err(ParseError::Truncated);
760                }
761                let options_start = cursor;
762                let (consumed, has_marker) = scan_options_bounded(&buf[options_start..mic_start])?;
763                let options_range = options_start..options_start + consumed;
764                cursor = options_start + consumed;
765                if parsed_sec.scf.encrypted() {
766                    let src_len = source_len(fcf.full_source());
767                    source = SourceAddrRef::Encrypted {
768                        offset: cursor,
769                        len: src_len,
770                    };
771                    Ok(Self {
772                        fcf,
773                        options_range,
774                        flood_hops,
775                        dst,
776                        channel,
777                        source,
778                        sec_info,
779                        body_range: cursor..mic_start,
780                        mic_range: mic_start..buf.len(),
781                        total_len: buf.len(),
782                    })
783                } else {
784                    let src_len = source_len(fcf.full_source());
785                    source = if fcf.full_source() {
786                        ensure_len(buf, cursor, 32)?;
787                        SourceAddrRef::FullKeyAt { offset: cursor }
788                    } else {
789                        ensure_len(buf, cursor, 3)?;
790                        SourceAddrRef::Hint(NodeHint([
791                            buf[cursor],
792                            buf[cursor + 1],
793                            buf[cursor + 2],
794                        ]))
795                    };
796                    cursor += src_len;
797                    let body_start = cursor;
798                    let body_end = if has_marker { mic_start } else { body_start };
799                    Ok(Self {
800                        fcf,
801                        options_range,
802                        flood_hops,
803                        dst,
804                        channel,
805                        source,
806                        sec_info,
807                        body_range: body_start..body_end,
808                        mic_range: mic_start..buf.len(),
809                        total_len: buf.len(),
810                    })
811                }
812            }
813            PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {
814                ensure_len(buf, cursor, 2)?;
815                channel = Some(ChannelId([buf[cursor], buf[cursor + 1]]));
816                cursor += 2;
817                let parsed_sec = SecInfo::decode(&buf[cursor..])?;
818                let sec_len = parsed_sec.wire_len();
819                sec_info = Some(parsed_sec);
820                cursor += sec_len;
821                let mic_len = parsed_sec.scf.mic_size()?.byte_len();
822                let mic_start = buf
823                    .len()
824                    .checked_sub(mic_len)
825                    .ok_or(ParseError::Truncated)?;
826                if mic_start < cursor {
827                    return Err(ParseError::Truncated);
828                }
829                let options_start = cursor;
830                let (consumed, has_marker) = scan_options_bounded(&buf[options_start..mic_start])?;
831                let options_range = options_start..options_start + consumed;
832                cursor = options_start + consumed;
833                let src_len = source_len(fcf.full_source());
834                ensure_len(buf, cursor, 3 + src_len)?;
835                if parsed_sec.scf.encrypted() {
836                    source = SourceAddrRef::Encrypted {
837                        offset: cursor + 3,
838                        len: src_len,
839                    };
840                    cursor += 3 + src_len;
841                } else {
842                    dst = Some(NodeHint([buf[cursor], buf[cursor + 1], buf[cursor + 2]]));
843                    cursor += 3;
844                    source = if fcf.full_source() {
845                        ensure_len(buf, cursor, 32)?;
846                        SourceAddrRef::FullKeyAt { offset: cursor }
847                    } else {
848                        ensure_len(buf, cursor, 3)?;
849                        SourceAddrRef::Hint(NodeHint([
850                            buf[cursor],
851                            buf[cursor + 1],
852                            buf[cursor + 2],
853                        ]))
854                    };
855                    cursor += src_len;
856                }
857                let body_start = cursor;
858                let body_end = if has_marker { mic_start } else { body_start };
859                Ok(Self {
860                    fcf,
861                    options_range,
862                    flood_hops,
863                    dst,
864                    channel,
865                    source,
866                    sec_info,
867                    body_range: body_start..body_end,
868                    mic_range: mic_start..buf.len(),
869                    total_len: buf.len(),
870                })
871            }
872            PacketType::Reserved5 => Err(ParseError::MalformedOption),
873        }
874    }
875
876    /// Convenience accessor for the decoded packet type.
877    pub fn packet_type(&self) -> PacketType {
878        self.fcf.packet_type()
879    }
880
881    /// Return whether the packet requests a MAC ACK.
882    pub fn ack_requested(&self) -> bool {
883        self.packet_type().ack_requested()
884    }
885
886    /// Return whether the parsed packet is a beacon broadcast with empty body.
887    pub fn is_beacon(&self) -> bool {
888        self.packet_type() == PacketType::Broadcast && self.body_range.is_empty()
889    }
890}
891
892/// Default Minimum RSSI threshold (dBm) when the option is present with a
893/// zero-length value. Per the spec this value is subject to change.
894pub const DEFAULT_MIN_RSSI_DBM: i16 = -100;
895
896/// Default Minimum SNR threshold (dB) when the option is present with a
897/// zero-length value. Per the spec this value is subject to change.
898pub const DEFAULT_MIN_SNR_DB: i8 = -3;
899
900#[derive(Clone, Debug, Default, PartialEq, Eq)]
901pub struct ParsedOptions {
902    pub region_code: Option<[u8; 2]>,
903    pub source_route: Option<Range<usize>>,
904    pub trace_route: Option<Range<usize>>,
905    pub trace_signal: Option<Range<usize>>,
906    pub min_rssi: Option<i16>,
907    pub min_snr: Option<i8>,
908    pub route_retry: bool,
909    pub has_unknown_critical: bool,
910}
911
912impl ParsedOptions {
913    pub fn extract(buf: &[u8], range: Range<usize>) -> Result<Self, ParseError> {
914        let mut parsed = Self::default();
915        if range.is_empty() {
916            return Ok(parsed);
917        }
918        let options = &buf[range.clone()];
919        for entry in OptionDecoder::new(options) {
920            let (number, value) = entry?;
921            let relative_start = unsafe { value.as_ptr().offset_from(options.as_ptr()) } as usize;
922            let value_start = range.start + relative_start;
923            let value_range = value_start..value_start + value.len();
924            match OptionNumber::from(number) {
925                OptionNumber::RegionCode if value.len() == 2 => {
926                    parsed.region_code = Some([value[0], value[1]]);
927                }
928                OptionNumber::TraceRoute => parsed.trace_route = Some(value_range),
929                OptionNumber::TraceSignal => parsed.trace_signal = Some(value_range),
930                OptionNumber::SourceRoute => parsed.source_route = Some(value_range),
931                OptionNumber::RouteRetry if value.is_empty() => parsed.route_retry = true,
932                // Minimum RSSI (option 5): unsigned 1-byte value read as a
933                // negative dBm threshold (e.g. 130 → -130 dBm). A zero-length
934                // value selects the default threshold. Longer values are
935                // malformed and ignored.
936                OptionNumber::MinRssi if value.len() <= 1 => {
937                    parsed.min_rssi = Some(match value.first() {
938                        Some(&byte) => -i16::from(byte),
939                        None => DEFAULT_MIN_RSSI_DBM,
940                    });
941                }
942                // Minimum SNR (option 9): signed 1-byte value in dB. A
943                // zero-length value selects the default threshold.
944                OptionNumber::MinSnr if value.len() <= 1 => {
945                    parsed.min_snr = Some(match value.first() {
946                        Some(&byte) => byte as i8,
947                        None => DEFAULT_MIN_SNR_DB,
948                    });
949                }
950                OptionNumber::Unknown(raw) if raw & 1 != 0 => parsed.has_unknown_critical = true,
951                _ => {}
952            }
953        }
954        Ok(parsed)
955    }
956}
957
958#[cfg(test)]
959mod parsed_options_tests {
960    use super::*;
961
962    #[test]
963    fn min_rssi_one_byte_is_negated_dbm() {
964        // Option 5, length 1, value 130 → -130 dBm.
965        let buf = [0x51u8, 130];
966        let parsed = ParsedOptions::extract(&buf, 0..buf.len()).unwrap();
967        assert_eq!(parsed.min_rssi, Some(-130));
968    }
969
970    #[test]
971    fn min_rssi_zero_len_selects_default() {
972        // Option 5, length 0.
973        let buf = [0x50u8];
974        let parsed = ParsedOptions::extract(&buf, 0..buf.len()).unwrap();
975        assert_eq!(parsed.min_rssi, Some(DEFAULT_MIN_RSSI_DBM));
976    }
977
978    #[test]
979    fn min_snr_one_byte_is_signed_db() {
980        // Option 9, length 1, value 0xFD (-3).
981        let buf = [0x91u8, 0xFD];
982        let parsed = ParsedOptions::extract(&buf, 0..buf.len()).unwrap();
983        assert_eq!(parsed.min_snr, Some(-3));
984    }
985
986    #[test]
987    fn min_snr_zero_len_selects_default() {
988        // Option 9, length 0.
989        let buf = [0x90u8];
990        let parsed = ParsedOptions::extract(&buf, 0..buf.len()).unwrap();
991        assert_eq!(parsed.min_snr, Some(DEFAULT_MIN_SNR_DB));
992    }
993
994    #[test]
995    fn min_rssi_two_bytes_is_ignored() {
996        // Length 2 is malformed for Min RSSI; the option is ignored.
997        let buf = [0x52u8, 0x00, 0x82];
998        let parsed = ParsedOptions::extract(&buf, 0..buf.len()).unwrap();
999        assert_eq!(parsed.min_rssi, None);
1000    }
1001}
1002
1003pub fn iter_options<'a>(buf: &'a [u8], range: Range<usize>) -> OptionDecoder<'a> {
1004    OptionDecoder::new(&buf[range])
1005}
1006
1007pub fn feed_aad(header: &PacketHeader, packet_buf: &[u8], mut sink: impl FnMut(&[u8])) {
1008    sink(&[header.fcf.aad_byte()]);
1009    for option in iter_options(packet_buf, header.options_range.clone()) {
1010        let Ok((number, value)) = option else {
1011            return;
1012        };
1013        let option_number = OptionNumber::from(number);
1014        if option_number.is_dynamic() {
1015            continue;
1016        }
1017        let mut tl = [0u8; 4];
1018        tl[..2].copy_from_slice(&number.to_be_bytes());
1019        tl[2..].copy_from_slice(&(value.len() as u16).to_be_bytes());
1020        sink(&tl);
1021        sink(value);
1022    }
1023
1024    if let Some(dst) = header.dst {
1025        sink(&dst.0);
1026    }
1027    if let Some(channel) = header.channel {
1028        sink(&channel.0);
1029    }
1030    match header.source {
1031        SourceAddrRef::Hint(hint) => sink(&hint.0),
1032        SourceAddrRef::FullKeyAt { offset } => sink(&packet_buf[offset..offset + 32]),
1033        SourceAddrRef::Encrypted { .. } | SourceAddrRef::None => {}
1034    }
1035    if let Some(sec_info) = header.sec_info {
1036        let mut buf = [0u8; 7];
1037        let Ok(len) = sec_info.encode(&mut buf) else {
1038            return;
1039        };
1040        sink(&buf[..len]);
1041    }
1042}
1043
1044fn ensure_len(buf: &[u8], offset: usize, len: usize) -> Result<(), ParseError> {
1045    if buf.len() < offset + len {
1046        Err(ParseError::Truncated)
1047    } else {
1048        Ok(())
1049    }
1050}
1051
1052/// Scan an options region that may end with an explicit `0xFF` marker or with
1053/// the end of the bounded slice.
1054///
1055/// Returns the number of bytes consumed (including the terminator byte when
1056/// present) and a flag indicating whether the terminator was observed. When
1057/// the terminator is absent, the caller can infer that no payload follows.
1058fn scan_options_bounded(data: &[u8]) -> Result<(usize, bool), ParseError> {
1059    let mut pos = 0;
1060    let mut last_number: u16 = 0;
1061    while pos < data.len() {
1062        let first = data[pos];
1063        if first == 0xFF {
1064            return Ok((pos + 1, true));
1065        }
1066        pos += 1;
1067        let delta_nibble = first >> 4;
1068        let len_nibble = first & 0x0F;
1069        let (delta, delta_len) = read_extended(&data[pos..], delta_nibble)?;
1070        pos += delta_len;
1071        let (len, len_len) = read_extended(&data[pos..], len_nibble)?;
1072        pos += len_len;
1073        if pos + len as usize > data.len() {
1074            return Err(ParseError::Truncated);
1075        }
1076        let number = last_number
1077            .checked_add(delta)
1078            .ok_or(ParseError::MalformedOption)?;
1079        pos += len as usize;
1080        last_number = number;
1081    }
1082    Ok((pos, false))
1083}
1084
1085fn read_extended(data: &[u8], nibble: u8) -> Result<(u16, usize), ParseError> {
1086    match nibble {
1087        0..=12 => Ok((nibble as u16, 0)),
1088        13 => {
1089            if data.is_empty() {
1090                return Err(ParseError::Truncated);
1091            }
1092            Ok((data[0] as u16 + 13, 1))
1093        }
1094        14 => {
1095            if data.len() < 2 {
1096                return Err(ParseError::Truncated);
1097            }
1098            Ok((u16::from_be_bytes([data[0], data[1]]) + 269, 2))
1099        }
1100        _ => Err(ParseError::InvalidOptionNibble),
1101    }
1102}
1103
1104pub(crate) fn source_len(full_source: bool) -> usize {
1105    if full_source { 32 } else { 3 }
1106}
1107
1108#[derive(Debug, PartialEq, Eq)]
1109pub struct UnsealedPacket<'a> {
1110    buf: &'a mut [u8],
1111    total_len: usize,
1112    body_range: Range<usize>,
1113    blind_addr_range: Option<Range<usize>>,
1114    mic_range: Range<usize>,
1115    sec_info_range: Range<usize>,
1116    aad_static_options: Range<usize>,
1117}
1118
1119impl<'a> UnsealedPacket<'a> {
1120    pub fn new(
1121        buf: &'a mut [u8],
1122        total_len: usize,
1123        body_range: Range<usize>,
1124        blind_addr_range: Option<Range<usize>>,
1125        mic_range: Range<usize>,
1126        sec_info_range: Range<usize>,
1127        aad_static_options: Range<usize>,
1128    ) -> Self {
1129        Self {
1130            buf,
1131            total_len,
1132            body_range,
1133            blind_addr_range,
1134            mic_range,
1135            sec_info_range,
1136            aad_static_options,
1137        }
1138    }
1139
1140    pub fn header(&self) -> Result<PacketHeader, ParseError> {
1141        PacketHeader::parse(self.as_bytes())
1142    }
1143
1144    pub fn body(&self) -> &[u8] {
1145        &self.buf[self.body_range.clone()]
1146    }
1147
1148    pub fn body_mut(&mut self) -> &mut [u8] {
1149        &mut self.buf[self.body_range.clone()]
1150    }
1151
1152    pub fn blind_addr_range(&self) -> Option<Range<usize>> {
1153        self.blind_addr_range.clone()
1154    }
1155
1156    pub fn blind_addr(&self) -> Option<&[u8]> {
1157        let range = self.blind_addr_range.clone()?;
1158        Some(&self.buf[range])
1159    }
1160
1161    pub fn mic_slot(&mut self) -> &mut [u8] {
1162        &mut self.buf[self.mic_range.clone()]
1163    }
1164
1165    pub fn as_bytes(&self) -> &[u8] {
1166        &self.buf[..self.total_len]
1167    }
1168
1169    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
1170        &mut self.buf[..self.total_len]
1171    }
1172
1173    pub fn total_len(&self) -> usize {
1174        self.total_len
1175    }
1176
1177    pub fn sec_info_range(&self) -> Range<usize> {
1178        self.sec_info_range.clone()
1179    }
1180
1181    pub fn aad_static_options(&self) -> Range<usize> {
1182        self.aad_static_options.clone()
1183    }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::{NodeHint, PacketType, PayloadType, PublicKey, RouterHint};
1189    use crate::AddressParseError;
1190
1191    #[test]
1192    fn node_management_rides_only_unicast() {
1193        for packet_type in [
1194            PacketType::Unicast,
1195            PacketType::UnicastAckReq,
1196            PacketType::BlindUnicast,
1197            PacketType::BlindUnicastAckReq,
1198        ] {
1199            assert!(PayloadType::NodeManagement.allowed_for(packet_type));
1200        }
1201        for packet_type in [
1202            PacketType::Multicast,
1203            PacketType::Broadcast,
1204            PacketType::MacAck,
1205        ] {
1206            assert!(!PayloadType::NodeManagement.allowed_for(packet_type));
1207        }
1208        assert!(PayloadType::TextMessage.allowed_for(PacketType::Multicast));
1209        assert!(PayloadType::CoapOverUmsh.allowed_for(PacketType::Multicast));
1210    }
1211
1212    // Reference address from the addressing chapter discussion; first byte 0x5e.
1213    const EXAMPLE_B58: &str = "7NeD1xuPGwZgikCNUaPhmMB13miitwoAEYdzhkvQVu9o";
1214
1215    #[test]
1216    fn public_key_display_matches_reference_vectors() {
1217        assert_eq!(
1218            PublicKey([0u8; 32]).to_string(),
1219            "11111111111111111111111111111111111111111111"
1220        );
1221        assert_eq!(
1222            PublicKey([0xFFu8; 32]).to_string(),
1223            "JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"
1224        );
1225    }
1226
1227    #[test]
1228    fn public_key_parses_base58_and_base16() {
1229        let key: PublicKey = EXAMPLE_B58.parse().unwrap();
1230        assert_eq!(key.to_string(), EXAMPLE_B58);
1231        assert_eq!(key.0[0], 0x5E);
1232
1233        let lower = format!("{key:x}");
1234        let upper = format!("{key:X}");
1235        assert_eq!(lower.len(), 64);
1236        assert_eq!(lower.parse::<PublicKey>().unwrap(), key);
1237        assert_eq!(upper.parse::<PublicKey>().unwrap(), key);
1238    }
1239
1240    #[test]
1241    fn public_key_parse_rejects_bad_input() {
1242        assert_eq!(
1243            "7NeD".parse::<PublicKey>(),
1244            Err(AddressParseError::InvalidLength)
1245        );
1246        let bad_hex = "gg".repeat(32);
1247        assert_eq!(
1248            bad_hex.parse::<PublicKey>(),
1249            Err(AddressParseError::InvalidCharacter)
1250        );
1251    }
1252
1253    #[test]
1254    fn node_hint_display_matches_reference_vectors() {
1255        // Unambiguous: all four characters are pinned down.
1256        assert_eq!(NodeHint([0x00, 0x00, 0x00]).to_string(), "1111");
1257        assert_eq!(NodeHint([0xFF, 0xFF, 0xFF]).to_string(), "JEKN");
1258        assert_eq!(NodeHint([0xA1, 0xB2, 0x03]).to_string(), "BtC5");
1259        assert_eq!(NodeHint([0x5E, 0xA1, 0xB2]).to_string(), "7NQL");
1260        // Carry case: the third digit is ambiguous (encodings read 9vEz/9vF1).
1261        assert_eq!(NodeHint([0x84, 0x81, 0x1B]).to_string(), "9v*");
1262        assert_eq!(NodeHint([0xFF, 0xFF, 0x94]).to_string(), "JE*");
1263    }
1264
1265    #[test]
1266    fn router_hint_display_matches_reference_vectors() {
1267        assert_eq!(RouterHint([0x00, 0x00]).to_string(), "111");
1268        assert_eq!(RouterHint([0xA1, 0xB2]).to_string(), "BtC");
1269        assert_eq!(RouterHint([0x5E, 0xA1]).to_string(), "7N*");
1270        assert_eq!(RouterHint([0x84, 0x81]).to_string(), "9v*");
1271        // Carry case: only the first digit is pinned down.
1272        assert_eq!(RouterHint([0x00, 0x41]).to_string(), "1*");
1273    }
1274
1275    #[test]
1276    fn hint_renderings_prefix_full_address() {
1277        // Deterministic sweep: every character before a `*` must match the
1278        // full base58 rendering of a key that carries the hint.
1279        for i in 0..=255u32 {
1280            let mut key = [0u8; 32];
1281            for (j, byte) in key.iter_mut().enumerate() {
1282                *byte = (i.wrapping_mul(151).wrapping_add(j as u32 * 91)) as u8;
1283            }
1284            let key = PublicKey(key);
1285            let full = key.to_string();
1286            for rendered in [key.hint().to_string(), key.router_hint().to_string()] {
1287                let verified = rendered.strip_suffix('*').unwrap_or(&rendered);
1288                assert!(
1289                    full.starts_with(verified),
1290                    "hint {rendered} does not prefix {full}"
1291                );
1292            }
1293        }
1294    }
1295}