Xclox
C++11 header-only cross-platform date and time library with an asynchronous NTP client
packet.h
1 /*
2  * Copyright (c) 2024 Abdullatif Kalla.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE.txt file in the root directory of this source tree.
6  */
7 
8 #include "xclox/ntp/packet.hpp"
9 
10 #include "tools/helper.hpp"
11 
12 #include <functional>
13 #include <map>
14 
15 using namespace xclox::ntp;
16 using namespace std::chrono;
17 
18 TEST_SUITE("Packet")
19 {
20  Packet::DataType zeros {};
21 
22  Packet::DataType ones { []() {Packet::DataType d;d.fill(0xFF);return d; }() };
23 
24  Packet::DataType pattern {
25  0xA3, // leap(2-bit), version(3-bit), mode(3-bit)
26  0x02, // stratum
27  0xFA, // poll
28  0xEC, // precision
29  0x98, 0x76, 0x54, 0x32, // root delay
30  0xCB, 0xA9, 0x87, 0x65, // root dispersion
31  0x23, 0x45, 0x67, 0x89, // reference ID
32  0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, 0xFE, 0xDC, // reference timestamp
33  0xE9, 0x87, 0x65, 0x43, 0x21, 0x0F, 0xED, 0xCB, // origin timestamp
34  0xE8, 0x76, 0x54, 0x32, 0x10, 0xFE, 0xDC, 0xBA, // receive timestamp
35  0xE7, 0x65, 0x43, 0x21, 0x0F, 0xED, 0xCB, 0xA9 // transmit timestamp
36  };
37 
38  const auto& ntpToSys = [](const system_clock::duration& duration) { return duration - xclox::ntp::internal::EpochDeltaSeconds; };
39 
40  TEST_CASE("default constructible to null")
41  {
42  Packet p;
43  CHECK(p.data() == zeros);
44  }
45 
46  TEST_CASE("constructible from raw data")
47  {
48  Packet p1(zeros);
49  CHECK(p1.data() == zeros);
50 
51  Packet p2(ones);
52  CHECK(p2.data() == ones);
53 
54  Packet p3(pattern);
55  CHECK(p3.data() == pattern);
56  }
57 
58  TEST_CASE("zeroed data is null")
59  {
60  Packet p1;
61  CHECK(p1.isNull() == true);
62 
63  Packet p2(zeros);
64  CHECK(p2.isNull() == true);
65 
66  Packet p3(ones);
67  CHECK(p3.isNull() == false);
68 
69  for (size_t i = 0; i != std::tuple_size<Packet::DataType> {}; ++i) {
70  Packet::DataType data {};
71  data[i] = 1;
72  Packet p4(data);
73  CHECK(p4.isNull() == false);
74  }
75  }
76 
77  TEST_CASE("leap bits [0-1]")
78  {
79  for (uint8_t i = 0; i < 4; ++i) {
80  Packet::DataType data {};
81  data[0] = static_cast<uint8_t>(i << 6);
82  Packet p(data);
83  CHECK(p.leap() == i);
84  }
85  }
86 
87  TEST_CASE("version bits [2-4]")
88  {
89  for (uint8_t i = 0; i < 8; ++i) {
90  Packet::DataType data {};
91  data[0] = static_cast<uint8_t>(i << 3);
92  Packet p(data);
93  CHECK(p.version() == i);
94  }
95  }
96 
97  TEST_CASE("mode bits [5-7]")
98  {
99  for (uint8_t i = 0; i < 8; ++i) {
100  Packet::DataType data {};
101  data[0] = static_cast<uint8_t>(i);
102  Packet p(data);
103  CHECK(p.mode() == i);
104  }
105  }
106 
107  TEST_CASE("leap, version, and mode bits coexist")
108  {
109  for (uint8_t i = 0; i < 8; ++i) {
110  Packet::DataType data {};
111  data[0] = static_cast<uint8_t>(i << 6 | i << 3 | i);
112  Packet p(data);
113  if (i < 4) {
114  CHECK(p.leap() == i);
115  }
116  CHECK(p.version() == i);
117  CHECK(p.mode() == i);
118  }
119  }
120 
121  TEST_CASE("stratum bits [8-15]")
122  {
123  Packet::DataType data {};
124  Packet p1(data);
125  CHECK(p1.stratum() == 0);
126  data[1] = 1;
127  Packet p2(data);
128  CHECK(p2.stratum() == 1);
129  data[1] = 254;
130  Packet p3(data);
131  CHECK(p3.stratum() == 254);
132  }
133 
134  TEST_CASE("poll bits [16-23]")
135  {
136  Packet::DataType data {};
137  Packet p1(data);
138  CHECK(p1.poll() == 0);
139  data[2] = 1;
140  Packet p2(data);
141  CHECK(p2.poll() == 1);
142  data[2] = static_cast<uint8_t>(-10);
143  Packet p3(data);
144  CHECK(p3.poll() == -10);
145  }
146 
147  TEST_CASE("precision bits [24-31]")
148  {
149  Packet::DataType data {};
150  Packet p1(data);
151  CHECK(p1.precision() == 0);
152  data[3] = 1;
153  Packet p2(data);
154  CHECK(p2.precision() == 1);
155  data[3] = static_cast<uint8_t>(-20);
156  Packet p3(data);
157  CHECK(p3.precision() == -20);
158  }
159 
160  TEST_CASE("32-bit values ( root delay [32-63], root dispersion [64-95], reference ID [96-127] )")
161  {
162  std::map<size_t, uint32_t (Packet::*)() const> offsetFunctionMap {
163  { 4, &Packet::rootDelay },
164  { 8, &Packet::rootDispersion },
165  { 12, &Packet::referenceID }
166  };
167  for (const auto& pair : offsetFunctionMap) {
168  Packet::DataType data {};
169  Packet p1(data);
170  CHECK((p1.*pair.second)() == 0);
171  data[pair.first + 3] = 1;
172  Packet p2(data);
173  CHECK((p2.*pair.second)() == 1);
174  data[pair.first + 0] = 0x01;
175  data[pair.first + 1] = 0x23;
176  data[pair.first + 2] = 0x45;
177  data[pair.first + 3] = 0x67;
178  Packet p3(data);
179  CHECK((p3.*pair.second)() == 0x01234567);
180  }
181  }
182 
183  TEST_CASE("64-bit timestamps ( reference [128-191], origin [192-255], receive [256-319], transmit [320-383] )")
184  {
185  std::map<size_t, uint64_t (Packet::*)() const> offsetFunctionMap {
187  { 24, &Packet::originTimestamp },
188  { 32, &Packet::receiveTimestamp },
190  };
191  for (const auto& pair : offsetFunctionMap) {
192  Packet::DataType data {};
193  Packet p1(data);
194  CHECK((p1.*pair.second)() == 0);
195  data[pair.first + 7] = 1;
196  Packet p2(data);
197  CHECK((p2.*pair.second)() == 1);
198  data[pair.first + 0] = 0x01;
199  data[pair.first + 1] = 0x23;
200  data[pair.first + 2] = 0x45;
201  data[pair.first + 3] = 0x67;
202  data[pair.first + 4] = 0x89;
203  data[pair.first + 5] = 0xAB;
204  data[pair.first + 6] = 0xCD;
205  data[pair.first + 7] = 0xEF;
206  Packet p3(data);
207  CHECK((p3.*pair.second)() == 0x0123456789ABCDEF);
208  }
209  }
210 
211  TEST_CASE("constructible from values")
212  {
213  Packet p0(
214  0, // leap
215  0, // version
216  0, // mode
217  0, // stratum
218  0, // poll
219  0, // precision
220  0, // rootDelay
221  0, // rootDispersion
222  0, // referenceID
223  0, // referenceTimestamp
224  0, // originTimestamp
225  0, // receiveTimestamp
226  0 // transmitTimestamp
227  );
228  CHECK(p0.data() == zeros);
229  Packet p1(
230  0xFF, // leap
231  0xFF, // version
232  0xFF, // mode
233  0xFF, // stratum
234  static_cast<int8_t>(0xFF), // poll
235  static_cast<int8_t>(0xFF), // precision
236  0xFFFFFFFF, // rootDelay
237  0xFFFFFFFF, // rootDispersion
238  0xFFFFFFFF, // referenceID
239  0xFFFFFFFFFFFFFFFF, // referenceTimestamp
240  0xFFFFFFFFFFFFFFFF, // originTimestamp
241  0xFFFFFFFFFFFFFFFF, // receiveTimestamp
242  0xFFFFFFFFFFFFFFFF // transmitTimestamp
243  );
244  CHECK(p1.data() == ones);
245  Packet p2(
246  0x02, // leap
247  0x04, // version
248  0x03, // mode
249  0x02, // stratum
250  static_cast<int8_t>(0xFA), // poll
251  static_cast<int8_t>(0xEC), // precision
252  0x98765432, // rootDelay
253  0xCBA98765, // rootDispersion
254  0x23456789, // referenceID
255  0xBA9876543210FEDC, // referenceTimestamp
256  0xE9876543210FEDCB, // originTimestamp
257  0xE876543210FEDCBA, // receiveTimestamp
258  0xE76543210FEDCBA9 // transmitTimestamp
259  );
260  CHECK(p2.data() == pattern);
261  }
262 
263  TEST_CASE("comparable")
264  {
265  Packet p1;
266  Packet p2(zeros);
267  Packet p3(ones);
268  Packet p4(pattern);
269  CHECK(p1 == p1);
270  CHECK(p1 == p2);
271  CHECK_FALSE(p2 == p3);
272  CHECK_FALSE(p3 == p4);
273  CHECK(p3 == p3);
274  CHECK(p4 == p4);
275  CHECK_FALSE(p1 != p1);
276  CHECK_FALSE(p1 != p2);
277  CHECK(p2 != p3);
278  CHECK(p3 != p4);
279  CHECK_FALSE(p3 != p3);
280  CHECK_FALSE(p4 != p4);
281  }
282 
283  TEST_CASE("copyable")
284  {
285  Packet p1;
286  Packet p2(zeros);
287  Packet p3(ones);
288  Packet p4(pattern);
289  Packet p5(p1);
290  Packet p6(p2);
291  Packet p7(p3);
292  Packet p8(p4);
293  CHECK(p1 == p5);
294  CHECK(p2 == p6);
295  CHECK(p3 == p7);
296  CHECK(p4 == p8);
297  }
298 
299  TEST_CASE("movable")
300  {
301  Packet p1;
302  Packet p2(zeros);
303  Packet p3(ones);
304  Packet p4(pattern);
305  Packet p5(std::move(p1));
306  Packet p6(std::move(p2));
307  Packet p7(std::move(p3));
308  Packet p8(std::move(p4));
309  CHECK(p1.isNull());
310  CHECK(p2.isNull());
311  CHECK(p3.isNull());
312  CHECK(p4.isNull());
313  CHECK(p5 == Packet());
314  CHECK(p6 == Packet(zeros));
315  CHECK(p7 == Packet(ones));
316  CHECK(p8 == Packet(pattern));
317  }
318 
319  TEST_CASE("packet delay & offset")
320  {
321  SUBCASE("null packet")
322  {
323  Packet p;
324  CHECK(p.delay(0) == system_clock::duration(0));
325  CHECK(p.offset(0) == system_clock::duration(0));
326  }
327  SUBCASE("up-to-date clocks")
328  {
329  uint64_t origin = 0xE902661000000000; // 2023-11-17 22:22:08.00
330  uint64_t receive = origin + 0x40000000; // 2023-11-17 22:22:08.25
331  uint64_t transmit = origin + 0x80000000; // 2023-11-17 22:22:08.50
332  uint64_t destination = origin + 0xC0000000; // 2023-11-17 22:22:08.75
333  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
334  CHECK(p.delay(destination) == milliseconds(500));
335  CHECK(p.offset(destination) == system_clock::duration(0));
336  }
337  SUBCASE("zero latency")
338  {
339  uint64_t origin = 0xE902661000000000; // 2023-11-17 22:22:08.0
340  uint64_t receive = origin; // 2023-11-17 22:22:08.0
341  uint64_t transmit = origin + 0x80000000; // 2023-11-17 22:22:08.5
342  uint64_t destination = transmit; // 2023-11-17 22:22:08.5
343  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
344  CHECK(p.delay(destination) == system_clock::duration(0));
345  CHECK(p.offset(destination) == system_clock::duration(0));
346  }
347  SUBCASE("client clock is at NTP epoch")
348  {
349  uint64_t origin = 0; // 1900-01-01 00:00:00.0000
350  uint64_t receive = 0xE902661010000000; // 2023-11-17 22:22:08.0625
351  uint64_t transmit = receive + 0x10000000; // 2023-11-17 22:22:08.1250
352  uint64_t destination = origin + 0x30000000; // 1900-01-01 00:00:00.1875
353  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
354  CHECK(p.delay(destination) == milliseconds(125));
355  CHECK(p.offset(destination) == seconds(0xE9026610));
356  }
357  SUBCASE("client clock is at end of NTP era")
358  {
359  uint64_t origin = 0xFFFFFFFF00000000; // 2036-02-07 06:28:15.0000
360  uint64_t receive = 0xE902661010000000; // 2023-11-17 22:22:08.0625
361  uint64_t transmit = receive + 0x10000000; // 2023-11-17 22:22:08.1250
362  uint64_t destination = origin + 0x30000000; // 2036-02-07 06:28:15.1875
363  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
364  CHECK(p.delay(destination) == milliseconds(125));
365  CHECK(p.offset(destination) == -seconds(0x16FD99EF));
366  }
367  SUBCASE("client clock is at start of next NTP era")
368  {
369  uint64_t origin = 0; // 2036-02-07 06:28:16.0000
370  uint64_t receive = 0xFFFFFFFF10000000; // 2036-02-07 06:28:15.0625
371  uint64_t transmit = receive + 0x10000000; // 2036-02-07 06:28:15.1250
372  uint64_t destination = origin + 0x40000000; // 2036-02-07 06:28:16.2500
373  system_clock::time_point destinationTP(ntpToSys(seconds(0x100000000) + milliseconds(250))); // 2036-02-07 06:28:16.2500
374  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
375  CHECK(p.delay(destination) == microseconds(187500));
376  // WRONG offset with a time stamp: 2036-02-07 06:28:15.218750
377  CHECK(p.offset(destination) == seconds(0xFFFFFFFF) - microseconds(31250));
378  // RIGHT offset with a time point: 2036-02-07 06:28:15.218750
379  CHECK(p.offset(destinationTP) == -seconds(1) - microseconds(31250));
380  }
381  SUBCASE("server clock is at start of next NTP era")
382  {
383  uint64_t origin = 0xFFFFFFFF00000000; // 2036-02-07 06:28:15.0000
384  uint64_t receive = 0x0000000010000000; // 2036-02-07 06:28:16.0625
385  uint64_t transmit = receive + 0x10000000; // 2036-02-07 06:28:16.1250
386  uint64_t destination = origin + 0x40000000; // 2036-02-07 06:28:15.2500
387  system_clock::time_point destinationTP(ntpToSys(seconds(0xFFFFFFFF) + milliseconds(250))); // 2036-02-07 06:28:15.2500
388  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
389  CHECK(p.delay(destination) == microseconds(187500));
390  // WRONG offset with a time stamp: 1900-01-01 00:00:00.218750
391  CHECK(p.offset(destination) == -seconds(0xFFFFFFFF) - microseconds(31250));
392  // RIGHT offset with a time point: 2036-02-07 06:28:16.218750
393  CHECK(p.offset(destinationTP) == seconds(1) - microseconds(31250));
394  }
395  SUBCASE("client clock behind server clock by 68 years")
396  {
397  uint64_t origin = 0x8000000100000000; // 1968-01-20 03:14:09.0000
398  uint64_t receive = 0x0000000010000000; // 2036-02-07 06:28:16.0625
399  uint64_t transmit = receive + 0x10000000; // 2036-02-07 06:28:16.1250
400  uint64_t destination = origin + 0x40000000; // 1968-01-20 03:14:09.2500
401  system_clock::time_point destinationTP(ntpToSys(seconds(0x80000001) + milliseconds(250))); // 1968-01-20 03:14:09.2500
402  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
403  CHECK(p.delay(destination) == microseconds(187500));
404  // WRONG offset with a time stamp: 1900-01-01 00:00:00.218750
405  CHECK(p.offset(destination) == -seconds(0x80000001) - microseconds(31250));
406  // RIGHT offset with a time point: 2036-02-07 06:28:16.218750
407  CHECK(p.offset(destinationTP) == seconds(0x7FFFFFFF) - microseconds(31250));
408  }
409  SUBCASE("server clock behind client clock by 68 years")
410  {
411  uint64_t origin = 0x8000000000000000; // 2104-02-26 09:42:24.0000
412  uint64_t receive = 0x0000000010000000; // 2036-02-07 06:28:16.0625
413  uint64_t transmit = receive + 0x10000000; // 2036-02-07 06:28:16.1250
414  uint64_t destination = origin + 0x40000000; // 2104-02-26 09:42:24.2500
415  system_clock::time_point destinationTP(ntpToSys(seconds(0x80000000) + milliseconds(250))); // 2104-02-26 09:42:24.2500
416  Packet p(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, origin, receive, transmit);
417  CHECK(p.delay(destination) == microseconds(187500));
418  // WRONG offset with a time stamp: 1900-01-01 00:00:00.218750
419  CHECK(p.offset(destination) == -seconds(0x80000000) - microseconds(31250));
420  // RIGHT offset with a time point: 2036-02-07 06:28:16.218750
421  CHECK(p.offset(destinationTP) == -seconds(0x80000000) - microseconds(31250));
422  }
423  } // TEST_CASE
424 } // TEST_SUITE
Packet is an immutable raw NTP packet.
Definition: packet.hpp:54
int8_t precision() const
Returns a signed integer representing the precision of the system clock, in log2 seconds.
Definition: packet.hpp:187
bool isNull() const
Returns whether the underlying data is all zeros.
Definition: packet.hpp:119
uint64_t transmitTimestamp() const
Returns the server's time at which the packet departed to the client.
Definition: packet.hpp:229
internal::DataType DataType
Type of packet's underlying data.
Definition: packet.hpp:56
std::chrono::system_clock::duration offset(uint64_t destination) const
Returns the time offset of the server relative to the client.
Definition: packet.hpp:253
uint32_t referenceID() const
Returns a 32-bit code identifying the particular server or reference clock.
Definition: packet.hpp:205
uint32_t rootDispersion() const
Returns the total dispersion to the reference clock, in NTP short format.
Definition: packet.hpp:199
std::chrono::system_clock::duration delay(uint64_t destination) const
Returns the round-trip delay of the NTP packet passed from client to server and back again.
Definition: packet.hpp:240
uint64_t originTimestamp() const
Returns the client's time at which the packet departed to the server.
Definition: packet.hpp:217
uint64_t referenceTimestamp() const
Returns the server's time at which the system clock was last set or corrected.
Definition: packet.hpp:211
int8_t poll() const
Returns a signed integer representing the maximum interval between successive messages,...
Definition: packet.hpp:181
DataType data() const
Returns a raw data representation of the underlying packet.
Definition: packet.hpp:113
uint64_t receiveTimestamp() const
Returns the server's time at which the packet arrived from the client.
Definition: packet.hpp:223
uint8_t stratum() const
Returns an unsigned integer representing the level of the server in the NTP hierarchy.
Definition: packet.hpp:175
uint32_t rootDelay() const
Returns the total round-trip delay to the reference clock, in NTP short format.
Definition: packet.hpp:193