use core::time::Duration;
use super::types::{NtpLeapIndicator, NtpMode};
use wireforge_core::util::read_u32be;
pub const NTP_PACKET_LEN: usize = 48;
const NTP_EPOCH_DELTA: u32 = 2_208_988_800;
#[derive(Debug, Clone)]
pub struct NtpPacket<'a> {
buf: &'a [u8],
}
impl<'a> NtpPacket<'a> {
pub fn new(buf: &'a [u8]) -> Option<Self> {
if buf.len() < NTP_PACKET_LEN { return None; }
Some(Self { buf })
}
#[inline] pub fn leap(&self) -> NtpLeapIndicator { NtpLeapIndicator::from(self.buf[0] >> 6) }
#[inline] pub fn version(&self) -> u8 { (self.buf[0] >> 3) & 0x07 }
#[inline] pub fn mode(&self) -> NtpMode { NtpMode::from(self.buf[0] & 0x07) }
#[inline] pub fn stratum(&self) -> u8 { self.buf[1] }
#[inline] pub fn poll_interval(&self) -> i8 { self.buf[2] as i8 }
#[inline] pub fn precision(&self) -> i8 { self.buf[3] as i8 }
#[inline] pub fn root_delay(&self) -> Duration { ntp_short_to_duration(read_u32be(&self.buf[4..8])) }
#[inline] pub fn root_dispersion(&self) -> Duration { ntp_short_to_duration(read_u32be(&self.buf[8..12])) }
#[inline] pub fn reference_id(&self) -> [u8; 4] { [self.buf[12], self.buf[13], self.buf[14], self.buf[15]] }
#[inline] pub fn reference_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[16..20]), read_u32be(&self.buf[20..24])) }
#[inline] pub fn origin_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[24..28]), read_u32be(&self.buf[28..32])) }
#[inline] pub fn receive_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[32..36]), read_u32be(&self.buf[36..40])) }
#[inline] pub fn transmit_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[40..44]), read_u32be(&self.buf[44..48])) }
}
fn ntp_short_to_duration(val: u32) -> Duration {
let secs = (val >> 16) as u64;
let frac = (val & 0xFFFF) as u64;
Duration::new(secs, (frac * 1_000_000_000 / 65536) as u32)
}
fn ntp_timestamp_to_duration(seconds: u32, fraction: u32) -> Option<Duration> {
if seconds == 0 && fraction == 0 { return None; }
let secs = seconds.saturating_sub(NTP_EPOCH_DELTA) as u64;
let nanos = (((fraction as u64) * 1_000_000_000) >> 32) as u32;
Some(Duration::new(secs, nanos))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_ntp_client() {
let mut data = [0u8; 48];
data[0] = 0x1B; data[1] = 0;
let pkt = NtpPacket::new(&data).unwrap();
assert_eq!(pkt.leap(), NtpLeapIndicator::NoWarning);
assert_eq!(pkt.version(), 3);
assert_eq!(pkt.mode(), NtpMode::Client);
assert_eq!(pkt.stratum(), 0);
}
#[test]
fn parse_ntp_server() {
let mut data = [0u8; 48];
data[0] = 0x24; data[1] = 2;
let pkt = NtpPacket::new(&data).unwrap();
assert_eq!(pkt.version(), 4);
assert_eq!(pkt.mode(), NtpMode::Server);
assert_eq!(pkt.stratum(), 2);
}
#[test]
fn parse_ntp_too_short() {
assert!(NtpPacket::new(&[]).is_none());
assert!(NtpPacket::new(&[0u8; 47]).is_none());
assert!(NtpPacket::new(&[0u8; 48]).is_some());
}
#[test]
fn ntp_timestamp_zero_returns_none() {
let data = [0u8; 48];
let pkt = NtpPacket::new(&data).unwrap();
assert_eq!(pkt.transmit_timestamp(), None);
}
}