#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "utils")]
pub mod utils;
mod log;
mod types;
pub use crate::types::*;
#[cfg(any(feature = "log", feature = "defmt"))]
use crate::log::debug;
use cfg_if::cfg_if;
use core::net;
pub async fn get_time<U, T>(addr: net::SocketAddr, socket: &U, context: NtpContext<T>) -> Result<NtpResult>
where
U: NtpUdpSocket,
T: NtpTimestampGenerator + Copy,
{
let result = sntp_send_request(addr, socket, context).await?;
sntp_process_response(addr, socket, context, result).await
}
pub async fn sntp_send_request<U, T>(
dest: net::SocketAddr,
socket: &U,
context: NtpContext<T>,
) -> Result<SendRequestResult>
where
U: NtpUdpSocket,
T: NtpTimestampGenerator,
{
#[cfg(any(feature = "log", feature = "defmt"))]
debug!("send request - Address: {:?}", dest);
let request = NtpPacket::new(context.timestamp_gen);
send_request(dest, &request, socket).await?;
Ok(SendRequestResult::from(request))
}
pub async fn sntp_process_response<U, T>(
dest: net::SocketAddr,
socket: &U,
mut context: NtpContext<T>,
send_req_result: SendRequestResult,
) -> Result<NtpResult>
where
U: NtpUdpSocket,
T: NtpTimestampGenerator,
{
let mut response_buf = RawNtpPacket::default();
let (response, src) = socket.recv_from(response_buf.0.as_mut()).await?;
context.timestamp_gen.init();
let recv_unix_seconds = context.timestamp_gen.timestamp_sec();
let recv_timestamp =
ntp_wire_timestamp_from_unix(recv_unix_seconds, context.timestamp_gen.timestamp_subsec_micros());
#[cfg(any(feature = "log", feature = "defmt"))]
debug!("Response: {}", response);
if dest != src {
return Err(Error::ResponseAddressMismatch);
}
if response < NTP_PACKET_LEN {
return Err(Error::IncorrectPayload);
}
let client_precision = context.timestamp_gen.precision();
let result = process_response(
send_req_result,
response_buf,
recv_timestamp,
recv_unix_seconds,
client_precision,
);
#[cfg(any(feature = "log", feature = "defmt"))]
if let Ok(r) = &result {
debug!("{:?}", r);
}
result
}
async fn send_request<U>(dest: net::SocketAddr, req: &NtpPacket, socket: &U) -> Result<()>
where
U: NtpUdpSocket,
{
let buf = RawNtpPacket::from(req);
match socket.send_to(&buf.0, dest).await {
Ok(size) => {
if size == buf.0.len() {
Ok(())
} else {
Err(Error::Network)
}
}
Err(_) => Err(Error::Network),
}
}
#[cfg(feature = "sync")]
pub mod sync {
#[cfg(any(feature = "log", feature = "defmt"))]
use crate::log::debug;
use crate::net;
use crate::types::{NtpContext, NtpResult, NtpTimestampGenerator, NtpUdpSocket, Result, SendRequestResult};
pub(crate) const SYNC_EXECUTOR_NUMBER_OF_TASKS: usize = 1;
use miniloop::executor::Executor;
pub fn get_time<U, T>(addr: net::SocketAddr, socket: &U, context: NtpContext<T>) -> Result<NtpResult>
where
U: NtpUdpSocket,
T: NtpTimestampGenerator + Copy,
{
let result = sntp_send_request(addr, socket, context)?;
#[cfg(any(feature = "log", feature = "defmt"))]
debug!("{:?}", result);
sntp_process_response(addr, socket, context, result)
}
pub fn sntp_send_request<U, T>(
dest: net::SocketAddr,
socket: &U,
context: NtpContext<T>,
) -> Result<SendRequestResult>
where
U: NtpUdpSocket,
T: NtpTimestampGenerator + Copy,
{
Executor::<1>::new().block_on(crate::sntp_send_request(dest, socket, context))
}
pub fn sntp_process_response<U, T>(
dest: net::SocketAddr,
socket: &U,
context: NtpContext<T>,
send_req_result: SendRequestResult,
) -> Result<NtpResult>
where
U: NtpUdpSocket,
T: NtpTimestampGenerator + Copy,
{
Executor::<SYNC_EXECUTOR_NUMBER_OF_TASKS>::new().block_on(crate::sntp_process_response(
dest,
socket,
context,
send_req_result,
))
}
}
fn precision_to_micros(precision: i8) -> u64 {
if precision >= 0 {
let shift = precision.cast_unsigned();
1_000_000u64 << shift
} else {
let shift = precision.abs().cast_unsigned();
if shift >= 64 {
0 } else {
let divisor = 1u64 << shift;
let numer = 1_000_000u64;
(numer + (divisor / 2)) / divisor
}
}
}
fn process_response(
send_req_result: SendRequestResult,
resp: RawNtpPacket,
recv_timestamp: u64,
recv_unix_seconds: u64,
client_precision: i8,
) -> Result<NtpResult> {
let packet = NtpPacket::from_be_bytes(&resp.0);
cfg_if!(
if #[cfg(any(feature = "log", feature = "defmt"))] {
let debug_packet = DebugNtpPacket::new(&packet, recv_timestamp);
debug!("{:#?}", debug_packet);
}
);
validate_response(&packet, &send_req_result)?;
let t1 = packet.origin_timestamp;
let t2 = packet.recv_timestamp;
let t3 = packet.tx_timestamp;
let t4 = recv_timestamp;
let units = Units::Microseconds;
let roundtrip = roundtrip_calculate(t1, t2, t3, t4, units, precision_to_micros(client_precision));
let offset = offset_calculate(t1, t2, t3, t4, units);
let timestamp = reconstruct_timestamp(packet.tx_timestamp, recv_unix_seconds).ok_or(Error::InvalidTimestamp)?;
let li = shifter(packet.li_vn_mode, LI_MASK, LI_SHIFT);
#[cfg(any(feature = "log", feature = "defmt"))]
debug!("Roundtrip delay: {} {}. Offset: {} {}", roundtrip, units, offset, units);
#[cfg(feature = "dispersion")]
let dispersion = dispersion_calculate(t1, t4, packet.precision, client_precision);
#[cfg(not(feature = "dispersion"))]
let _ = client_precision;
#[cfg(not(feature = "dispersion"))]
let dispersion: u64 = 0;
Ok(NtpResult {
seconds: timestamp.unix_seconds,
seconds_fraction: timestamp.fraction,
roundtrip,
offset,
stratum: packet.stratum,
precision: packet.precision,
leap_indicator: li,
root_delay: packet.root_delay,
root_dispersion: packet.root_dispersion,
reference_id: packet.ref_id.to_be_bytes(), reference_timestamp: packet.ref_timestamp,
poll: packet.poll,
dispersion,
})
}
fn validate_response(packet: &NtpPacket, send_req_result: &SendRequestResult) -> Result<()> {
if send_req_result.originate_timestamp != packet.origin_timestamp {
return Err(Error::IncorrectOriginTimestamp);
}
let mode = shifter(packet.li_vn_mode, MODE_MASK, MODE_SHIFT);
let li = shifter(packet.li_vn_mode, LI_MASK, LI_SHIFT);
let resp_version = shifter(packet.li_vn_mode, VERSION_MASK, VERSION_SHIFT);
let req_version = shifter(send_req_result.version, VERSION_MASK, VERSION_SHIFT);
if mode != SNTP_UNICAST {
return Err(Error::IncorrectMode);
}
if packet.stratum == 0 {
return Err(Error::KissOfDeath(KissOfDeathCode::from_bytes(
packet.ref_id.to_be_bytes(),
)));
}
if li == LI_UNSYNCHRONIZED {
return Err(Error::UnsynchronizedClock);
}
if resp_version == 0 || resp_version > req_version {
return Err(Error::IncorrectResponseVersion);
}
if packet.stratum >= 16 {
return Err(Error::IncorrectStratumHeaders);
}
if packet.tx_timestamp == 0 {
return Err(Error::InvalidTimestamp);
}
if (packet.root_delay / 2).saturating_add(packet.root_dispersion) >= MAXDISP {
return Err(Error::ExcessiveRootDistance);
}
if packet.ref_timestamp != 0 && packet.ref_timestamp.wrapping_sub(packet.tx_timestamp).cast_signed() > 0 {
return Err(Error::BackwardReferenceTimestamp);
}
Ok(())
}
fn shifter(val: u8, mask: u8, shift: u8) -> u8 {
(val & mask) >> shift
}
#[cfg(feature = "dispersion")]
fn dispersion_calculate(t1: u64, t4: u64, server_precision: i8, client_precision: i8) -> u64 {
let server_precision_usecs = precision_to_micros(server_precision);
let client_precision_usecs = precision_to_micros(client_precision);
let elapsed = t4.wrapping_sub(t1).cast_signed();
let elapsed = if elapsed <= 0 { 0 } else { elapsed.cast_unsigned() };
let elapsed_sec = elapsed >> 32;
let elapsed_sec_fraction = elapsed & u64::from(u32::MAX);
let elapsed_usecs = convert_delays(elapsed_sec, elapsed_sec_fraction, u64::from(USEC_IN_SEC));
let phi_term = elapsed_usecs * 15 / 1_000_000;
server_precision_usecs
.saturating_add(client_precision_usecs)
.saturating_add(phi_term)
}
fn convert_delays(sec: u64, fraction: u64, units: u64) -> u64 {
sec * units + fraction * units / (1u64 << 32)
}
fn ntp_wire_timestamp_from_unix(unix_seconds: u64, micros: u32) -> u64 {
let ntp_seconds = unix_seconds.wrapping_add(u64::from(NtpPacket::NTP_TIMESTAMP_DELTA));
let fraction = u64::from(micros) * NTP_ERA_SECONDS / u64::from(USEC_IN_SEC);
((ntp_seconds & u64::from(u32::MAX)) << 32) | fraction
}
#[derive(Copy, Clone)]
struct ReconstructedTimestamp {
unix_seconds: u64,
fraction: u32,
}
fn reconstruct_timestamp(raw: u64, local_unix_context_seconds: u64) -> Option<ReconstructedTimestamp> {
if raw == 0 {
return None;
}
let raw_secs = raw >> 32;
let raw_fraction = u32::try_from(raw & u64::from(u32::MAX)).unwrap();
let local_ntp = local_unix_context_seconds.checked_add(u64::from(NtpPacket::NTP_TIMESTAMP_DELTA))?;
let base_era = local_ntp >> 32;
let mut best_candidate = None;
for era in [base_era.wrapping_sub(1), base_era, base_era.wrapping_add(1)] {
let candidate_ntp = (era << 32) | raw_secs;
if candidate_ntp < u64::from(NtpPacket::NTP_TIMESTAMP_DELTA) {
continue;
}
let distance = candidate_ntp.abs_diff(local_ntp);
if match best_candidate {
None => true,
Some((_, best)) => distance < best,
} {
best_candidate = Some((candidate_ntp, distance));
}
}
let (candidate_ntp, _) = best_candidate?;
Some(ReconstructedTimestamp {
unix_seconds: candidate_ntp - u64::from(NtpPacket::NTP_TIMESTAMP_DELTA),
fraction: raw_fraction,
})
}
fn roundtrip_calculate(t1: u64, t2: u64, t3: u64, t4: u64, units: Units, min_us: u64) -> u64 {
let outbound = t4.wrapping_sub(t1).cast_signed();
let server_time = t3.wrapping_sub(t2).cast_signed();
let delta = outbound.saturating_sub(server_time);
if delta <= 0 {
return min_us;
}
let delta = delta.cast_unsigned();
let delta_sec = (delta & SECONDS_MASK) >> 32;
let delta_sec_fraction = delta & SECONDS_FRAC_MASK;
let value = match units {
Units::Milliseconds => convert_delays(delta_sec, delta_sec_fraction, u64::from(MSEC_IN_SEC)),
Units::Microseconds => convert_delays(delta_sec, delta_sec_fraction, u64::from(USEC_IN_SEC)),
};
value.max(min_us)
}
fn offset_calculate(t1: u64, t2: u64, t3: u64, t4: u64, units: Units) -> i64 {
let theta = (t2.wrapping_sub(t1).cast_signed() / 2).saturating_add(t3.wrapping_sub(t4).cast_signed() / 2);
let theta_sec = (theta.unsigned_abs() & SECONDS_MASK) >> 32;
let theta_sec_fraction = theta.unsigned_abs() & SECONDS_FRAC_MASK;
match units {
Units::Milliseconds => {
convert_delays(theta_sec, theta_sec_fraction, u64::from(MSEC_IN_SEC)).cast_signed() * theta.signum()
}
Units::Microseconds => {
convert_delays(theta_sec, theta_sec_fraction, u64::from(USEC_IN_SEC)).cast_signed() * theta.signum()
}
}
}
fn get_ntp_timestamp<T: NtpTimestampGenerator>(timestamp_gen: &T) -> u64 {
ntp_wire_timestamp_from_unix(timestamp_gen.timestamp_sec(), timestamp_gen.timestamp_subsec_micros())
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub fn fraction_to_milliseconds(sec_fraction: u32) -> u32 {
(u64::from(sec_fraction) * u64::from(MSEC_IN_SEC) / (1u64 << 32)) as u32
}
#[must_use]
pub fn fraction_to_microseconds(sec_fraction: u32) -> u32 {
(u64::from(sec_fraction) * u64::from(USEC_IN_SEC) / (1u64 << 32)) as u32
}
#[must_use]
pub fn fraction_to_nanoseconds(sec_fraction: u32) -> u32 {
(u64::from(sec_fraction) * u64::from(NSEC_IN_SEC) / (1u64 << 32)) as u32
}
#[must_use]
pub fn fraction_to_picoseconds(sec_fraction: u32) -> u64 {
u64::try_from(u128::from(sec_fraction) * u128::from(PSEC_IN_SEC) / (1u128 << 32)).unwrap_or(0)
}
#[cfg(test)]
mod sntpc_ntp_result_tests {
use crate::{
Error, NTP_ERA_SECONDS, NtpPacket, SendRequestResult, ntp_wire_timestamp_from_unix, offset_calculate,
reconstruct_timestamp, roundtrip_calculate, types::Units, validate_response,
};
struct Timestamps(u64, u64, u64, u64);
struct OffsetCalcTestCase {
timestamp: Timestamps,
expected: i64,
}
impl OffsetCalcTestCase {
fn new(t1: u64, t2: u64, t3: u64, t4: u64, expected: i64) -> Self {
OffsetCalcTestCase {
timestamp: Timestamps(t1, t2, t3, t4),
expected,
}
}
fn t1(&self) -> u64 {
self.timestamp.0
}
fn t2(&self) -> u64 {
self.timestamp.1
}
fn t3(&self) -> u64 {
self.timestamp.2
}
fn t4(&self) -> u64 {
self.timestamp.3
}
}
#[test]
fn test_offset_calculate_us() {
let tests = [
OffsetCalcTestCase::new(
16_893_142_954_672_769_962,
16_893_142_959_053_084_959,
16_893_142_959_053_112_968,
16_893_142_954_793_063_406,
1_005_870,
),
OffsetCalcTestCase::new(
16_893_362_966_131_575_843,
16_893_362_966_715_800_791,
16_893_362_966_715_869_584,
16_893_362_967_084_349_913,
25115,
),
OffsetCalcTestCase::new(
16_893_399_716_399_327_198,
16_893_399_716_453_045_029,
16_893_399_716_453_098_083,
16_893_399_716_961_924_964,
-52981,
),
OffsetCalcTestCase::new(
9_487_534_663_484_046_772u64,
16_882_120_099_581_835_046u64,
16_882_120_099_583_884_144u64,
9_487_534_663_651_464_597u64,
1_721_686_086_620_926,
),
];
for t in tests {
let offset = offset_calculate(t.t1(), t.t2(), t.t3(), t.t4(), Units::Microseconds);
let expected = t.expected;
assert_eq!(offset, expected);
}
}
#[test]
fn test_offset_calculate_across_era_boundary() {
let t1 = 0xffff_ffff_0000_0000u64;
let t2 = 0xffff_ffff_6000_0000u64;
let t3 = 0x0000_0000_4000_0000u64;
let t4 = 0x0000_0000_8000_0000u64;
assert_eq!(offset_calculate(t1, t2, t3, t4, Units::Microseconds), 62_500);
}
#[test]
fn test_offset_calculate_ms() {
let tests = [
OffsetCalcTestCase::new(
16_893_142_954_672_769_962,
16_893_142_959_053_084_959,
16_893_142_959_053_112_968,
16_893_142_954_793_063_406,
1_005_870 / 1_000,
),
OffsetCalcTestCase::new(
16_893_362_966_131_575_843,
16_893_362_966_715_800_791,
16_893_362_966_715_869_584,
16_893_362_967_084_349_913,
25115 / 1_000,
),
OffsetCalcTestCase::new(
16_893_399_716_399_327_198,
16_893_399_716_453_045_029,
16_893_399_716_453_098_083,
16_893_399_716_961_924_964,
-52981 / 1_000,
),
OffsetCalcTestCase::new(
9_487_534_663_484_046_772u64,
16_882_120_099_581_835_046u64,
16_882_120_099_583_884_144u64,
9_487_534_663_651_464_597u64,
1_721_686_086_620_926 / 1_000,
),
];
for t in tests {
let offset = offset_calculate(t.t1(), t.t2(), t.t3(), t.t4(), Units::Milliseconds);
let expected = t.expected;
assert_eq!(offset, expected);
}
}
#[test]
fn test_units_str_representation() {
assert_eq!(format!("{}", Units::Milliseconds), "ms");
assert_eq!(format!("{}", Units::Microseconds), "us");
}
#[test]
fn test_roundtrip_calculate_normal() {
let t1 = 1_000_000_000u64;
let t2 = 1_000_010_000u64;
let t3 = 1_000_020_000u64;
let t4 = 1_000_030_000u64;
let result = roundtrip_calculate(t1, t2, t3, t4, Units::Microseconds, 0);
assert!(result > 0);
}
#[test]
fn test_roundtrip_calculate_clock_backward() {
let t1 = 2_000_000_000u64;
let t2 = 1_000_010_000u64;
let t3 = 1_000_020_000u64;
let t4 = 1_000_030_000u64;
let result = roundtrip_calculate(t1, t2, t3, t4, Units::Microseconds, 1);
assert_eq!(result, 1);
}
#[test]
fn test_roundtrip_calculate_negative_delay() {
let t1 = 1_000_000_000u64;
let t2 = 1_000_100_000u64;
let t3 = 1_000_200_000u64;
let t4 = 1_000_050_000u64;
let result = roundtrip_calculate(t1, t2, t3, t4, Units::Microseconds, 1);
assert_eq!(result, 1);
}
#[test]
fn test_roundtrip_calculate_all_zeros() {
let result = roundtrip_calculate(0, 0, 0, 0, Units::Microseconds, 1);
assert_eq!(result, 1);
}
#[test]
fn test_ntp_wire_timestamp_from_unix_rollover() {
assert_eq!(ntp_wire_timestamp_from_unix(2_085_978_495, 0), 0xffff_ffff_0000_0000);
assert_eq!(ntp_wire_timestamp_from_unix(2_085_978_496, 0), 0);
assert_eq!(ntp_wire_timestamp_from_unix(2_085_978_497, 0), 0x0000_0001_0000_0000);
}
#[test]
fn test_reconstruct_timestamp_rollover_and_zero() {
assert!(reconstruct_timestamp(0, 2_085_978_496).is_none());
assert_eq!(
reconstruct_timestamp(0x0000_0001_0000_0000, 0).unwrap().unix_seconds,
2_085_978_497
);
let just_after = ntp_wire_timestamp_from_unix(2_085_978_497, 123_456);
let reconstructed = reconstruct_timestamp(just_after, 2_085_978_497).unwrap();
assert_eq!(reconstructed.unix_seconds, 2_085_978_497);
assert_eq!(
reconstructed.fraction,
u32::try_from(123_456u64 * NTP_ERA_SECONDS / 1_000_000).unwrap()
);
let just_before = ntp_wire_timestamp_from_unix(2_085_978_495, 0);
let reconstructed = reconstruct_timestamp(just_before, 2_085_978_496).unwrap();
assert_eq!(reconstructed.unix_seconds, 2_085_978_495);
}
#[test]
fn test_reconstruct_timestamp_skips_pre_unix_candidate() {
let raw = 100_000_000u64 << 32;
assert_eq!(reconstruct_timestamp(raw, 0).unwrap().unix_seconds, 2_185_978_496);
}
#[test]
fn test_reconstruct_timestamp_supports_era_2_plus() {
let unix_seconds = 6_380_955_792u64 + 123;
let raw = ntp_wire_timestamp_from_unix(unix_seconds, 42);
let reconstructed = reconstruct_timestamp(raw, unix_seconds).unwrap();
assert_eq!(reconstructed.unix_seconds, unix_seconds);
assert_eq!(
reconstructed.fraction,
u32::try_from(42u64 * NTP_ERA_SECONDS / 1_000_000).unwrap()
);
}
#[test]
fn test_reconstruct_timestamp_tie_prefers_earlier_era() {
let local_ntp = (1u64 << 32) + 1 + (1u64 << 31);
let local_unix_seconds = local_ntp - u64::from(NtpPacket::NTP_TIMESTAMP_DELTA);
let reconstructed = reconstruct_timestamp(1u64 << 32, local_unix_seconds).unwrap();
assert_eq!(reconstructed.unix_seconds, 2_085_978_497);
}
#[test]
fn test_reconstruct_timestamp_half_era_boundary_switches_after_tie() {
let raw = 1u64 << 32;
let era_1_ntp = (1u64 << 32) + 1;
let era_1_unix = era_1_ntp - u64::from(NtpPacket::NTP_TIMESTAMP_DELTA);
let era_2_unix = era_1_unix + NTP_ERA_SECONDS;
let before_tie = era_1_ntp + (1u64 << 31) - 1;
assert_eq!(
reconstruct_timestamp(raw, before_tie - u64::from(NtpPacket::NTP_TIMESTAMP_DELTA))
.unwrap()
.unix_seconds,
era_1_unix
);
let after_tie = era_1_ntp + (1u64 << 31) + 1;
assert_eq!(
reconstruct_timestamp(raw, after_tie - u64::from(NtpPacket::NTP_TIMESTAMP_DELTA))
.unwrap()
.unix_seconds,
era_2_unix
);
}
#[test]
fn test_reconstruct_timestamp_uses_bad_pivot_wrong_era() {
let actual_unix_seconds = 6_380_955_792u64 + 123;
let stale_pivot = actual_unix_seconds - NTP_ERA_SECONDS;
let raw = ntp_wire_timestamp_from_unix(actual_unix_seconds, 0);
let reconstructed = reconstruct_timestamp(raw, stale_pivot).unwrap();
assert_eq!(reconstructed.unix_seconds, stale_pivot);
assert_ne!(reconstructed.unix_seconds, actual_unix_seconds);
}
#[test]
fn test_reconstruct_timestamp_uses_bad_pivot_two_eras_stale() {
let actual_unix_seconds = 3 * NTP_ERA_SECONDS + 1_000;
let stale_pivot = actual_unix_seconds - 2 * NTP_ERA_SECONDS;
let raw = ntp_wire_timestamp_from_unix(actual_unix_seconds, 0);
let reconstructed = reconstruct_timestamp(raw, stale_pivot).unwrap();
assert_eq!(reconstructed.unix_seconds, stale_pivot);
assert_ne!(reconstructed.unix_seconds, actual_unix_seconds);
}
#[test]
fn test_reconstruct_timestamp_zero_secs_nonzero_fraction() {
let raw = 10_000u64;
let reconstructed = reconstruct_timestamp(raw, 0).unwrap();
assert_eq!(reconstructed.unix_seconds, 2_085_978_496);
assert_eq!(reconstructed.fraction, 10_000);
}
#[test]
fn test_reconstruct_timestamp_roundtrip_across_eras() {
for era in 0..=2u64 {
let unix_seconds = era * NTP_ERA_SECONDS + 1_000;
let raw = ntp_wire_timestamp_from_unix(unix_seconds, 500_000);
let reconstructed = reconstruct_timestamp(raw, unix_seconds).unwrap();
assert_eq!(reconstructed.unix_seconds, unix_seconds, "era {era}");
assert_eq!(
reconstructed.fraction,
u32::try_from(500_000u64 * NTP_ERA_SECONDS / 1_000_000).unwrap(),
"era {era}"
);
}
}
#[test]
fn test_roundtrip_calculate_rollover_wraps() {
let t1 = 0xffff_ffff_0000_0000;
let t2 = 0xffff_ffff_8000_0000;
let t3 = 0x0000_0000_8000_0000;
let t4 = 0x0000_0001_0000_0000;
assert_eq!(roundtrip_calculate(t1, t2, t3, t4, Units::Microseconds, 1), 1_000_000);
}
#[test]
fn test_validate_response_rejects_zero_tx_timestamp() {
let packet = NtpPacket {
li_vn_mode: 0x24,
stratum: 1,
poll: 0,
precision: 0,
root_delay: 0,
root_dispersion: 0,
ref_id: 0,
ref_timestamp: 0,
origin_timestamp: 1,
recv_timestamp: 0,
tx_timestamp: 0,
};
let send = SendRequestResult {
originate_timestamp: 1,
version: packet.li_vn_mode,
};
assert!(matches!(
validate_response(&packet, &send),
Err(Error::InvalidTimestamp)
));
}
#[cfg(feature = "dispersion")]
#[test]
fn test_precision_to_micros() {
assert_eq!(super::precision_to_micros(0), 1_000_000);
assert_eq!(super::precision_to_micros(1), 2_000_000);
assert!(super::precision_to_micros(-10) >= 976);
assert!(super::precision_to_micros(-10) <= 978);
assert_eq!(super::precision_to_micros(-20), 1);
assert_eq!(super::precision_to_micros(-30), 0);
}
#[cfg(feature = "dispersion")]
#[test]
fn test_dispersion_computation() {
use crate::NtpResult;
let result = NtpResult::new(0, 0, 1000, 0, 1, -20, 0, 0, 0, [0; 4], 0, 0, 0);
assert_eq!(result.dispersion(), 0);
let result = NtpResult::new(0, 0, 1000, 0, 1, -20, 0, 0, 0, [0; 4], 0, 0, 42);
assert_eq!(result.dispersion(), 42);
}
#[cfg(feature = "dispersion")]
#[test]
fn test_dispersion_calculate_uses_elapsed_client_time() {
let t1 = 1u64 << 32;
let t4 = 3u64 << 32;
assert_eq!(super::dispersion_calculate(t1, t4, -20, -20), 32);
assert_eq!(super::dispersion_calculate(t4, t1, -20, -20), 2);
assert_eq!(super::dispersion_calculate(0xffff_ffff_0000_0000, 0, -20, -20), 17);
assert_eq!(
super::dispersion_calculate(0xffff_ffff_8000_0000, 0x0000_0000_4000_0000, -20, -20),
13
);
}
#[test]
fn test_dispersion_field_default() {
use crate::NtpResult;
let result = NtpResult::new(0, 0, 1000, 0, 1, -20, 0, 0, 0, [0; 4], 0, 0, 0);
assert_eq!(result.dispersion(), 0);
}
}