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    /// Pack fields into raw bytes.
109    pub fn pack(
110        flags: PacketFlags,
111        hops: u8,
112        destination_hash: &[u8; 16],
113        transport_id: Option<&[u8; 16]>,
114        context: u8,
115        data: &[u8],
116    ) -> Result<Self, PacketError> {
117        Self::pack_with_max_mtu(
118            flags,
119            hops,
120            destination_hash,
121            transport_id,
122            context,
123            data,
124            constants::MTU,
125        )
126    }
127
128    /// Pack fields into raw bytes and packet hash without constructing a full RawPacket.
129    pub fn pack_raw_with_hash(
130        flags: PacketFlags,
131        hops: u8,
132        destination_hash: &[u8; 16],
133        transport_id: Option<&[u8; 16]>,
134        context: u8,
135        data: &[u8],
136    ) -> Result<(Vec<u8>, [u8; 32]), PacketError> {
137        Self::pack_raw_with_hash_with_max_mtu(
138            flags,
139            hops,
140            destination_hash,
141            transport_id,
142            context,
143            data,
144            constants::MTU,
145        )
146    }
147
148    /// Pack fields into raw bytes with a caller-provided MTU limit.
149    pub fn pack_with_max_mtu(
150        flags: PacketFlags,
151        hops: u8,
152        destination_hash: &[u8; 16],
153        transport_id: Option<&[u8; 16]>,
154        context: u8,
155        data: &[u8],
156        max_mtu: usize,
157    ) -> Result<Self, PacketError> {
158        let (raw, packet_hash) = Self::pack_raw_with_hash_with_max_mtu(
159            flags,
160            hops,
161            destination_hash,
162            transport_id,
163            context,
164            data,
165            max_mtu,
166        )?;
167
168        Ok(RawPacket {
169            flags,
170            hops,
171            transport_id: transport_id.copied(),
172            destination_hash: *destination_hash,
173            context,
174            raw,
175            packet_hash,
176            rssi: None,
177            snr: None,
178        })
179    }
180
181    /// Pack fields into raw bytes and packet hash with a caller-provided MTU limit.
182    pub fn pack_raw_with_hash_with_max_mtu(
183        flags: PacketFlags,
184        hops: u8,
185        destination_hash: &[u8; 16],
186        transport_id: Option<&[u8; 16]>,
187        context: u8,
188        data: &[u8],
189        max_mtu: usize,
190    ) -> Result<(Vec<u8>, [u8; 32]), PacketError> {
191        let raw = Self::pack_raw_with_max_mtu(
192            flags,
193            hops,
194            destination_hash,
195            transport_id,
196            context,
197            data,
198            max_mtu,
199        )?;
200        let packet_hash = Self::compute_packet_hash(flags.header_type, &raw);
201        Ok((raw, packet_hash))
202    }
203
204    /// Pack only wire bytes when the caller does not need a packet hash.
205    /// Validation and encoding match the hash-producing packing methods.
206    pub fn pack_raw_with_max_mtu(
207        flags: PacketFlags,
208        hops: u8,
209        destination_hash: &[u8; 16],
210        transport_id: Option<&[u8; 16]>,
211        context: u8,
212        data: &[u8],
213        max_mtu: usize,
214    ) -> Result<Vec<u8>, PacketError> {
215        if flags.header_type == constants::HEADER_2 && transport_id.is_none() {
216            return Err(PacketError::MissingTransportId);
217        }
218
219        let mut raw = Vec::new();
220        raw.push(flags.pack());
221        raw.push(hops);
222
223        if let Some(transport_id) = transport_id {
224            if flags.header_type == constants::HEADER_2 {
225                raw.extend_from_slice(transport_id);
226            }
227        }
228
229        raw.extend_from_slice(destination_hash);
230        raw.push(context);
231        raw.extend_from_slice(data);
232
233        if raw.len() > max_mtu {
234            return Err(PacketError::ExceedsMtu);
235        }
236
237        Ok(raw)
238    }
239
240    /// Unpack raw bytes into fields.
241    pub fn unpack(raw: &[u8]) -> Result<Self, PacketError> {
242        Self::unpack_with::<true, _>(raw)
243    }
244
245    /// Unpack wire bytes the caller owns, keeping them as `raw` instead of
246    /// copying them.
247    pub fn unpack_owned(raw: Vec<u8>) -> Result<Self, PacketError> {
248        Self::unpack_with::<true, _>(raw)
249    }
250
251    /// Unpack owned wire bytes into fields, leaving `packet_hash` zeroed.
252    /// Callers must hash the packet with [`RawPacket::hash_in_place`] before
253    /// anything reads `packet_hash`.
254    pub(crate) fn unpack_unhashed(raw: Vec<u8>) -> Result<Self, PacketError> {
255        Self::unpack_with::<false, _>(raw)
256    }
257
258    fn unpack_with<const HASH: bool, B>(input: B) -> Result<Self, PacketError>
259    where
260        B: AsRef<[u8]> + Into<Vec<u8>>,
261    {
262        let raw = input.as_ref();
263        if raw.len() < constants::HEADER_MINSIZE {
264            return Err(PacketError::TooShort);
265        }
266
267        let flags = PacketFlags::unpack(raw[0]);
268        let hops = raw[1];
269        if hops >= constants::PATHFINDER_M {
270            return Err(PacketError::InvalidHopCount(hops));
271        }
272
273        let dst_len = constants::TRUNCATED_HASHLENGTH / 8; // 16
274
275        if flags.header_type == constants::HEADER_2 {
276            // HEADER_2: [flags:1][hops:1][transport_id:16][dest_hash:16][context:1][data:*]
277            let min_len = 2 + dst_len * 2 + 1;
278            if raw.len() < min_len {
279                return Err(PacketError::TooShort);
280            }
281
282            let mut transport_id = [0u8; 16];
283            transport_id.copy_from_slice(&raw[2..2 + dst_len]);
284
285            let mut destination_hash = [0u8; 16];
286            destination_hash.copy_from_slice(&raw[2 + dst_len..2 + 2 * dst_len]);
287
288            let context = raw[2 + 2 * dst_len];
289            if raw.len() == 2 + 2 * dst_len + 1 {
290                return Err(PacketError::ZeroLengthData);
291            }
292
293            let packet_hash = if HASH {
294                Self::compute_packet_hash(flags.header_type, raw)
295            } else {
296                [0; 32]
297            };
298
299            Ok(RawPacket {
300                flags,
301                hops,
302                transport_id: Some(transport_id),
303                destination_hash,
304                context,
305                raw: input.into(),
306                packet_hash,
307                rssi: None,
308                snr: None,
309            })
310        } else if flags.header_type == constants::HEADER_1 {
311            // HEADER_1: [flags:1][hops:1][dest_hash:16][context:1][data:*]
312            let min_len = 2 + dst_len + 1;
313            if raw.len() < min_len {
314                return Err(PacketError::TooShort);
315            }
316
317            let mut destination_hash = [0u8; 16];
318            destination_hash.copy_from_slice(&raw[2..2 + dst_len]);
319
320            let context = raw[2 + dst_len];
321            if raw.len() == 2 + dst_len + 1 {
322                return Err(PacketError::ZeroLengthData);
323            }
324
325            let packet_hash = if HASH {
326                Self::compute_packet_hash(flags.header_type, raw)
327            } else {
328                [0; 32]
329            };
330
331            Ok(RawPacket {
332                flags,
333                hops,
334                transport_id: None,
335                destination_hash,
336                context,
337                raw: input.into(),
338                packet_hash,
339                rssi: None,
340                snr: None,
341            })
342        } else {
343            Err(PacketError::InvalidHeaderType)
344        }
345    }
346
347    /// Get the hashable part of the packet.
348    ///
349    /// From Python Packet.py:354-361:
350    /// - Take raw[0] & 0x0F (mask out upper 4 bits of flags)
351    /// - For HEADER_1: append raw[2:]
352    /// - For HEADER_2: skip transport_id: append raw[18:]
353    pub fn get_hashable_part(&self) -> Vec<u8> {
354        Self::compute_hashable_part(self.flags.header_type, &self.raw)
355    }
356
357    fn hashable_tail(header_type: u8, raw: &[u8]) -> &[u8] {
358        if header_type == constants::HEADER_2 {
359            // Skip transport_id: raw[2..18] is transport_id (16 bytes)
360            &raw[(constants::TRUNCATED_HASHLENGTH / 8 + 2)..]
361        } else {
362            &raw[2..]
363        }
364    }
365
366    fn compute_hashable_part(header_type: u8, raw: &[u8]) -> Vec<u8> {
367        let tail = Self::hashable_tail(header_type, raw);
368        let mut hashable = Vec::with_capacity(1 + tail.len());
369        hashable.push(raw[0] & 0b00001111);
370        hashable.extend_from_slice(tail);
371        hashable
372    }
373
374    /// SHA-256 of the hashable part, fed from the wire bytes without building
375    /// the concatenated buffer that [`RawPacket::get_hashable_part`] returns.
376    fn compute_packet_hash(header_type: u8, raw: &[u8]) -> [u8; 32] {
377        let mut hasher = rns_crypto::sha256::Sha256::new();
378        hasher.update(&[raw[0] & 0b00001111]);
379        hasher.update(Self::hashable_tail(header_type, raw));
380        hasher.digest()
381    }
382
383    /// Compute `packet_hash` from the packet's wire bytes.
384    pub(crate) fn hash_in_place(&mut self) {
385        self.packet_hash = Self::compute_packet_hash(self.flags.header_type, &self.raw);
386    }
387
388    /// Full SHA-256 hash of the hashable part.
389    pub fn get_hash(&self) -> [u8; 32] {
390        self.packet_hash
391    }
392
393    /// Truncated hash (first 16 bytes) of the cached packet hash.
394    ///
395    /// Packet construction and unpacking compute `packet_hash` once; routing
396    /// paths use this prefix instead of hashing the raw packet again.
397    pub fn get_truncated_hash(&self) -> [u8; 16] {
398        let mut result = [0u8; 16];
399        result.copy_from_slice(&self.packet_hash[..16]);
400        result
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn raw_only_packing_preserves_wire_layout_and_limits() {
410        for header in [constants::HEADER_1, constants::HEADER_2] {
411            for transport in [None, Some([0xBB; 16])] {
412                for len in [0, 1, 464, 16348] {
413                    let flags = PacketFlags {
414                        header_type: header,
415                        context_flag: constants::FLAG_UNSET,
416                        transport_type: constants::TRANSPORT_BROADCAST,
417                        destination_type: constants::DESTINATION_LINK,
418                        packet_type: constants::PACKET_TYPE_DATA,
419                    };
420                    let data = vec![0xCC; len];
421                    let mut expected = vec![flags.pack(), 7];
422                    if header == constants::HEADER_2 {
423                        expected.extend_from_slice(&[0xBB; 16]);
424                    }
425                    expected.extend_from_slice(&[0xAA; 16]);
426                    expected.push(constants::CONTEXT_RESOURCE);
427                    expected.extend_from_slice(&data);
428                    for limit in [0, expected.len() - 1, expected.len(), expected.len() + 1] {
429                        let result = RawPacket::pack_raw_with_max_mtu(
430                            flags,
431                            7,
432                            &[0xAA; 16],
433                            transport.as_ref(),
434                            constants::CONTEXT_RESOURCE,
435                            &data,
436                            limit,
437                        );
438                        if header == constants::HEADER_2 && transport.is_none() {
439                            assert!(matches!(result, Err(PacketError::MissingTransportId)));
440                        } else if limit < expected.len() {
441                            assert!(matches!(result, Err(PacketError::ExceedsMtu)));
442                        } else {
443                            assert_eq!(result.unwrap(), expected);
444                            let (raw, hash) = RawPacket::pack_raw_with_hash_with_max_mtu(
445                                flags,
446                                7,
447                                &[0xAA; 16],
448                                transport.as_ref(),
449                                constants::CONTEXT_RESOURCE,
450                                &data,
451                                limit,
452                            )
453                            .unwrap();
454                            assert_eq!(raw, expected);
455                            if len == 0 {
456                                assert!(matches!(
457                                    RawPacket::unpack(&raw),
458                                    Err(PacketError::ZeroLengthData)
459                                ));
460                            } else {
461                                assert_eq!(RawPacket::unpack(&raw).unwrap().packet_hash, hash);
462                            }
463                        }
464                    }
465                }
466            }
467        }
468    }
469
470    #[test]
471    fn test_flags_pack_header1_data_single_broadcast() {
472        let flags = PacketFlags {
473            header_type: constants::HEADER_1,
474            context_flag: constants::FLAG_UNSET,
475            transport_type: constants::TRANSPORT_BROADCAST,
476            destination_type: constants::DESTINATION_SINGLE,
477            packet_type: constants::PACKET_TYPE_DATA,
478        };
479        assert_eq!(flags.pack(), 0x00);
480    }
481
482    #[test]
483    fn test_flags_pack_header2_announce_single_transport() {
484        let flags = PacketFlags {
485            header_type: constants::HEADER_2,
486            context_flag: constants::FLAG_UNSET,
487            transport_type: constants::TRANSPORT_TRANSPORT,
488            destination_type: constants::DESTINATION_SINGLE,
489            packet_type: constants::PACKET_TYPE_ANNOUNCE,
490        };
491        // 0b01010001 = 0x51
492        assert_eq!(flags.pack(), 0x51);
493    }
494
495    #[test]
496    fn test_flags_roundtrip() {
497        for byte in 0..=0x7Fu8 {
498            let flags = PacketFlags::unpack(byte);
499            assert_eq!(flags.pack(), byte);
500        }
501    }
502
503    #[test]
504    fn test_pack_header1() {
505        let dest_hash = [0xAA; 16];
506        let data = b"hello";
507        let flags = PacketFlags {
508            header_type: constants::HEADER_1,
509            context_flag: constants::FLAG_UNSET,
510            transport_type: constants::TRANSPORT_BROADCAST,
511            destination_type: constants::DESTINATION_SINGLE,
512            packet_type: constants::PACKET_TYPE_DATA,
513        };
514
515        let pkt =
516            RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, data).unwrap();
517
518        // Verify layout: [flags:1][hops:1][dest:16][context:1][data:5] = 24 bytes
519        assert_eq!(pkt.raw.len(), 24);
520        assert_eq!(pkt.raw[0], 0x00); // flags
521        assert_eq!(pkt.raw[1], 0x00); // hops
522        assert_eq!(&pkt.raw[2..18], &dest_hash); // dest hash
523        assert_eq!(pkt.raw[18], 0x00); // context
524        assert_eq!(&pkt.raw[19..], b"hello"); // data
525    }
526
527    #[test]
528    fn test_pack_header2() {
529        let dest_hash = [0xAA; 16];
530        let transport_id = [0xBB; 16];
531        let data = b"world";
532        let flags = PacketFlags {
533            header_type: constants::HEADER_2,
534            context_flag: constants::FLAG_UNSET,
535            transport_type: constants::TRANSPORT_TRANSPORT,
536            destination_type: constants::DESTINATION_SINGLE,
537            packet_type: constants::PACKET_TYPE_ANNOUNCE,
538        };
539
540        let pkt = RawPacket::pack(
541            flags,
542            3,
543            &dest_hash,
544            Some(&transport_id),
545            constants::CONTEXT_NONE,
546            data,
547        )
548        .unwrap();
549
550        // Layout: [flags:1][hops:1][transport:16][dest:16][context:1][data:5] = 40 bytes
551        assert_eq!(pkt.raw.len(), 40);
552        assert_eq!(pkt.raw[0], flags.pack());
553        assert_eq!(pkt.raw[1], 3);
554        assert_eq!(&pkt.raw[2..18], &transport_id);
555        assert_eq!(&pkt.raw[18..34], &dest_hash);
556        assert_eq!(pkt.raw[34], 0x00);
557        assert_eq!(&pkt.raw[35..], b"world");
558    }
559
560    #[test]
561    fn test_unpack_roundtrip_header1() {
562        let dest_hash = [0x11; 16];
563        let data = b"test data";
564        let flags = PacketFlags {
565            header_type: constants::HEADER_1,
566            context_flag: constants::FLAG_UNSET,
567            transport_type: constants::TRANSPORT_BROADCAST,
568            destination_type: constants::DESTINATION_SINGLE,
569            packet_type: constants::PACKET_TYPE_DATA,
570        };
571
572        let pkt = RawPacket::pack(
573            flags,
574            5,
575            &dest_hash,
576            None,
577            constants::CONTEXT_RESOURCE,
578            data,
579        )
580        .unwrap();
581        let unpacked = RawPacket::unpack(&pkt.raw).unwrap();
582
583        assert_eq!(unpacked.flags, flags);
584        assert_eq!(unpacked.hops, 5);
585        assert!(unpacked.transport_id.is_none());
586        assert_eq!(unpacked.destination_hash, dest_hash);
587        assert_eq!(unpacked.context, constants::CONTEXT_RESOURCE);
588        assert_eq!(unpacked.data(), data);
589        assert_eq!(unpacked.packet_hash, pkt.packet_hash);
590    }
591
592    #[test]
593    fn test_unpack_roundtrip_header2() {
594        let dest_hash = [0x22; 16];
595        let transport_id = [0x33; 16];
596        let data = b"transported";
597        let flags = PacketFlags {
598            header_type: constants::HEADER_2,
599            context_flag: constants::FLAG_SET,
600            transport_type: constants::TRANSPORT_TRANSPORT,
601            destination_type: constants::DESTINATION_SINGLE,
602            packet_type: constants::PACKET_TYPE_ANNOUNCE,
603        };
604
605        let pkt = RawPacket::pack(
606            flags,
607            2,
608            &dest_hash,
609            Some(&transport_id),
610            constants::CONTEXT_NONE,
611            data,
612        )
613        .unwrap();
614        let unpacked = RawPacket::unpack(&pkt.raw).unwrap();
615
616        assert_eq!(unpacked.flags, flags);
617        assert_eq!(unpacked.hops, 2);
618        assert_eq!(unpacked.transport_id.unwrap(), transport_id);
619        assert_eq!(unpacked.destination_hash, dest_hash);
620        assert_eq!(unpacked.context, constants::CONTEXT_NONE);
621        assert_eq!(unpacked.data(), data);
622        assert_eq!(unpacked.packet_hash, pkt.packet_hash);
623    }
624
625    #[test]
626    fn truncated_hash_is_derived_from_cached_packet_hash() {
627        let mut packet = RawPacket::pack(
628            PacketFlags {
629                header_type: constants::HEADER_1,
630                context_flag: constants::FLAG_UNSET,
631                transport_type: constants::TRANSPORT_BROADCAST,
632                destination_type: constants::DESTINATION_SINGLE,
633                packet_type: constants::PACKET_TYPE_DATA,
634            },
635            1,
636            &[0x42; 16],
637            None,
638            constants::CONTEXT_NONE,
639            b"cached hash",
640        )
641        .unwrap();
642        let expected: [u8; 16] = packet.packet_hash[..16].try_into().unwrap();
643
644        // Changing the retained wire buffer demonstrates that this accessor is
645        // a prefix operation, not a second hash computation.
646        let last = packet.raw.len() - 1;
647        packet.raw[last] ^= 0xff;
648
649        assert_eq!(packet.get_truncated_hash(), expected);
650    }
651
652    #[test]
653    fn test_unpack_too_short() {
654        assert!(RawPacket::unpack(&[0x00; 5]).is_err());
655    }
656
657    #[test]
658    fn unpack_rejects_zero_length_data_for_both_header_types() {
659        for header_type in [constants::HEADER_1, constants::HEADER_2] {
660            let packet = RawPacket::pack(
661                PacketFlags {
662                    header_type,
663                    context_flag: constants::FLAG_UNSET,
664                    transport_type: constants::TRANSPORT_BROADCAST,
665                    destination_type: constants::DESTINATION_SINGLE,
666                    packet_type: constants::PACKET_TYPE_DATA,
667                },
668                0,
669                &[0x42; 16],
670                (header_type == constants::HEADER_2).then_some(&[0x24; 16]),
671                constants::CONTEXT_NONE,
672                b"",
673            )
674            .unwrap();
675            assert!(matches!(
676                RawPacket::unpack(&packet.raw),
677                Err(PacketError::ZeroLengthData)
678            ));
679        }
680    }
681
682    #[test]
683    fn unpack_enforces_pathfinder_hop_boundary_but_pack_remains_permissive() {
684        let flags = PacketFlags {
685            header_type: constants::HEADER_1,
686            context_flag: constants::FLAG_UNSET,
687            transport_type: constants::TRANSPORT_BROADCAST,
688            destination_type: constants::DESTINATION_SINGLE,
689            packet_type: constants::PACKET_TYPE_DATA,
690        };
691        let accepted = RawPacket::pack(flags, 127, &[1; 16], None, 0, b"x").unwrap();
692        assert_eq!(RawPacket::unpack(&accepted.raw).unwrap().hops, 127);
693
694        for hops in [128, 255] {
695            let packed = RawPacket::pack(flags, hops, &[1; 16], None, 0, b"x").unwrap();
696            assert!(matches!(
697                RawPacket::unpack(&packed.raw),
698                Err(PacketError::InvalidHopCount(value)) if value == hops
699            ));
700        }
701    }
702
703    #[test]
704    fn test_pack_exceeds_mtu() {
705        let flags = PacketFlags {
706            header_type: constants::HEADER_1,
707            context_flag: constants::FLAG_UNSET,
708            transport_type: constants::TRANSPORT_BROADCAST,
709            destination_type: constants::DESTINATION_SINGLE,
710            packet_type: constants::PACKET_TYPE_DATA,
711        };
712        let data = [0u8; 500]; // way too much data
713        let result = RawPacket::pack(flags, 0, &[0; 16], None, 0, &data);
714        assert!(result.is_err());
715    }
716
717    #[test]
718    fn test_header2_missing_transport_id() {
719        let flags = PacketFlags {
720            header_type: constants::HEADER_2,
721            context_flag: constants::FLAG_UNSET,
722            transport_type: constants::TRANSPORT_TRANSPORT,
723            destination_type: constants::DESTINATION_SINGLE,
724            packet_type: constants::PACKET_TYPE_ANNOUNCE,
725        };
726        let result = RawPacket::pack(flags, 0, &[0; 16], None, 0, b"data");
727        assert!(result.is_err());
728    }
729
730    #[test]
731    fn test_hashable_part_header1_masks_upper_flags() {
732        let dest_hash = [0xCC; 16];
733        let flags = PacketFlags {
734            header_type: constants::HEADER_1,
735            context_flag: constants::FLAG_SET,
736            transport_type: constants::TRANSPORT_BROADCAST,
737            destination_type: constants::DESTINATION_SINGLE,
738            packet_type: constants::PACKET_TYPE_DATA,
739        };
740
741        let pkt =
742            RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, b"test").unwrap();
743        let hashable = pkt.get_hashable_part();
744
745        // First byte should have upper 4 bits masked out
746        assert_eq!(hashable[0], pkt.raw[0] & 0x0F);
747        // Rest should be raw[2:]
748        assert_eq!(&hashable[1..], &pkt.raw[2..]);
749    }
750
751    #[test]
752    fn test_hashable_part_header2_strips_transport_id() {
753        let dest_hash = [0xDD; 16];
754        let transport_id = [0xEE; 16];
755        let flags = PacketFlags {
756            header_type: constants::HEADER_2,
757            context_flag: constants::FLAG_UNSET,
758            transport_type: constants::TRANSPORT_TRANSPORT,
759            destination_type: constants::DESTINATION_SINGLE,
760            packet_type: constants::PACKET_TYPE_ANNOUNCE,
761        };
762
763        let pkt = RawPacket::pack(
764            flags,
765            0,
766            &dest_hash,
767            Some(&transport_id),
768            constants::CONTEXT_NONE,
769            b"data",
770        )
771        .unwrap();
772        let hashable = pkt.get_hashable_part();
773
774        // First byte: flags masked
775        assert_eq!(hashable[0], pkt.raw[0] & 0x0F);
776        // Should skip transport_id: raw[18:] = dest_hash + context + data
777        assert_eq!(&hashable[1..], &pkt.raw[18..]);
778    }
779}