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