use crate::error::ConversionError;
use crate::packet::{LeapIndicator, ReferenceIdentifier};
#[cfg(all(feature = "chrono", feature = "time"))]
use std::convert::TryInto;
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct SntpDuration(f64);
impl SntpDuration {
pub(crate) fn from_secs_f64(secs: f64) -> SntpDuration {
SntpDuration(secs)
}
pub fn abs_as_std_duration(&self) -> Result<std::time::Duration, ConversionError> {
std::time::Duration::try_from_secs_f64(self.0.abs()).map_err(|_| ConversionError::Overflow)
}
pub fn signum(&self) -> i32 {
self.0.signum() as i32
}
pub fn as_secs_f64(&self) -> f64 {
self.0
}
#[cfg(feature = "chrono")]
pub fn into_chrono_duration(self) -> Result<chrono::Duration, ConversionError> {
self.try_into()
}
#[cfg(feature = "time")]
pub fn into_time_duration(self) -> Result<time::Duration, ConversionError> {
self.try_into()
}
}
#[cfg(feature = "chrono")]
impl TryInto<chrono::Duration> for SntpDuration {
type Error = ConversionError;
fn try_into(self) -> Result<chrono::Duration, ConversionError> {
let abs = chrono::Duration::from_std(self.abs_as_std_duration()?)
.map_err(|_| ConversionError::Overflow)?;
Ok(abs * self.signum())
}
}
#[cfg(feature = "time")]
impl TryInto<time::Duration> for SntpDuration {
type Error = ConversionError;
fn try_into(self) -> Result<time::Duration, ConversionError> {
Ok(time::Duration::seconds_f64(self.0))
}
}
#[derive(Debug, Clone, Copy)]
pub struct SntpDateTime {
offset: SntpDuration,
}
impl SntpDateTime {
pub(crate) fn new(offset: SntpDuration) -> SntpDateTime {
SntpDateTime { offset }
}
pub fn unix_timestamp(&self) -> Result<std::time::Duration, ConversionError> {
let now = SystemTime::now();
let corrected = if self.offset.signum() >= 0 {
now.checked_add(self.offset.abs_as_std_duration()?)
.ok_or(ConversionError::Overflow)?
} else {
now.checked_sub(self.offset.abs_as_std_duration()?)
.ok_or(ConversionError::Overflow)?
};
corrected
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| ConversionError::Overflow)
}
pub fn into_system_time(self) -> Result<std::time::SystemTime, ConversionError> {
self.try_into()
}
#[cfg(feature = "chrono")]
pub fn into_chrono_datetime(self) -> Result<chrono::DateTime<chrono::Utc>, ConversionError> {
self.try_into()
}
#[cfg(feature = "time")]
pub fn into_offset_date_time(self) -> Result<time::OffsetDateTime, ConversionError> {
self.try_into()
}
}
impl TryInto<std::time::SystemTime> for SntpDateTime {
type Error = ConversionError;
fn try_into(self) -> Result<std::time::SystemTime, ConversionError> {
if self.offset.signum() > 0 {
SystemTime::now()
.checked_add(self.offset.abs_as_std_duration()?)
.ok_or(ConversionError::Overflow)
} else {
SystemTime::now()
.checked_sub(self.offset.abs_as_std_duration()?)
.ok_or(ConversionError::Overflow)
}
}
}
#[cfg(feature = "chrono")]
impl TryInto<chrono::DateTime<chrono::Utc>> for SntpDateTime {
type Error = ConversionError;
fn try_into(self) -> Result<chrono::DateTime<chrono::Utc>, ConversionError> {
let chrono_offset: chrono::Duration = self.offset.try_into()?;
chrono::Utc::now()
.checked_add_signed(chrono_offset)
.ok_or(ConversionError::Overflow)
}
}
#[cfg(feature = "time")]
impl TryInto<time::OffsetDateTime> for SntpDateTime {
type Error = ConversionError;
fn try_into(self) -> Result<time::OffsetDateTime, ConversionError> {
let time_offset: time::Duration = self.offset.try_into()?;
time::OffsetDateTime::now_utc()
.checked_add(time_offset)
.ok_or(ConversionError::Overflow)
}
}
#[derive(Debug, Clone)]
pub struct SynchronizationResult {
clock_offset_s: f64,
round_trip_delay_s: f64,
reference_identifier: ReferenceIdentifier,
leap_indicator: LeapIndicator,
stratum: u8,
}
impl SynchronizationResult {
pub(crate) fn new(
clock_offset_s: f64,
round_trip_delay_s: f64,
reference_identifier: ReferenceIdentifier,
leap_indicator: LeapIndicator,
stratum: u8,
) -> SynchronizationResult {
SynchronizationResult {
clock_offset_s,
round_trip_delay_s,
reference_identifier,
leap_indicator,
stratum,
}
}
pub fn clock_offset(&self) -> SntpDuration {
SntpDuration::from_secs_f64(self.clock_offset_s)
}
pub fn round_trip_delay(&self) -> SntpDuration {
SntpDuration::from_secs_f64(self.round_trip_delay_s)
}
pub fn reference_identifier(&self) -> &ReferenceIdentifier {
&self.reference_identifier
}
pub fn datetime(&self) -> SntpDateTime {
SntpDateTime::new(self.clock_offset())
}
pub fn leap_indicator(&self) -> LeapIndicator {
self.leap_indicator
}
pub fn stratum(&self) -> u8 {
self.stratum
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sntp_duration_as_secs_f64_works() {
let positive_duration = SntpDuration::from_secs_f64(3600.0);
let negative_duration = SntpDuration::from_secs_f64(-3600.0);
assert_eq!(positive_duration.as_secs_f64(), 3600.0);
assert_eq!(negative_duration.as_secs_f64(), -3600.0);
}
#[test]
fn sntp_duration_abs_and_signum_works() {
let positive_duration = SntpDuration::from_secs_f64(3600.0);
let negative_duration = SntpDuration::from_secs_f64(-3600.0);
assert_eq!(
positive_duration.abs_as_std_duration().unwrap(),
std::time::Duration::from_secs(3600)
);
assert_eq!(
negative_duration.abs_as_std_duration().unwrap(),
std::time::Duration::from_secs(3600)
);
assert_eq!(positive_duration.signum(), 1);
assert_eq!(negative_duration.signum(), -1);
}
#[test]
fn sntp_duration_abs_fails_on_overflow() {
let duration = SntpDuration::from_secs_f64(2e19);
assert!(duration.abs_as_std_duration().is_err());
}
#[cfg(feature = "chrono")]
#[test]
fn sntp_duration_converting_to_chrono_duration_works() {
let positive_duration = SntpDuration::from_secs_f64(3600.0);
let negative_duration = SntpDuration::from_secs_f64(-3600.0);
let positive_chrono: chrono::Duration = positive_duration.try_into().unwrap();
let negative_chrono: chrono::Duration = negative_duration.try_into().unwrap();
assert_eq!(positive_chrono, chrono::Duration::hours(1));
assert_eq!(negative_chrono, chrono::Duration::hours(-1));
}
#[cfg(feature = "chrono")]
#[test]
fn sntp_duration_converting_to_chrono_duration_fails() {
let nan_duration_result: Result<chrono::Duration, ConversionError> =
SntpDuration::from_secs_f64(f64::NAN).try_into();
assert!(nan_duration_result.is_err());
}
#[cfg(feature = "time")]
#[test]
fn sntp_duration_converting_to_time_duration_works() {
let positive_duration = SntpDuration::from_secs_f64(3600.0);
let negative_duration = SntpDuration::from_secs_f64(-3600.0);
let positive_time: time::Duration = positive_duration.try_into().unwrap();
let negative_time: time::Duration = negative_duration.try_into().unwrap();
assert_eq!(positive_time, time::Duration::hours(1));
assert_eq!(negative_time, time::Duration::hours(-1));
}
#[test]
fn sntp_date_time_converting_to_system_time_works() {
let now = std::time::SystemTime::now();
let datetime = SntpDateTime::new(SntpDuration::from_secs_f64(3600.0));
let systemtime_1 = datetime.into_system_time().unwrap();
let systemtime_2 = now
.checked_add(std::time::Duration::from_secs_f64(3600.0))
.unwrap();
assert_eq!(
systemtime_1
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
systemtime_2
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
}
#[cfg(feature = "chrono")]
#[test]
fn sntp_date_time_converting_to_chrono_datetime_works() {
let datetime = SntpDateTime::new(SntpDuration::from_secs_f64(0.1));
let converted: chrono::DateTime<chrono::Utc> = datetime.try_into().unwrap();
let diff = converted - chrono::Utc::now();
assert!(diff.num_milliseconds() > 90);
assert!(diff.num_milliseconds() < 110);
}
#[cfg(feature = "chrono")]
#[test]
fn sntp_date_time_converting_to_chrono_datetime_fails_for_nan() {
let datetime = SntpDateTime::new(SntpDuration::from_secs_f64(f64::NAN));
let converted: Result<chrono::DateTime<chrono::Utc>, ConversionError> = datetime.try_into();
assert!(converted.is_err());
}
#[cfg(feature = "time")]
#[test]
fn sntp_date_time_converting_to_time_offset_datetime_works() {
let datetime = SntpDateTime::new(SntpDuration::from_secs_f64(0.1));
let converted: time::OffsetDateTime = datetime.try_into().unwrap();
let diff = converted - time::OffsetDateTime::now_utc();
assert!(diff.whole_milliseconds() > 90);
assert!(diff.whole_milliseconds() < 110);
}
}