Skip to main content

ntp_proto/protocol/
bytes.rs

1use crate::error::ParseError;
2
3use super::{
4    ConstPackedSizeBytes, DateFormat, FromBytes, KissOfDeath, LeapIndicator, Mode, Packet,
5    PrimarySource, ReferenceIdentifier, ShortFormat, Stratum, TimestampFormat, ToBytes, Version,
6};
7
8impl FromBytes for ShortFormat {
9    fn from_bytes(buf: &[u8]) -> Result<(Self, usize), ParseError> {
10        if buf.len() < Self::PACKED_SIZE_BYTES {
11            return Err(ParseError::BufferTooShort {
12                needed: Self::PACKED_SIZE_BYTES,
13                available: buf.len(),
14            });
15        }
16        let seconds = u16::from_be_bytes([buf[0], buf[1]]);
17        let fraction = u16::from_be_bytes([buf[2], buf[3]]);
18        Ok((ShortFormat { seconds, fraction }, Self::PACKED_SIZE_BYTES))
19    }
20}
21
22impl FromBytes for TimestampFormat {
23    fn from_bytes(buf: &[u8]) -> Result<(Self, usize), ParseError> {
24        if buf.len() < Self::PACKED_SIZE_BYTES {
25            return Err(ParseError::BufferTooShort {
26                needed: Self::PACKED_SIZE_BYTES,
27                available: buf.len(),
28            });
29        }
30        let seconds = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
31        let fraction = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
32        Ok((
33            TimestampFormat { seconds, fraction },
34            Self::PACKED_SIZE_BYTES,
35        ))
36    }
37}
38
39impl FromBytes for DateFormat {
40    fn from_bytes(buf: &[u8]) -> Result<(Self, usize), ParseError> {
41        if buf.len() < Self::PACKED_SIZE_BYTES {
42            return Err(ParseError::BufferTooShort {
43                needed: Self::PACKED_SIZE_BYTES,
44                available: buf.len(),
45            });
46        }
47        let era_number = i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
48        let era_offset = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
49        let fraction = u64::from_be_bytes([
50            buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
51        ]);
52        Ok((
53            DateFormat {
54                era_number,
55                era_offset,
56                fraction,
57            },
58            Self::PACKED_SIZE_BYTES,
59        ))
60    }
61}
62
63impl FromBytes for Stratum {
64    fn from_bytes(buf: &[u8]) -> Result<(Self, usize), ParseError> {
65        if buf.is_empty() {
66            return Err(ParseError::BufferTooShort {
67                needed: 1,
68                available: 0,
69            });
70        }
71        Ok((Stratum(buf[0]), 1))
72    }
73}
74
75impl FromBytes for (LeapIndicator, Version, Mode) {
76    fn from_bytes(buf: &[u8]) -> Result<(Self, usize), ParseError> {
77        if buf.is_empty() {
78            return Err(ParseError::BufferTooShort {
79                needed: 1,
80                available: 0,
81            });
82        }
83        let li_vn_mode = buf[0];
84        let li_u8 = li_vn_mode >> 6;
85        let vn_u8 = (li_vn_mode >> 3) & 0b111;
86        let mode_u8 = li_vn_mode & 0b111;
87        let li = LeapIndicator::try_from(li_u8).map_err(|_| ParseError::InvalidField {
88            field: "leap indicator",
89            value: li_u8 as u32,
90        })?;
91        let vn = Version(vn_u8);
92        let mode = Mode::try_from(mode_u8).map_err(|_| ParseError::InvalidField {
93            field: "association mode",
94            value: mode_u8 as u32,
95        })?;
96        Ok(((li, vn, mode), 1))
97    }
98}
99
100impl ReferenceIdentifier {
101    /// Parse a reference identifier from 4 bytes, using stratum for disambiguation.
102    ///
103    /// The interpretation of the reference identifier depends on the stratum:
104    /// - Stratum 0: Kiss-o'-Death code
105    /// - Stratum 1: Primary source identifier
106    /// - Stratum 2-15: Secondary/client reference (IPv4 or IPv6 hash)
107    /// - Stratum 16+: Unknown
108    pub fn from_bytes_with_stratum(bytes: [u8; 4], stratum: Stratum) -> Self {
109        let u = u32::from_be_bytes(bytes);
110        if stratum == Stratum::UNSPECIFIED {
111            match KissOfDeath::try_from(u) {
112                Ok(kod) => ReferenceIdentifier::KissOfDeath(kod),
113                Err(_) => ReferenceIdentifier::Unknown(bytes),
114            }
115        } else if stratum == Stratum::PRIMARY {
116            match PrimarySource::try_from(u) {
117                Ok(src) => ReferenceIdentifier::PrimarySource(src),
118                Err(_) => ReferenceIdentifier::Unknown(bytes),
119            }
120        } else if stratum.is_secondary() {
121            ReferenceIdentifier::SecondaryOrClient(bytes)
122        } else {
123            ReferenceIdentifier::Unknown(bytes)
124        }
125    }
126}
127
128impl FromBytes for Packet {
129    fn from_bytes(buf: &[u8]) -> Result<(Self, usize), ParseError> {
130        if buf.len() < Self::PACKED_SIZE_BYTES {
131            return Err(ParseError::BufferTooShort {
132                needed: Self::PACKED_SIZE_BYTES,
133                available: buf.len(),
134            });
135        }
136
137        let mut offset = 0;
138
139        let ((leap_indicator, version, mode), n) =
140            <(LeapIndicator, Version, Mode)>::from_bytes(&buf[offset..])?;
141        offset += n;
142
143        let (stratum, n) = Stratum::from_bytes(&buf[offset..])?;
144        offset += n;
145
146        let poll = buf[offset] as i8;
147        offset += 1;
148
149        let precision = buf[offset] as i8;
150        offset += 1;
151
152        let (root_delay, n) = ShortFormat::from_bytes(&buf[offset..])?;
153        offset += n;
154
155        let (root_dispersion, n) = ShortFormat::from_bytes(&buf[offset..])?;
156        offset += n;
157
158        let ref_id_bytes = [
159            buf[offset],
160            buf[offset + 1],
161            buf[offset + 2],
162            buf[offset + 3],
163        ];
164        let reference_id = ReferenceIdentifier::from_bytes_with_stratum(ref_id_bytes, stratum);
165        offset += 4;
166
167        let (reference_timestamp, n) = TimestampFormat::from_bytes(&buf[offset..])?;
168        offset += n;
169
170        let (origin_timestamp, n) = TimestampFormat::from_bytes(&buf[offset..])?;
171        offset += n;
172
173        let (receive_timestamp, n) = TimestampFormat::from_bytes(&buf[offset..])?;
174        offset += n;
175
176        let (transmit_timestamp, n) = TimestampFormat::from_bytes(&buf[offset..])?;
177        offset += n;
178
179        Ok((
180            Packet {
181                leap_indicator,
182                version,
183                mode,
184                stratum,
185                poll,
186                precision,
187                root_delay,
188                root_dispersion,
189                reference_id,
190                reference_timestamp,
191                origin_timestamp,
192                receive_timestamp,
193                transmit_timestamp,
194            },
195            offset,
196        ))
197    }
198}
199
200// Buffer-based writer implementations (io-independent).
201
202impl ToBytes for ShortFormat {
203    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
204        if buf.len() < Self::PACKED_SIZE_BYTES {
205            return Err(ParseError::BufferTooShort {
206                needed: Self::PACKED_SIZE_BYTES,
207                available: buf.len(),
208            });
209        }
210        let s = self.seconds.to_be_bytes();
211        let f = self.fraction.to_be_bytes();
212        buf[0] = s[0];
213        buf[1] = s[1];
214        buf[2] = f[0];
215        buf[3] = f[1];
216        Ok(Self::PACKED_SIZE_BYTES)
217    }
218}
219
220impl ToBytes for TimestampFormat {
221    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
222        if buf.len() < Self::PACKED_SIZE_BYTES {
223            return Err(ParseError::BufferTooShort {
224                needed: Self::PACKED_SIZE_BYTES,
225                available: buf.len(),
226            });
227        }
228        let s = self.seconds.to_be_bytes();
229        let f = self.fraction.to_be_bytes();
230        buf[..4].copy_from_slice(&s);
231        buf[4..8].copy_from_slice(&f);
232        Ok(Self::PACKED_SIZE_BYTES)
233    }
234}
235
236impl ToBytes for DateFormat {
237    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
238        if buf.len() < Self::PACKED_SIZE_BYTES {
239            return Err(ParseError::BufferTooShort {
240                needed: Self::PACKED_SIZE_BYTES,
241                available: buf.len(),
242            });
243        }
244        buf[..4].copy_from_slice(&self.era_number.to_be_bytes());
245        buf[4..8].copy_from_slice(&self.era_offset.to_be_bytes());
246        buf[8..16].copy_from_slice(&self.fraction.to_be_bytes());
247        Ok(Self::PACKED_SIZE_BYTES)
248    }
249}
250
251impl ToBytes for Stratum {
252    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
253        if buf.is_empty() {
254            return Err(ParseError::BufferTooShort {
255                needed: 1,
256                available: 0,
257            });
258        }
259        buf[0] = self.0;
260        Ok(1)
261    }
262}
263
264impl ToBytes for (LeapIndicator, Version, Mode) {
265    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
266        if buf.is_empty() {
267            return Err(ParseError::BufferTooShort {
268                needed: 1,
269                available: 0,
270            });
271        }
272        let (li, vn, mode) = *self;
273        let mut li_vn_mode = 0u8;
274        li_vn_mode |= (li as u8) << 6;
275        li_vn_mode |= vn.0 << 3;
276        li_vn_mode |= mode as u8;
277        buf[0] = li_vn_mode;
278        Ok(1)
279    }
280}
281
282impl ToBytes for ReferenceIdentifier {
283    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
284        if buf.len() < Self::PACKED_SIZE_BYTES {
285            return Err(ParseError::BufferTooShort {
286                needed: Self::PACKED_SIZE_BYTES,
287                available: buf.len(),
288            });
289        }
290        let bytes = self.as_bytes();
291        buf[..4].copy_from_slice(&bytes);
292        Ok(Self::PACKED_SIZE_BYTES)
293    }
294}
295
296impl ToBytes for Packet {
297    fn to_bytes(&self, buf: &mut [u8]) -> Result<usize, ParseError> {
298        if buf.len() < Self::PACKED_SIZE_BYTES {
299            return Err(ParseError::BufferTooShort {
300                needed: Self::PACKED_SIZE_BYTES,
301                available: buf.len(),
302            });
303        }
304
305        let mut offset = 0;
306
307        let li_vn_mode = (self.leap_indicator, self.version, self.mode);
308        offset += li_vn_mode.to_bytes(&mut buf[offset..])?;
309        offset += self.stratum.to_bytes(&mut buf[offset..])?;
310        buf[offset] = self.poll as u8;
311        offset += 1;
312        buf[offset] = self.precision as u8;
313        offset += 1;
314        offset += self.root_delay.to_bytes(&mut buf[offset..])?;
315        offset += self.root_dispersion.to_bytes(&mut buf[offset..])?;
316        offset += self.reference_id.to_bytes(&mut buf[offset..])?;
317        offset += self.reference_timestamp.to_bytes(&mut buf[offset..])?;
318        offset += self.origin_timestamp.to_bytes(&mut buf[offset..])?;
319        offset += self.receive_timestamp.to_bytes(&mut buf[offset..])?;
320        offset += self.transmit_timestamp.to_bytes(&mut buf[offset..])?;
321
322        Ok(offset)
323    }
324}
325
326// ============================================================================
327// Tests
328// ============================================================================
329
330#[cfg(all(test, feature = "std"))]
331mod tests {
332    use super::*;
333
334    // ── ShortFormat ──────────────────────────────────────────────────
335
336    #[test]
337    fn short_format_roundtrip() {
338        let sf = ShortFormat {
339            seconds: 0x1234,
340            fraction: 0x5678,
341        };
342        let mut buf = [0u8; 4];
343        let written = sf.to_bytes(&mut buf).unwrap();
344        assert_eq!(written, 4);
345        let (decoded, consumed) = ShortFormat::from_bytes(&buf).unwrap();
346        assert_eq!(consumed, 4);
347        assert_eq!(decoded.seconds, sf.seconds);
348        assert_eq!(decoded.fraction, sf.fraction);
349    }
350
351    #[test]
352    fn short_format_edge_values() {
353        for (s, f) in [(0u16, 0u16), (u16::MAX, u16::MAX)] {
354            let sf = ShortFormat {
355                seconds: s,
356                fraction: f,
357            };
358            let mut buf = [0u8; 4];
359            sf.to_bytes(&mut buf).unwrap();
360            let (decoded, _) = ShortFormat::from_bytes(&buf).unwrap();
361            assert_eq!(decoded.seconds, s);
362            assert_eq!(decoded.fraction, f);
363        }
364    }
365
366    #[test]
367    fn short_format_buffer_too_short_read() {
368        let buf = [0u8; 3];
369        let err = ShortFormat::from_bytes(&buf).unwrap_err();
370        assert!(matches!(
371            err,
372            ParseError::BufferTooShort {
373                needed: 4,
374                available: 3
375            }
376        ));
377    }
378
379    #[test]
380    fn short_format_buffer_too_short_write() {
381        let sf = ShortFormat::default();
382        let mut buf = [0u8; 3];
383        let err = sf.to_bytes(&mut buf).unwrap_err();
384        assert!(matches!(err, ParseError::BufferTooShort { .. }));
385    }
386
387    // ── TimestampFormat ─────────────────────────────────────────────
388
389    #[test]
390    fn timestamp_format_roundtrip() {
391        let ts = TimestampFormat {
392            seconds: 3_913_056_000,
393            fraction: 0xABCD_1234,
394        };
395        let mut buf = [0u8; 8];
396        let written = ts.to_bytes(&mut buf).unwrap();
397        assert_eq!(written, 8);
398        let (decoded, consumed) = TimestampFormat::from_bytes(&buf).unwrap();
399        assert_eq!(consumed, 8);
400        assert_eq!(decoded.seconds, ts.seconds);
401        assert_eq!(decoded.fraction, ts.fraction);
402    }
403
404    #[test]
405    fn timestamp_format_edge_values() {
406        for (s, f) in [(0u32, 0u32), (u32::MAX, u32::MAX)] {
407            let ts = TimestampFormat {
408                seconds: s,
409                fraction: f,
410            };
411            let mut buf = [0u8; 8];
412            ts.to_bytes(&mut buf).unwrap();
413            let (decoded, _) = TimestampFormat::from_bytes(&buf).unwrap();
414            assert_eq!(decoded.seconds, s);
415            assert_eq!(decoded.fraction, f);
416        }
417    }
418
419    #[test]
420    fn timestamp_format_buffer_too_short() {
421        let buf = [0u8; 7];
422        let err = TimestampFormat::from_bytes(&buf).unwrap_err();
423        assert!(matches!(
424            err,
425            ParseError::BufferTooShort {
426                needed: 8,
427                available: 7
428            }
429        ));
430    }
431
432    // ── DateFormat ──────────────────────────────────────────────────
433
434    #[test]
435    fn date_format_roundtrip() {
436        let df = DateFormat {
437            era_number: -1,
438            era_offset: 0x1234_5678,
439            fraction: 0xDEAD_BEEF_CAFE_BABE,
440        };
441        let mut buf = [0u8; 16];
442        let written = df.to_bytes(&mut buf).unwrap();
443        assert_eq!(written, 16);
444        let (decoded, consumed) = DateFormat::from_bytes(&buf).unwrap();
445        assert_eq!(consumed, 16);
446        assert_eq!(decoded.era_number, df.era_number);
447        assert_eq!(decoded.era_offset, df.era_offset);
448        assert_eq!(decoded.fraction, df.fraction);
449    }
450
451    #[test]
452    fn date_format_buffer_too_short() {
453        let buf = [0u8; 15];
454        let err = DateFormat::from_bytes(&buf).unwrap_err();
455        assert!(matches!(
456            err,
457            ParseError::BufferTooShort {
458                needed: 16,
459                available: 15
460            }
461        ));
462    }
463
464    // ── Stratum ─────────────────────────────────────────────────────
465
466    #[test]
467    fn stratum_roundtrip() {
468        for val in [0u8, 1, 2, 15, 16, 255] {
469            let s = Stratum(val);
470            let mut buf = [0u8; 1];
471            s.to_bytes(&mut buf).unwrap();
472            let (decoded, consumed) = Stratum::from_bytes(&buf).unwrap();
473            assert_eq!(consumed, 1);
474            assert_eq!(decoded.0, val);
475        }
476    }
477
478    #[test]
479    fn stratum_buffer_empty() {
480        let buf: [u8; 0] = [];
481        let err = Stratum::from_bytes(&buf).unwrap_err();
482        assert!(matches!(err, ParseError::BufferTooShort { .. }));
483    }
484
485    // ── (LeapIndicator, Version, Mode) ──────────────────────────────
486
487    #[test]
488    fn li_vn_mode_roundtrip() {
489        let tuple = (LeapIndicator::NoWarning, Version::V4, Mode::Client);
490        let mut buf = [0u8; 1];
491        let written = tuple.to_bytes(&mut buf).unwrap();
492        assert_eq!(written, 1);
493        let (decoded, consumed) = <(LeapIndicator, Version, Mode)>::from_bytes(&buf).unwrap();
494        assert_eq!(consumed, 1);
495        assert_eq!(decoded.0, LeapIndicator::NoWarning);
496        assert_eq!(decoded.1, Version::V4);
497        assert_eq!(decoded.2, Mode::Client);
498    }
499
500    #[test]
501    fn li_vn_mode_byte_encoding() {
502        // LI=0, VN=4, Mode=3 → (0<<6)|(4<<3)|3 = 0x23
503        let tuple = (LeapIndicator::NoWarning, Version::V4, Mode::Client);
504        let mut buf = [0u8; 1];
505        tuple.to_bytes(&mut buf).unwrap();
506        assert_eq!(buf[0], 0x23);
507    }
508
509    #[test]
510    fn li_vn_mode_all_leap_indicators() {
511        for li in [
512            LeapIndicator::NoWarning,
513            LeapIndicator::AddOne,
514            LeapIndicator::SubOne,
515            LeapIndicator::Unknown,
516        ] {
517            let mut buf = [0u8; 1];
518            (li, Version::V4, Mode::Server).to_bytes(&mut buf).unwrap();
519            let (decoded, _) = <(LeapIndicator, Version, Mode)>::from_bytes(&buf).unwrap();
520            assert_eq!(decoded.0, li);
521        }
522    }
523
524    #[test]
525    fn li_vn_mode_buffer_empty() {
526        let buf: [u8; 0] = [];
527        let err = <(LeapIndicator, Version, Mode)>::from_bytes(&buf).unwrap_err();
528        assert!(matches!(err, ParseError::BufferTooShort { .. }));
529    }
530
531    // ── ReferenceIdentifier ─────────────────────────────────────────
532
533    #[test]
534    fn reference_id_to_bytes_primary() {
535        let ref_id = ReferenceIdentifier::PrimarySource(PrimarySource::Gps);
536        let mut buf = [0u8; 4];
537        let written = ref_id.to_bytes(&mut buf).unwrap();
538        assert_eq!(written, 4);
539    }
540
541    #[test]
542    fn reference_id_to_bytes_secondary() {
543        let ref_id = ReferenceIdentifier::SecondaryOrClient([192, 168, 1, 1]);
544        let mut buf = [0u8; 4];
545        ref_id.to_bytes(&mut buf).unwrap();
546        assert_eq!(buf, [192, 168, 1, 1]);
547    }
548
549    #[test]
550    fn reference_id_buffer_too_short() {
551        let ref_id = ReferenceIdentifier::PrimarySource(PrimarySource::Gps);
552        let mut buf = [0u8; 3];
553        let err = ref_id.to_bytes(&mut buf).unwrap_err();
554        assert!(matches!(err, ParseError::BufferTooShort { .. }));
555    }
556
557    #[test]
558    fn reference_id_from_bytes_with_stratum_kod() {
559        let kod = ReferenceIdentifier::KissOfDeath(KissOfDeath::Deny);
560        let bytes = kod.as_bytes();
561        let decoded = ReferenceIdentifier::from_bytes_with_stratum(bytes, Stratum::UNSPECIFIED);
562        assert!(matches!(
563            decoded,
564            ReferenceIdentifier::KissOfDeath(KissOfDeath::Deny)
565        ));
566    }
567
568    #[test]
569    fn reference_id_from_bytes_with_stratum_primary() {
570        let src = ReferenceIdentifier::PrimarySource(PrimarySource::Gps);
571        let bytes = src.as_bytes();
572        let decoded = ReferenceIdentifier::from_bytes_with_stratum(bytes, Stratum::PRIMARY);
573        assert!(matches!(
574            decoded,
575            ReferenceIdentifier::PrimarySource(PrimarySource::Gps)
576        ));
577    }
578
579    #[test]
580    fn reference_id_from_bytes_with_stratum_secondary() {
581        let bytes = [10, 0, 0, 1];
582        let decoded = ReferenceIdentifier::from_bytes_with_stratum(bytes, Stratum(2));
583        assert!(matches!(
584            decoded,
585            ReferenceIdentifier::SecondaryOrClient([10, 0, 0, 1])
586        ));
587    }
588
589    #[test]
590    fn reference_id_from_bytes_with_stratum_unknown() {
591        let bytes = [0xFF, 0xFE, 0xFD, 0xFC];
592        let decoded = ReferenceIdentifier::from_bytes_with_stratum(bytes, Stratum(16));
593        assert!(matches!(decoded, ReferenceIdentifier::Unknown(_)));
594    }
595
596    // ── Packet ──────────────────────────────────────────────────────
597
598    fn make_test_packet() -> Packet {
599        Packet {
600            leap_indicator: LeapIndicator::NoWarning,
601            version: Version::V4,
602            mode: Mode::Client,
603            stratum: Stratum::UNSPECIFIED,
604            poll: 6,
605            precision: -20,
606            root_delay: ShortFormat {
607                seconds: 1,
608                fraction: 0x8000,
609            },
610            root_dispersion: ShortFormat {
611                seconds: 0,
612                fraction: 0x4000,
613            },
614            reference_id: ReferenceIdentifier::default(),
615            reference_timestamp: TimestampFormat {
616                seconds: 3_913_056_000,
617                fraction: 0,
618            },
619            origin_timestamp: TimestampFormat::default(),
620            receive_timestamp: TimestampFormat::default(),
621            transmit_timestamp: TimestampFormat {
622                seconds: 3_913_056_001,
623                fraction: 0x1234_5678,
624            },
625        }
626    }
627
628    #[test]
629    fn packet_roundtrip() {
630        let pkt = make_test_packet();
631        let mut buf = [0u8; 48];
632        let written = pkt.to_bytes(&mut buf).unwrap();
633        assert_eq!(written, 48);
634        let (decoded, consumed) = Packet::from_bytes(&buf).unwrap();
635        assert_eq!(consumed, 48);
636        assert_eq!(decoded.leap_indicator, pkt.leap_indicator);
637        assert_eq!(decoded.version, pkt.version);
638        assert_eq!(decoded.mode, pkt.mode);
639        assert_eq!(decoded.stratum, pkt.stratum);
640        assert_eq!(decoded.poll, pkt.poll);
641        assert_eq!(decoded.precision, pkt.precision);
642        assert_eq!(decoded.root_delay, pkt.root_delay);
643        assert_eq!(decoded.root_dispersion, pkt.root_dispersion);
644        assert_eq!(decoded.reference_timestamp, pkt.reference_timestamp);
645        assert_eq!(decoded.origin_timestamp, pkt.origin_timestamp);
646        assert_eq!(decoded.receive_timestamp, pkt.receive_timestamp);
647        assert_eq!(decoded.transmit_timestamp, pkt.transmit_timestamp);
648    }
649
650    #[test]
651    fn packet_size_constant() {
652        assert_eq!(Packet::PACKED_SIZE_BYTES, 48);
653    }
654
655    #[test]
656    fn packet_from_bytes_too_short() {
657        let buf = [0u8; 47];
658        let err = Packet::from_bytes(&buf).unwrap_err();
659        assert!(matches!(
660            err,
661            ParseError::BufferTooShort {
662                needed: 48,
663                available: 47
664            }
665        ));
666    }
667
668    #[test]
669    fn packet_to_bytes_too_short() {
670        let pkt = make_test_packet();
671        let mut buf = [0u8; 47];
672        let err = pkt.to_bytes(&mut buf).unwrap_err();
673        assert!(matches!(err, ParseError::BufferTooShort { .. }));
674    }
675
676    #[test]
677    fn packet_stratum1_gps_reference() {
678        let pkt = Packet {
679            stratum: Stratum::PRIMARY,
680            reference_id: ReferenceIdentifier::PrimarySource(PrimarySource::Gps),
681            ..make_test_packet()
682        };
683        let mut buf = [0u8; 48];
684        pkt.to_bytes(&mut buf).unwrap();
685        let (decoded, _) = Packet::from_bytes(&buf).unwrap();
686        assert!(matches!(
687            decoded.reference_id,
688            ReferenceIdentifier::PrimarySource(PrimarySource::Gps)
689        ));
690    }
691
692    #[test]
693    fn packet_stratum0_kod() {
694        let pkt = Packet {
695            stratum: Stratum::UNSPECIFIED,
696            reference_id: ReferenceIdentifier::KissOfDeath(KissOfDeath::Deny),
697            ..make_test_packet()
698        };
699        let mut buf = [0u8; 48];
700        pkt.to_bytes(&mut buf).unwrap();
701        let (decoded, _) = Packet::from_bytes(&buf).unwrap();
702        assert!(matches!(
703            decoded.reference_id,
704            ReferenceIdentifier::KissOfDeath(KissOfDeath::Deny)
705        ));
706    }
707
708    #[test]
709    fn packet_stratum2_secondary() {
710        let pkt = Packet {
711            stratum: Stratum(2),
712            reference_id: ReferenceIdentifier::SecondaryOrClient([10, 0, 0, 1]),
713            ..make_test_packet()
714        };
715        let mut buf = [0u8; 48];
716        pkt.to_bytes(&mut buf).unwrap();
717        let (decoded, _) = Packet::from_bytes(&buf).unwrap();
718        assert!(matches!(
719            decoded.reference_id,
720            ReferenceIdentifier::SecondaryOrClient([10, 0, 0, 1])
721        ));
722    }
723
724    #[test]
725    fn packet_negative_poll_precision() {
726        let pkt = Packet {
727            poll: -6,
728            precision: -32,
729            ..make_test_packet()
730        };
731        let mut buf = [0u8; 48];
732        pkt.to_bytes(&mut buf).unwrap();
733        let (decoded, _) = Packet::from_bytes(&buf).unwrap();
734        assert_eq!(decoded.poll, -6);
735        assert_eq!(decoded.precision, -32);
736    }
737
738    #[test]
739    fn packet_extra_bytes_ignored() {
740        let pkt = make_test_packet();
741        let mut buf = [0u8; 64];
742        pkt.to_bytes(&mut buf).unwrap();
743        let (decoded, consumed) = Packet::from_bytes(&buf).unwrap();
744        assert_eq!(consumed, 48);
745        assert_eq!(decoded.version, pkt.version);
746    }
747
748    // ── Cross-module consistency ────────────────────────────────────
749
750    #[test]
751    fn bytes_and_io_produce_same_output() {
752        // Verify that ToBytes (buffer-based) produces the same bytes as
753        // WriteToBytes (io-based) for the same packet.
754        use crate::protocol::WriteBytes;
755
756        let pkt = make_test_packet();
757
758        // Buffer-based
759        let mut buf_bytes = [0u8; 48];
760        pkt.to_bytes(&mut buf_bytes).unwrap();
761
762        // IO-based
763        let mut io_bytes = Vec::new();
764        io_bytes.write_bytes(pkt).unwrap();
765
766        assert_eq!(&buf_bytes[..], &io_bytes[..]);
767    }
768}