Skip to main content

rns_core/
packet.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use crate::constants;
5
6#[derive(Debug)]
7pub enum PacketError {
8    TooShort,
9    ExceedsMtu,
10    MissingTransportId,
11    InvalidHeaderType,
12    ZeroLengthData,
13    /// Hop counts at or above PATHFINDER_M are invalid on received wire packets.
14    InvalidHopCount(u8),
15}
16
17impl fmt::Display for PacketError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            PacketError::TooShort => write!(f, "Packet too short"),
21            PacketError::ExceedsMtu => write!(f, "Packet exceeds MTU"),
22            PacketError::MissingTransportId => write!(f, "HEADER_2 requires transport_id"),
23            PacketError::InvalidHeaderType => write!(f, "Invalid header type"),
24            PacketError::ZeroLengthData => write!(f, "Zero-length data field"),
25            PacketError::InvalidHopCount(hops) => write!(f, "Invalid hop count: {}", hops),
26        }
27    }
28}
29
30// =============================================================================
31// PacketFlags: packs 5 fields into one byte
32// =============================================================================
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct PacketFlags {
36    pub header_type: u8,
37    pub context_flag: u8,
38    pub transport_type: u8,
39    pub destination_type: u8,
40    pub packet_type: u8,
41}
42
43impl PacketFlags {
44    /// Pack fields into a single flags byte.
45    ///
46    /// Bit layout:
47    /// ```text
48    /// Bit 6:     header_type (1 bit)
49    /// Bit 5:     context_flag (1 bit)
50    /// Bit 4:     transport_type (1 bit)
51    /// Bits 3-2:  destination_type (2 bits)
52    /// Bits 1-0:  packet_type (2 bits)
53    /// ```
54    pub fn pack(&self) -> u8 {
55        (self.header_type << 6)
56            | (self.context_flag << 5)
57            | (self.transport_type << 4)
58            | (self.destination_type << 2)
59            | self.packet_type
60    }
61
62    /// Unpack a flags byte into fields.
63    pub fn unpack(byte: u8) -> Self {
64        PacketFlags {
65            header_type: (byte & 0b01000000) >> 6,
66            context_flag: (byte & 0b00100000) >> 5,
67            transport_type: (byte & 0b00010000) >> 4,
68            destination_type: (byte & 0b00001100) >> 2,
69            packet_type: byte & 0b00000011,
70        }
71    }
72}
73
74// =============================================================================
75// RawPacket: wire-level packet representation
76// =============================================================================
77
78#[derive(Debug, Clone)]
79pub struct RawPacket {
80    pub flags: PacketFlags,
81    pub hops: u8,
82    pub transport_id: Option<[u8; 16]>,
83    pub destination_hash: [u8; 16],
84    pub context: u8,
85    pub raw: Vec<u8>,
86    pub packet_hash: [u8; 32],
87    pub rssi: Option<i16>,
88    pub snr: Option<f32>,
89}
90
91impl RawPacket {
92    /// Offset of the payload in the wire bytes of a packet with this header type.
93    #[inline]
94    pub const fn data_offset(header_type: u8) -> usize {
95        if header_type == constants::HEADER_2 {
96            2 + 2 * (constants::TRUNCATED_HASHLENGTH / 8) + 1
97        } else {
98            2 + constants::TRUNCATED_HASHLENGTH / 8 + 1
99        }
100    }
101
102    /// The payload: the wire bytes after the header.
103    #[inline]
104    pub fn data(&self) -> &[u8] {
105        &self.raw[Self::data_offset(self.flags.header_type)..]
106    }
107
108    /// Whether this packet is a part of a link resource. Nothing reads such
109    /// a packet's hash: the packet filter admits parts before deduplication,
110    /// no proof or receipt covers them, and the resource receiver locates a
111    /// part by its map hash. A part's hash cannot mark any other packet as a
112    /// duplicate either, since the context byte is hashed.
113    #[inline]
114    pub fn is_link_resource_part(&self) -> bool {
115        Self::is_link_resource_part_header(&self.flags, self.context)
116    }
117
118    /// [`RawPacket::is_link_resource_part`] for wire bytes that are not
119    /// unpacked; `false` when they are too short to carry a context byte.
120    pub fn is_link_resource_part_wire(raw: &[u8]) -> bool {
121        let Some(&flags) = raw.first() else {
122            return false;
123        };
124        let flags = PacketFlags::unpack(flags);
125        raw.get(Self::data_offset(flags.header_type) - 1)
126            .is_some_and(|&context| Self::is_link_resource_part_header(&flags, context))
127    }
128
129    /// Whether a link peer never proves this packet: link data in a context
130    /// with its own protocol handling (requests and responses, resource
131    /// control and parts, keepalives, identification, RTT and close). Only
132    /// plain link data and channel messages are proven, so a sent packet in
133    /// one of these contexts has no receipt to wait for.
134    pub fn is_unproven_link_data(&self) -> bool {
135        self.flags.packet_type == constants::PACKET_TYPE_DATA
136            && self.flags.destination_type == constants::DESTINATION_LINK
137            && matches!(
138                self.context,
139                constants::CONTEXT_REQUEST
140                    | constants::CONTEXT_RESPONSE
141                    | constants::CONTEXT_RESOURCE
142                    | constants::CONTEXT_RESOURCE_ADV
143                    | constants::CONTEXT_RESOURCE_REQ
144                    | constants::CONTEXT_RESOURCE_HMU
145                    | constants::CONTEXT_RESOURCE_ICL
146                    | constants::CONTEXT_RESOURCE_RCL
147                    | constants::CONTEXT_KEEPALIVE
148                    | constants::CONTEXT_LINKIDENTIFY
149                    | constants::CONTEXT_LRRTT
150                    | constants::CONTEXT_LINKCLOSE
151            )
152    }
153
154    #[inline]
155    fn is_link_resource_part_header(flags: &PacketFlags, context: u8) -> bool {
156        flags.packet_type == constants::PACKET_TYPE_DATA
157            && flags.destination_type == constants::DESTINATION_LINK
158            && context == constants::CONTEXT_RESOURCE
159    }
160
161    /// Pack fields into raw bytes.
162    pub fn pack(
163        flags: PacketFlags,
164        hops: u8,
165        destination_hash: &[u8; 16],
166        transport_id: Option<&[u8; 16]>,
167        context: u8,
168        data: &[u8],
169    ) -> Result<Self, PacketError> {
170        Self::pack_with_max_mtu(
171            flags,
172            hops,
173            destination_hash,
174            transport_id,
175            context,
176            data,
177            constants::MTU,
178        )
179    }
180
181    /// Pack fields into raw bytes and packet hash without constructing a full RawPacket.
182    pub fn pack_raw_with_hash(
183        flags: PacketFlags,
184        hops: u8,
185        destination_hash: &[u8; 16],
186        transport_id: Option<&[u8; 16]>,
187        context: u8,
188        data: &[u8],
189    ) -> Result<(Vec<u8>, [u8; 32]), PacketError> {
190        Self::pack_raw_with_hash_with_max_mtu(
191            flags,
192            hops,
193            destination_hash,
194            transport_id,
195            context,
196            data,
197            constants::MTU,
198        )
199    }
200
201    /// Pack fields into raw bytes with a caller-provided MTU limit.
202    pub fn pack_with_max_mtu(
203        flags: PacketFlags,
204        hops: u8,
205        destination_hash: &[u8; 16],
206        transport_id: Option<&[u8; 16]>,
207        context: u8,
208        data: &[u8],
209        max_mtu: usize,
210    ) -> Result<Self, PacketError> {
211        let (raw, packet_hash) = Self::pack_raw_with_hash_with_max_mtu(
212            flags,
213            hops,
214            destination_hash,
215            transport_id,
216            context,
217            data,
218            max_mtu,
219        )?;
220
221        Ok(RawPacket {
222            flags,
223            hops,
224            transport_id: transport_id.copied(),
225            destination_hash: *destination_hash,
226            context,
227            raw,
228            packet_hash,
229            rssi: None,
230            snr: None,
231        })
232    }
233
234    /// Pack fields into raw bytes and packet hash with a caller-provided MTU limit.
235    pub fn pack_raw_with_hash_with_max_mtu(
236        flags: PacketFlags,
237        hops: u8,
238        destination_hash: &[u8; 16],
239        transport_id: Option<&[u8; 16]>,
240        context: u8,
241        data: &[u8],
242        max_mtu: usize,
243    ) -> Result<(Vec<u8>, [u8; 32]), PacketError> {
244        let raw = Self::pack_raw_with_max_mtu(
245            flags,
246            hops,
247            destination_hash,
248            transport_id,
249            context,
250            data,
251            max_mtu,
252        )?;
253        let packet_hash = Self::compute_packet_hash(flags.header_type, &raw);
254        Ok((raw, packet_hash))
255    }
256
257    /// Pack only wire bytes when the caller does not need a packet hash.
258    /// Validation and encoding match the hash-producing packing methods.
259    pub fn pack_raw_with_max_mtu(
260        flags: PacketFlags,
261        hops: u8,
262        destination_hash: &[u8; 16],
263        transport_id: Option<&[u8; 16]>,
264        context: u8,
265        data: &[u8],
266        max_mtu: usize,
267    ) -> Result<Vec<u8>, PacketError> {
268        if flags.header_type == constants::HEADER_2 && transport_id.is_none() {
269            return Err(PacketError::MissingTransportId);
270        }
271
272        let mut raw = Vec::new();
273        raw.push(flags.pack());
274        raw.push(hops);
275
276        if let Some(transport_id) = transport_id {
277            if flags.header_type == constants::HEADER_2 {
278                raw.extend_from_slice(transport_id);
279            }
280        }
281
282        raw.extend_from_slice(destination_hash);
283        raw.push(context);
284        raw.extend_from_slice(data);
285
286        if raw.len() > max_mtu {
287            return Err(PacketError::ExceedsMtu);
288        }
289
290        Ok(raw)
291    }
292
293    /// Unpack raw bytes into fields.
294    pub fn unpack(raw: &[u8]) -> Result<Self, PacketError> {
295        Self::unpack_with::<true, _>(raw)
296    }
297
298    /// Unpack wire bytes the caller owns, keeping them as `raw` instead of
299    /// copying them.
300    pub fn unpack_owned(raw: Vec<u8>) -> Result<Self, PacketError> {
301        Self::unpack_with::<true, _>(raw)
302    }
303
304    /// Unpack owned wire bytes into fields, leaving `packet_hash` zeroed.
305    /// Callers must hash the packet with [`RawPacket::hash_in_place`] before
306    /// anything reads `packet_hash`.
307    pub(crate) fn unpack_unhashed(raw: Vec<u8>) -> Result<Self, PacketError> {
308        Self::unpack_with::<false, _>(raw)
309    }
310
311    /// Unpack owned wire bytes of a packet this node sends. A link resource
312    /// part is left unhashed, its `packet_hash` zeroed, because nothing reads
313    /// it (see [`RawPacket::is_link_resource_part`]).
314    pub fn unpack_outbound(raw: Vec<u8>) -> Result<Self, PacketError> {
315        let mut packet = Self::unpack_unhashed(raw)?;
316        if !packet.is_link_resource_part() {
317            packet.hash_in_place();
318        }
319        Ok(packet)
320    }
321
322    /// Unpack wire bytes whose packet hash the caller already computed from
323    /// these exact bytes (for example a delivered packet's hash), instead of
324    /// hashing them again. The hash is trusted, not checked.
325    pub fn unpack_prehashed(raw: &[u8], packet_hash: [u8; 32]) -> Result<Self, PacketError> {
326        let mut packet = Self::unpack_with::<false, _>(raw)?;
327        packet.packet_hash = packet_hash;
328        Ok(packet)
329    }
330
331    fn unpack_with<const HASH: bool, B>(input: B) -> Result<Self, PacketError>
332    where
333        B: AsRef<[u8]> + Into<Vec<u8>>,
334    {
335        let raw = input.as_ref();
336        if raw.len() < constants::HEADER_MINSIZE {
337            return Err(PacketError::TooShort);
338        }
339
340        let flags = PacketFlags::unpack(raw[0]);
341        let hops = raw[1];
342        if hops >= constants::PATHFINDER_M {
343            return Err(PacketError::InvalidHopCount(hops));
344        }
345
346        let dst_len = constants::TRUNCATED_HASHLENGTH / 8; // 16
347
348        if flags.header_type == constants::HEADER_2 {
349            // HEADER_2: [flags:1][hops:1][transport_id:16][dest_hash:16][context:1][data:*]
350            let min_len = 2 + dst_len * 2 + 1;
351            if raw.len() < min_len {
352                return Err(PacketError::TooShort);
353            }
354
355            let mut transport_id = [0u8; 16];
356            transport_id.copy_from_slice(&raw[2..2 + dst_len]);
357
358            let mut destination_hash = [0u8; 16];
359            destination_hash.copy_from_slice(&raw[2 + dst_len..2 + 2 * dst_len]);
360
361            let context = raw[2 + 2 * dst_len];
362            if raw.len() == 2 + 2 * dst_len + 1 {
363                return Err(PacketError::ZeroLengthData);
364            }
365
366            let packet_hash = if HASH {
367                Self::compute_packet_hash(flags.header_type, raw)
368            } else {
369                [0; 32]
370            };
371
372            Ok(RawPacket {
373                flags,
374                hops,
375                transport_id: Some(transport_id),
376                destination_hash,
377                context,
378                raw: input.into(),
379                packet_hash,
380                rssi: None,
381                snr: None,
382            })
383        } else if flags.header_type == constants::HEADER_1 {
384            // HEADER_1: [flags:1][hops:1][dest_hash:16][context:1][data:*]
385            let min_len = 2 + dst_len + 1;
386            if raw.len() < min_len {
387                return Err(PacketError::TooShort);
388            }
389
390            let mut destination_hash = [0u8; 16];
391            destination_hash.copy_from_slice(&raw[2..2 + dst_len]);
392
393            let context = raw[2 + dst_len];
394            if raw.len() == 2 + dst_len + 1 {
395                return Err(PacketError::ZeroLengthData);
396            }
397
398            let packet_hash = if HASH {
399                Self::compute_packet_hash(flags.header_type, raw)
400            } else {
401                [0; 32]
402            };
403
404            Ok(RawPacket {
405                flags,
406                hops,
407                transport_id: None,
408                destination_hash,
409                context,
410                raw: input.into(),
411                packet_hash,
412                rssi: None,
413                snr: None,
414            })
415        } else {
416            Err(PacketError::InvalidHeaderType)
417        }
418    }
419
420    /// Get the hashable part of the packet.
421    ///
422    /// From Python Packet.py:354-361:
423    /// - Take raw[0] & 0x0F (mask out upper 4 bits of flags)
424    /// - For HEADER_1: append raw[2:]
425    /// - For HEADER_2: skip transport_id: append raw[18:]
426    pub fn get_hashable_part(&self) -> Vec<u8> {
427        Self::compute_hashable_part(self.flags.header_type, &self.raw)
428    }
429
430    fn hashable_tail(header_type: u8, raw: &[u8]) -> &[u8] {
431        if header_type == constants::HEADER_2 {
432            // Skip transport_id: raw[2..18] is transport_id (16 bytes)
433            &raw[(constants::TRUNCATED_HASHLENGTH / 8 + 2)..]
434        } else {
435            &raw[2..]
436        }
437    }
438
439    fn compute_hashable_part(header_type: u8, raw: &[u8]) -> Vec<u8> {
440        let tail = Self::hashable_tail(header_type, raw);
441        let mut hashable = Vec::with_capacity(1 + tail.len());
442        hashable.push(raw[0] & 0b00001111);
443        hashable.extend_from_slice(tail);
444        hashable
445    }
446
447    /// SHA-256 of the hashable part, fed from the wire bytes without building
448    /// the concatenated buffer that [`RawPacket::get_hashable_part`] returns.
449    fn compute_packet_hash(header_type: u8, raw: &[u8]) -> [u8; 32] {
450        let mut hasher = rns_crypto::sha256::Sha256::new();
451        hasher.update(&[raw[0] & 0b00001111]);
452        hasher.update(Self::hashable_tail(header_type, raw));
453        hasher.digest()
454    }
455
456    /// Compute `packet_hash` from the packet's wire bytes.
457    pub(crate) fn hash_in_place(&mut self) {
458        self.packet_hash = Self::compute_packet_hash(self.flags.header_type, &self.raw);
459    }
460
461    /// Full SHA-256 hash of the hashable part.
462    pub fn get_hash(&self) -> [u8; 32] {
463        self.packet_hash
464    }
465
466    /// Truncated hash (first 16 bytes) of the cached packet hash.
467    ///
468    /// Packet construction and unpacking compute `packet_hash` once; routing
469    /// paths use this prefix instead of hashing the raw packet again.
470    pub fn get_truncated_hash(&self) -> [u8; 16] {
471        let mut result = [0u8; 16];
472        result.copy_from_slice(&self.packet_hash[..16]);
473        result
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn unpack_prehashed_matches_unpack_but_keeps_the_given_hash() {
483        let flags = PacketFlags {
484            header_type: constants::HEADER_1,
485            context_flag: constants::FLAG_UNSET,
486            transport_type: constants::TRANSPORT_BROADCAST,
487            destination_type: constants::DESTINATION_LINK,
488            packet_type: constants::PACKET_TYPE_DATA,
489        };
490        let packet = RawPacket::pack(
491            flags,
492            3,
493            &[0x42; 16],
494            None,
495            constants::CONTEXT_RESOURCE,
496            &[9; 300],
497        )
498        .unwrap();
499        let full = RawPacket::unpack(&packet.raw).unwrap();
500        let known = RawPacket::unpack_prehashed(&packet.raw, full.packet_hash).unwrap();
501        assert_eq!(known.raw, full.raw);
502        assert_eq!(known.packet_hash, full.packet_hash);
503        assert_eq!(known.destination_hash, full.destination_hash);
504        assert_eq!((known.hops, known.context), (full.hops, full.context));
505        assert_eq!(known.data(), full.data());
506        // The hash is trusted, not recomputed.
507        let other = RawPacket::unpack_prehashed(&packet.raw, [7; 32]).unwrap();
508        assert_eq!(other.packet_hash, [7; 32]);
509        // Structural errors are the same.
510        assert_eq!(
511            format!(
512                "{:?}",
513                RawPacket::unpack_prehashed(&packet.raw[..10], [0; 32]).unwrap_err()
514            ),
515            format!("{:?}", RawPacket::unpack(&packet.raw[..10]).unwrap_err())
516        );
517    }
518
519    #[test]
520    fn only_link_resource_parts_are_sent_unhashed() {
521        let cases = [
522            (
523                constants::DESTINATION_LINK,
524                constants::PACKET_TYPE_DATA,
525                constants::CONTEXT_RESOURCE,
526                true,
527            ),
528            (
529                constants::DESTINATION_LINK,
530                constants::PACKET_TYPE_DATA,
531                constants::CONTEXT_CHANNEL,
532                false,
533            ),
534            (
535                constants::DESTINATION_LINK,
536                constants::PACKET_TYPE_DATA,
537                constants::CONTEXT_RESOURCE_REQ,
538                false,
539            ),
540            (
541                constants::DESTINATION_LINK,
542                constants::PACKET_TYPE_PROOF,
543                constants::CONTEXT_RESOURCE,
544                false,
545            ),
546            (
547                constants::DESTINATION_SINGLE,
548                constants::PACKET_TYPE_DATA,
549                constants::CONTEXT_RESOURCE,
550                false,
551            ),
552        ];
553        for header_type in [constants::HEADER_1, constants::HEADER_2] {
554            for (destination_type, packet_type, context, part) in cases {
555                let flags = PacketFlags {
556                    header_type,
557                    context_flag: constants::FLAG_UNSET,
558                    transport_type: constants::TRANSPORT_BROADCAST,
559                    destination_type,
560                    packet_type,
561                };
562                let transport_id = (header_type == constants::HEADER_2).then_some([0x11; 16]);
563                let packet = RawPacket::pack(
564                    flags,
565                    0,
566                    &[0x42; 16],
567                    transport_id.as_ref(),
568                    context,
569                    &[9; 300],
570                )
571                .unwrap();
572                assert_eq!(packet.is_link_resource_part(), part);
573                assert_eq!(RawPacket::is_link_resource_part_wire(&packet.raw), part);
574                let sent = RawPacket::unpack_outbound(packet.raw.clone()).unwrap();
575                assert_eq!(sent.raw, packet.raw);
576                let expected = if part { [0; 32] } else { packet.packet_hash };
577                assert_eq!(sent.packet_hash, expected);
578            }
579        }
580        // Bytes too short to carry a context byte are not a part.
581        let part = RawPacket::pack(
582            PacketFlags {
583                header_type: constants::HEADER_1,
584                context_flag: constants::FLAG_UNSET,
585                transport_type: constants::TRANSPORT_BROADCAST,
586                destination_type: constants::DESTINATION_LINK,
587                packet_type: constants::PACKET_TYPE_DATA,
588            },
589            0,
590            &[0x42; 16],
591            None,
592            constants::CONTEXT_RESOURCE,
593            &[9; 4],
594        )
595        .unwrap();
596        let context_at = RawPacket::data_offset(constants::HEADER_1) - 1;
597        assert!(RawPacket::is_link_resource_part_wire(
598            &part.raw[..=context_at]
599        ));
600        assert!(!RawPacket::is_link_resource_part_wire(
601            &part.raw[..context_at]
602        ));
603        assert!(!RawPacket::is_link_resource_part_wire(&[]));
604    }
605
606    #[test]
607    fn only_plain_link_data_and_channel_messages_await_proofs() {
608        let packet = |destination_type, packet_type, context| {
609            RawPacket::pack(
610                PacketFlags {
611                    header_type: constants::HEADER_1,
612                    context_flag: constants::FLAG_UNSET,
613                    transport_type: constants::TRANSPORT_BROADCAST,
614                    destination_type,
615                    packet_type,
616                },
617                0,
618                &[0x42; 16],
619                None,
620                context,
621                b"payload",
622            )
623            .unwrap()
624        };
625        let link_data = |context| {
626            packet(
627                constants::DESTINATION_LINK,
628                constants::PACKET_TYPE_DATA,
629                context,
630            )
631        };
632        for context in [constants::CONTEXT_NONE, constants::CONTEXT_CHANNEL] {
633            assert!(
634                !link_data(context).is_unproven_link_data(),
635                "context {context}"
636            );
637        }
638        for context in [
639            constants::CONTEXT_REQUEST,
640            constants::CONTEXT_RESPONSE,
641            constants::CONTEXT_RESOURCE,
642            constants::CONTEXT_RESOURCE_ADV,
643            constants::CONTEXT_RESOURCE_REQ,
644            constants::CONTEXT_RESOURCE_HMU,
645            constants::CONTEXT_RESOURCE_ICL,
646            constants::CONTEXT_RESOURCE_RCL,
647            constants::CONTEXT_KEEPALIVE,
648            constants::CONTEXT_LINKIDENTIFY,
649            constants::CONTEXT_LRRTT,
650            constants::CONTEXT_LINKCLOSE,
651        ] {
652            assert!(
653                link_data(context).is_unproven_link_data(),
654                "context {context}"
655            );
656        }
657        // Only link data qualifies.
658        let request = constants::CONTEXT_REQUEST;
659        assert!(!packet(
660            constants::DESTINATION_SINGLE,
661            constants::PACKET_TYPE_DATA,
662            request
663        )
664        .is_unproven_link_data());
665        assert!(!packet(
666            constants::DESTINATION_LINK,
667            constants::PACKET_TYPE_PROOF,
668            request
669        )
670        .is_unproven_link_data());
671    }
672
673    #[test]
674    fn raw_only_packing_preserves_wire_layout_and_limits() {
675        for header in [constants::HEADER_1, constants::HEADER_2] {
676            for transport in [None, Some([0xBB; 16])] {
677                for len in [0, 1, 464, 16348] {
678                    let flags = PacketFlags {
679                        header_type: header,
680                        context_flag: constants::FLAG_UNSET,
681                        transport_type: constants::TRANSPORT_BROADCAST,
682                        destination_type: constants::DESTINATION_LINK,
683                        packet_type: constants::PACKET_TYPE_DATA,
684                    };
685                    let data = vec![0xCC; len];
686                    let mut expected = vec![flags.pack(), 7];
687                    if header == constants::HEADER_2 {
688                        expected.extend_from_slice(&[0xBB; 16]);
689                    }
690                    expected.extend_from_slice(&[0xAA; 16]);
691                    expected.push(constants::CONTEXT_RESOURCE);
692                    expected.extend_from_slice(&data);
693                    for limit in [0, expected.len() - 1, expected.len(), expected.len() + 1] {
694                        let result = RawPacket::pack_raw_with_max_mtu(
695                            flags,
696                            7,
697                            &[0xAA; 16],
698                            transport.as_ref(),
699                            constants::CONTEXT_RESOURCE,
700                            &data,
701                            limit,
702                        );
703                        if header == constants::HEADER_2 && transport.is_none() {
704                            assert!(matches!(result, Err(PacketError::MissingTransportId)));
705                        } else if limit < expected.len() {
706                            assert!(matches!(result, Err(PacketError::ExceedsMtu)));
707                        } else {
708                            assert_eq!(result.unwrap(), expected);
709                            let (raw, hash) = RawPacket::pack_raw_with_hash_with_max_mtu(
710                                flags,
711                                7,
712                                &[0xAA; 16],
713                                transport.as_ref(),
714                                constants::CONTEXT_RESOURCE,
715                                &data,
716                                limit,
717                            )
718                            .unwrap();
719                            assert_eq!(raw, expected);
720                            if len == 0 {
721                                assert!(matches!(
722                                    RawPacket::unpack(&raw),
723                                    Err(PacketError::ZeroLengthData)
724                                ));
725                            } else {
726                                assert_eq!(RawPacket::unpack(&raw).unwrap().packet_hash, hash);
727                            }
728                        }
729                    }
730                }
731            }
732        }
733    }
734
735    #[test]
736    fn test_flags_pack_header1_data_single_broadcast() {
737        let flags = PacketFlags {
738            header_type: constants::HEADER_1,
739            context_flag: constants::FLAG_UNSET,
740            transport_type: constants::TRANSPORT_BROADCAST,
741            destination_type: constants::DESTINATION_SINGLE,
742            packet_type: constants::PACKET_TYPE_DATA,
743        };
744        assert_eq!(flags.pack(), 0x00);
745    }
746
747    #[test]
748    fn test_flags_pack_header2_announce_single_transport() {
749        let flags = PacketFlags {
750            header_type: constants::HEADER_2,
751            context_flag: constants::FLAG_UNSET,
752            transport_type: constants::TRANSPORT_TRANSPORT,
753            destination_type: constants::DESTINATION_SINGLE,
754            packet_type: constants::PACKET_TYPE_ANNOUNCE,
755        };
756        // 0b01010001 = 0x51
757        assert_eq!(flags.pack(), 0x51);
758    }
759
760    #[test]
761    fn test_flags_roundtrip() {
762        for byte in 0..=0x7Fu8 {
763            let flags = PacketFlags::unpack(byte);
764            assert_eq!(flags.pack(), byte);
765        }
766    }
767
768    #[test]
769    fn test_pack_header1() {
770        let dest_hash = [0xAA; 16];
771        let data = b"hello";
772        let flags = PacketFlags {
773            header_type: constants::HEADER_1,
774            context_flag: constants::FLAG_UNSET,
775            transport_type: constants::TRANSPORT_BROADCAST,
776            destination_type: constants::DESTINATION_SINGLE,
777            packet_type: constants::PACKET_TYPE_DATA,
778        };
779
780        let pkt =
781            RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, data).unwrap();
782
783        // Verify layout: [flags:1][hops:1][dest:16][context:1][data:5] = 24 bytes
784        assert_eq!(pkt.raw.len(), 24);
785        assert_eq!(pkt.raw[0], 0x00); // flags
786        assert_eq!(pkt.raw[1], 0x00); // hops
787        assert_eq!(&pkt.raw[2..18], &dest_hash); // dest hash
788        assert_eq!(pkt.raw[18], 0x00); // context
789        assert_eq!(&pkt.raw[19..], b"hello"); // data
790    }
791
792    #[test]
793    fn test_pack_header2() {
794        let dest_hash = [0xAA; 16];
795        let transport_id = [0xBB; 16];
796        let data = b"world";
797        let flags = PacketFlags {
798            header_type: constants::HEADER_2,
799            context_flag: constants::FLAG_UNSET,
800            transport_type: constants::TRANSPORT_TRANSPORT,
801            destination_type: constants::DESTINATION_SINGLE,
802            packet_type: constants::PACKET_TYPE_ANNOUNCE,
803        };
804
805        let pkt = RawPacket::pack(
806            flags,
807            3,
808            &dest_hash,
809            Some(&transport_id),
810            constants::CONTEXT_NONE,
811            data,
812        )
813        .unwrap();
814
815        // Layout: [flags:1][hops:1][transport:16][dest:16][context:1][data:5] = 40 bytes
816        assert_eq!(pkt.raw.len(), 40);
817        assert_eq!(pkt.raw[0], flags.pack());
818        assert_eq!(pkt.raw[1], 3);
819        assert_eq!(&pkt.raw[2..18], &transport_id);
820        assert_eq!(&pkt.raw[18..34], &dest_hash);
821        assert_eq!(pkt.raw[34], 0x00);
822        assert_eq!(&pkt.raw[35..], b"world");
823    }
824
825    #[test]
826    fn test_unpack_roundtrip_header1() {
827        let dest_hash = [0x11; 16];
828        let data = b"test data";
829        let flags = PacketFlags {
830            header_type: constants::HEADER_1,
831            context_flag: constants::FLAG_UNSET,
832            transport_type: constants::TRANSPORT_BROADCAST,
833            destination_type: constants::DESTINATION_SINGLE,
834            packet_type: constants::PACKET_TYPE_DATA,
835        };
836
837        let pkt = RawPacket::pack(
838            flags,
839            5,
840            &dest_hash,
841            None,
842            constants::CONTEXT_RESOURCE,
843            data,
844        )
845        .unwrap();
846        let unpacked = RawPacket::unpack(&pkt.raw).unwrap();
847
848        assert_eq!(unpacked.flags, flags);
849        assert_eq!(unpacked.hops, 5);
850        assert!(unpacked.transport_id.is_none());
851        assert_eq!(unpacked.destination_hash, dest_hash);
852        assert_eq!(unpacked.context, constants::CONTEXT_RESOURCE);
853        assert_eq!(unpacked.data(), data);
854        assert_eq!(unpacked.packet_hash, pkt.packet_hash);
855    }
856
857    #[test]
858    fn test_unpack_roundtrip_header2() {
859        let dest_hash = [0x22; 16];
860        let transport_id = [0x33; 16];
861        let data = b"transported";
862        let flags = PacketFlags {
863            header_type: constants::HEADER_2,
864            context_flag: constants::FLAG_SET,
865            transport_type: constants::TRANSPORT_TRANSPORT,
866            destination_type: constants::DESTINATION_SINGLE,
867            packet_type: constants::PACKET_TYPE_ANNOUNCE,
868        };
869
870        let pkt = RawPacket::pack(
871            flags,
872            2,
873            &dest_hash,
874            Some(&transport_id),
875            constants::CONTEXT_NONE,
876            data,
877        )
878        .unwrap();
879        let unpacked = RawPacket::unpack(&pkt.raw).unwrap();
880
881        assert_eq!(unpacked.flags, flags);
882        assert_eq!(unpacked.hops, 2);
883        assert_eq!(unpacked.transport_id.unwrap(), transport_id);
884        assert_eq!(unpacked.destination_hash, dest_hash);
885        assert_eq!(unpacked.context, constants::CONTEXT_NONE);
886        assert_eq!(unpacked.data(), data);
887        assert_eq!(unpacked.packet_hash, pkt.packet_hash);
888    }
889
890    #[test]
891    fn truncated_hash_is_derived_from_cached_packet_hash() {
892        let mut packet = RawPacket::pack(
893            PacketFlags {
894                header_type: constants::HEADER_1,
895                context_flag: constants::FLAG_UNSET,
896                transport_type: constants::TRANSPORT_BROADCAST,
897                destination_type: constants::DESTINATION_SINGLE,
898                packet_type: constants::PACKET_TYPE_DATA,
899            },
900            1,
901            &[0x42; 16],
902            None,
903            constants::CONTEXT_NONE,
904            b"cached hash",
905        )
906        .unwrap();
907        let expected: [u8; 16] = packet.packet_hash[..16].try_into().unwrap();
908
909        // Changing the retained wire buffer demonstrates that this accessor is
910        // a prefix operation, not a second hash computation.
911        let last = packet.raw.len() - 1;
912        packet.raw[last] ^= 0xff;
913
914        assert_eq!(packet.get_truncated_hash(), expected);
915    }
916
917    #[test]
918    fn test_unpack_too_short() {
919        assert!(RawPacket::unpack(&[0x00; 5]).is_err());
920    }
921
922    #[test]
923    fn unpack_rejects_zero_length_data_for_both_header_types() {
924        for header_type in [constants::HEADER_1, constants::HEADER_2] {
925            let packet = RawPacket::pack(
926                PacketFlags {
927                    header_type,
928                    context_flag: constants::FLAG_UNSET,
929                    transport_type: constants::TRANSPORT_BROADCAST,
930                    destination_type: constants::DESTINATION_SINGLE,
931                    packet_type: constants::PACKET_TYPE_DATA,
932                },
933                0,
934                &[0x42; 16],
935                (header_type == constants::HEADER_2).then_some(&[0x24; 16]),
936                constants::CONTEXT_NONE,
937                b"",
938            )
939            .unwrap();
940            assert!(matches!(
941                RawPacket::unpack(&packet.raw),
942                Err(PacketError::ZeroLengthData)
943            ));
944        }
945    }
946
947    #[test]
948    fn unpack_enforces_pathfinder_hop_boundary_but_pack_remains_permissive() {
949        let flags = PacketFlags {
950            header_type: constants::HEADER_1,
951            context_flag: constants::FLAG_UNSET,
952            transport_type: constants::TRANSPORT_BROADCAST,
953            destination_type: constants::DESTINATION_SINGLE,
954            packet_type: constants::PACKET_TYPE_DATA,
955        };
956        let accepted = RawPacket::pack(flags, 127, &[1; 16], None, 0, b"x").unwrap();
957        assert_eq!(RawPacket::unpack(&accepted.raw).unwrap().hops, 127);
958
959        for hops in [128, 255] {
960            let packed = RawPacket::pack(flags, hops, &[1; 16], None, 0, b"x").unwrap();
961            assert!(matches!(
962                RawPacket::unpack(&packed.raw),
963                Err(PacketError::InvalidHopCount(value)) if value == hops
964            ));
965        }
966    }
967
968    #[test]
969    fn test_pack_exceeds_mtu() {
970        let flags = PacketFlags {
971            header_type: constants::HEADER_1,
972            context_flag: constants::FLAG_UNSET,
973            transport_type: constants::TRANSPORT_BROADCAST,
974            destination_type: constants::DESTINATION_SINGLE,
975            packet_type: constants::PACKET_TYPE_DATA,
976        };
977        let data = [0u8; 500]; // way too much data
978        let result = RawPacket::pack(flags, 0, &[0; 16], None, 0, &data);
979        assert!(result.is_err());
980    }
981
982    #[test]
983    fn test_header2_missing_transport_id() {
984        let flags = PacketFlags {
985            header_type: constants::HEADER_2,
986            context_flag: constants::FLAG_UNSET,
987            transport_type: constants::TRANSPORT_TRANSPORT,
988            destination_type: constants::DESTINATION_SINGLE,
989            packet_type: constants::PACKET_TYPE_ANNOUNCE,
990        };
991        let result = RawPacket::pack(flags, 0, &[0; 16], None, 0, b"data");
992        assert!(result.is_err());
993    }
994
995    #[test]
996    fn test_hashable_part_header1_masks_upper_flags() {
997        let dest_hash = [0xCC; 16];
998        let flags = PacketFlags {
999            header_type: constants::HEADER_1,
1000            context_flag: constants::FLAG_SET,
1001            transport_type: constants::TRANSPORT_BROADCAST,
1002            destination_type: constants::DESTINATION_SINGLE,
1003            packet_type: constants::PACKET_TYPE_DATA,
1004        };
1005
1006        let pkt =
1007            RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, b"test").unwrap();
1008        let hashable = pkt.get_hashable_part();
1009
1010        // First byte should have upper 4 bits masked out
1011        assert_eq!(hashable[0], pkt.raw[0] & 0x0F);
1012        // Rest should be raw[2:]
1013        assert_eq!(&hashable[1..], &pkt.raw[2..]);
1014    }
1015
1016    #[test]
1017    fn test_hashable_part_header2_strips_transport_id() {
1018        let dest_hash = [0xDD; 16];
1019        let transport_id = [0xEE; 16];
1020        let flags = PacketFlags {
1021            header_type: constants::HEADER_2,
1022            context_flag: constants::FLAG_UNSET,
1023            transport_type: constants::TRANSPORT_TRANSPORT,
1024            destination_type: constants::DESTINATION_SINGLE,
1025            packet_type: constants::PACKET_TYPE_ANNOUNCE,
1026        };
1027
1028        let pkt = RawPacket::pack(
1029            flags,
1030            0,
1031            &dest_hash,
1032            Some(&transport_id),
1033            constants::CONTEXT_NONE,
1034            b"data",
1035        )
1036        .unwrap();
1037        let hashable = pkt.get_hashable_part();
1038
1039        // First byte: flags masked
1040        assert_eq!(hashable[0], pkt.raw[0] & 0x0F);
1041        // Should skip transport_id: raw[18:] = dest_hash + context + data
1042        assert_eq!(&hashable[1..], &pkt.raw[18..]);
1043    }
1044}