use crate::error::{KissCode, ProtocolError, SynchroniztationError};
use crate::packet::{LeapIndicator, Mode, Packet, ReferenceIdentifier, SntpTimestamp};
use chrono::{DateTime, Duration, Utc};
#[derive(Debug, Clone)]
pub struct SynchronizationResult {
clock_offset: Duration,
round_trip_delay: Duration,
reference_identifier: ReferenceIdentifier,
leap_indicator: LeapIndicator,
stratum: u8,
}
impl SynchronizationResult {
pub fn clock_offset(&self) -> Duration {
self.clock_offset
}
pub fn round_trip_delay(&self) -> Duration {
self.round_trip_delay
}
pub fn reference_identifier(&self) -> &ReferenceIdentifier {
&self.reference_identifier
}
pub fn datetime(&self) -> DateTime<Utc> {
Utc::now() + self.clock_offset
}
pub fn leap_indicator(&self) -> LeapIndicator {
self.leap_indicator
}
pub fn stratum(&self) -> u8 {
self.stratum
}
}
pub struct Request {
packet: Packet,
}
impl Request {
pub fn new() -> Request {
Request {
packet: Packet {
li: LeapIndicator::NoWarning,
mode: Mode::Client,
stratum: 0,
reference_identifier: ReferenceIdentifier::Empty,
reference_timestamp: SntpTimestamp::zero(),
originate_timestamp: SntpTimestamp::zero(),
receive_timestamp: SntpTimestamp::zero(),
transmit_timestamp: SntpTimestamp::from_datetime(Utc::now()),
},
}
}
pub fn as_bytes(&self) -> [u8; Packet::ENCODED_LEN] {
self.packet.to_bytes()
}
fn into_packet(self) -> Packet {
self.packet
}
}
pub struct Reply {
request: Packet,
reply: Packet,
reply_timestamp: DateTime<Utc>,
}
impl Reply {
pub fn new(request: Request, reply: Packet) -> Reply {
Reply {
request: request.into_packet(),
reply,
reply_timestamp: Utc::now(),
}
}
fn check(&self) -> Result<(), ProtocolError> {
if self.reply.stratum == 0 {
return Err(ProtocolError::KissODeath(KissCode::new(
&self.reply.reference_identifier,
)));
}
if self.reply.originate_timestamp != self.request.transmit_timestamp {
return Err(ProtocolError::InvalidOriginateTimestamp);
}
if self.reply.transmit_timestamp.is_zero() {
return Err(ProtocolError::InvalidTransmitTimestamp);
}
if self.reply.mode != Mode::Server && self.reply.mode != Mode::Broadcast {
return Err(ProtocolError::InvalidMode);
}
Ok(())
}
pub fn process(self) -> Result<SynchronizationResult, SynchroniztationError> {
self.check()?;
let originate_ts = self.reply.originate_timestamp.to_datetime();
let transmit_ts = self.reply.transmit_timestamp.to_datetime();
let receive_ts = self.reply.receive_timestamp.to_datetime();
let round_trip_delay = (self.reply_timestamp - originate_ts) - (transmit_ts - receive_ts);
let clock_offset = ((receive_ts - originate_ts) + (transmit_ts - self.reply_timestamp)) / 2;
Ok(SynchronizationResult {
round_trip_delay,
clock_offset,
reference_identifier: self.reply.reference_identifier.clone(),
leap_indicator: self.reply.li,
stratum: self.reply.stratum,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_between {
($var: expr, $lower: expr, $upper: expr) => {
if $var < $lower || $var > $upper {
panic!(
"Assertion failed, {:?} is not between {:?} and {:?}",
$var, $lower, $upper
);
}
};
}
#[test]
fn basic_synchronization_works() {
let request = Request::new();
std::thread::sleep(Duration::milliseconds(100).to_std().unwrap());
let now = Utc::now();
std::thread::sleep(Duration::milliseconds(100).to_std().unwrap());
let reply_packet = Packet {
li: LeapIndicator::NoWarning,
mode: Mode::Server,
stratum: 1,
reference_identifier: ReferenceIdentifier::new_ascii([0x4c, 0x4f, 0x43, 0x4c]).unwrap(),
reference_timestamp: SntpTimestamp::from_datetime(now - Duration::days(1)),
originate_timestamp: request.packet.transmit_timestamp,
receive_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
transmit_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
};
let reply = Reply::new(request, reply_packet);
let result = reply.process().unwrap();
assert_between!(result.clock_offset().num_milliseconds(), -510, -490);
assert_between!(result.round_trip_delay().num_milliseconds(), 190, 210);
assert_eq!(result.reference_identifier().to_string(), "LOCL");
assert_eq!(result.leap_indicator(), LeapIndicator::NoWarning);
assert_eq!(result.stratum(), 1);
}
#[test]
fn sync_fails_if_reply_originate_ts_does_not_match_request_transmit_ts() {
let request = Request::new();
let now = Utc::now();
let reply_packet = Packet {
li: LeapIndicator::NoWarning,
mode: Mode::Server,
stratum: 1,
reference_identifier: ReferenceIdentifier::new_ascii([0x4c, 0x4f, 0x43, 0x4c]).unwrap(),
reference_timestamp: SntpTimestamp::from_datetime(now - Duration::days(1)),
originate_timestamp: SntpTimestamp::from_datetime(now),
receive_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
transmit_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
};
let reply = Reply::new(request, reply_packet);
let result = reply.process();
assert!(result.is_err());
}
#[test]
fn sync_fails_if_reply_contains_zero_transmit_timestamp() {
let request = Request::new();
let now = Utc::now();
let reply_packet = Packet {
li: LeapIndicator::NoWarning,
mode: Mode::Server,
stratum: 1,
reference_identifier: ReferenceIdentifier::new_ascii([0x4c, 0x4f, 0x43, 0x4c]).unwrap(),
reference_timestamp: SntpTimestamp::from_datetime(now - Duration::days(1)),
originate_timestamp: request.packet.transmit_timestamp,
receive_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
transmit_timestamp: SntpTimestamp::zero(),
};
let reply = Reply::new(request, reply_packet);
let result = reply.process();
assert!(result.is_err());
}
#[test]
fn sync_fails_if_reply_contains_wrong_mode() {
let request = Request::new();
let now = Utc::now();
let reply_packet = Packet {
li: LeapIndicator::NoWarning,
mode: Mode::Client,
stratum: 1,
reference_identifier: ReferenceIdentifier::new_ascii([0x4c, 0x4f, 0x43, 0x4c]).unwrap(),
reference_timestamp: SntpTimestamp::from_datetime(now - Duration::days(1)),
originate_timestamp: request.packet.transmit_timestamp,
receive_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
transmit_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
};
let reply = Reply::new(request, reply_packet);
let result = reply.process();
assert!(result.is_err());
}
#[test]
fn sync_fails_if_kiss_o_death_received() {
let request = Request::new();
let now = Utc::now();
let reply_packet = Packet {
li: LeapIndicator::NoWarning,
mode: Mode::Server,
stratum: 0,
reference_identifier: ReferenceIdentifier::new_ascii([0x52, 0x41, 0x54, 0x45]).unwrap(),
reference_timestamp: SntpTimestamp::from_datetime(now - Duration::days(1)),
originate_timestamp: request.packet.transmit_timestamp,
receive_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
transmit_timestamp: SntpTimestamp::from_datetime(now - Duration::milliseconds(500)),
};
let reply = Reply::new(request, reply_packet);
let err = reply.process().unwrap_err();
if let SynchroniztationError::ProtocolError(ProtocolError::KissODeath(
KissCode::RateExceeded,
)) = err
{
} else {
panic!("Wrong error received");
}
}
}