1pub const PORT: u16 = 123;
15pub const PACKET_LEN: usize = 48;
17
18const NTP_TO_UNIX: u64 = 2_208_988_800;
20
21const LI_VN_MODE_CLIENT: u8 = (4 << 3) | 3;
23const MODE_SERVER: u8 = 4;
24const MODE_BROADCAST: u8 = 5;
25
26pub const fn request() -> [u8; PACKET_LEN] {
29 let mut p = [0u8; PACKET_LEN];
30 p[0] = LI_VN_MODE_CLIENT;
31 p
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36#[cfg_attr(feature = "defmt", derive(defmt::Format))]
37pub struct ServerTime {
38 pub unix_seconds: u64,
40 pub micros: u32,
42 pub stratum: u8,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48#[cfg_attr(feature = "defmt", derive(defmt::Format))]
49pub enum Error {
50 TooShort,
52 NotAServerReply,
54 KissOfDeath([u8; 4]),
57 Unsynchronised,
59 NoTimestamp,
61}
62
63pub fn parse_reply(p: &[u8]) -> Result<ServerTime, Error> {
68 let p: &[u8; PACKET_LEN] = p
69 .get(..PACKET_LEN)
70 .and_then(|s| s.try_into().ok())
71 .ok_or(Error::TooShort)?;
72 let mode = p[0] & 0x07;
73 if mode != MODE_SERVER && mode != MODE_BROADCAST {
74 return Err(Error::NotAServerReply);
75 }
76 let stratum = p[1];
77 if stratum == 0 {
78 return Err(Error::KissOfDeath([p[12], p[13], p[14], p[15]]));
79 }
80 if p[0] >> 6 == 3 {
81 return Err(Error::Unsynchronised);
82 }
83 let secs = u32::from_be_bytes([p[40], p[41], p[42], p[43]]);
84 let frac = u32::from_be_bytes([p[44], p[45], p[46], p[47]]);
85 if secs == 0 && frac == 0 {
86 return Err(Error::NoTimestamp);
87 }
88 let ntp_secs = if u64::from(secs) < NTP_TO_UNIX {
90 u64::from(secs) + (1 << 32)
91 } else {
92 u64::from(secs)
93 };
94 Ok(ServerTime {
95 unix_seconds: ntp_secs - NTP_TO_UNIX,
96 micros: ((u64::from(frac) * 1_000_000) >> 32) as u32,
97 stratum,
98 })
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 fn reply(secs: u32, frac: u32) -> [u8; PACKET_LEN] {
106 let mut p = [0u8; PACKET_LEN];
107 p[0] = (4 << 3) | MODE_SERVER;
108 p[1] = 2;
109 p[40..44].copy_from_slice(&secs.to_be_bytes());
110 p[44..48].copy_from_slice(&frac.to_be_bytes());
111 p
112 }
113
114 #[test]
115 fn request_header() {
116 let r = request();
117 assert_eq!(r[0], 0x23);
118 assert!(r[1..].iter().all(|b| *b == 0));
119 }
120
121 #[test]
122 fn converts_ntp_to_unix_in_both_eras() {
123 let t = parse_reply(&reply(3_976_214_400, 1 << 31)).unwrap();
125 assert_eq!(t.unix_seconds, 1_767_225_600);
126 assert_eq!(t.micros, 500_000);
127 assert_eq!(t.stratum, 2);
128
129 let t = parse_reply(&reply(10, 0)).unwrap();
131 assert_eq!(t.unix_seconds, 2_085_978_506);
132 }
133
134 #[test]
135 fn rejects_bad_replies() {
136 assert_eq!(parse_reply(&[0; 10]), Err(Error::TooShort));
137
138 let mut kod = reply(3_976_214_400, 0);
139 kod[1] = 0;
140 kod[12..16].copy_from_slice(b"RATE");
141 assert_eq!(parse_reply(&kod), Err(Error::KissOfDeath(*b"RATE")));
142
143 let mut client = reply(3_976_214_400, 0);
144 client[0] = LI_VN_MODE_CLIENT;
145 assert_eq!(parse_reply(&client), Err(Error::NotAServerReply));
146
147 let mut unsync = reply(3_976_214_400, 0);
148 unsync[0] |= 0xC0;
149 assert_eq!(parse_reply(&unsync), Err(Error::Unsynchronised));
150
151 assert_eq!(parse_reply(&reply(0, 0)), Err(Error::NoTimestamp));
152 }
153}