Skip to main content

ntp_proto/protocol/
io.rs

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