wireforge_app/ntp/
parser.rs1use core::time::Duration;
4
5use super::types::{NtpLeapIndicator, NtpMode};
6use wireforge_core::util::read_u32be;
7
8pub const NTP_PACKET_LEN: usize = 48;
9
10const NTP_EPOCH_DELTA: u32 = 2_208_988_800;
12
13#[derive(Debug, Clone)]
15pub struct NtpPacket<'a> {
16 buf: &'a [u8],
17}
18
19impl<'a> NtpPacket<'a> {
20 pub fn new(buf: &'a [u8]) -> Option<Self> {
21 if buf.len() < NTP_PACKET_LEN { return None; }
22 Some(Self { buf })
23 }
24
25 #[inline] pub fn leap(&self) -> NtpLeapIndicator { NtpLeapIndicator::from(self.buf[0] >> 6) }
26 #[inline] pub fn version(&self) -> u8 { (self.buf[0] >> 3) & 0x07 }
27 #[inline] pub fn mode(&self) -> NtpMode { NtpMode::from(self.buf[0] & 0x07) }
28 #[inline] pub fn stratum(&self) -> u8 { self.buf[1] }
29 #[inline] pub fn poll_interval(&self) -> i8 { self.buf[2] as i8 }
30 #[inline] pub fn precision(&self) -> i8 { self.buf[3] as i8 }
31 #[inline] pub fn root_delay(&self) -> Duration { ntp_short_to_duration(read_u32be(&self.buf[4..8])) }
32 #[inline] pub fn root_dispersion(&self) -> Duration { ntp_short_to_duration(read_u32be(&self.buf[8..12])) }
33 #[inline] pub fn reference_id(&self) -> [u8; 4] { [self.buf[12], self.buf[13], self.buf[14], self.buf[15]] }
34 #[inline] pub fn reference_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[16..20]), read_u32be(&self.buf[20..24])) }
35 #[inline] pub fn origin_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[24..28]), read_u32be(&self.buf[28..32])) }
36 #[inline] pub fn receive_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[32..36]), read_u32be(&self.buf[36..40])) }
37 #[inline] pub fn transmit_timestamp(&self) -> Option<Duration> { ntp_timestamp_to_duration(read_u32be(&self.buf[40..44]), read_u32be(&self.buf[44..48])) }
38}
39
40fn ntp_short_to_duration(val: u32) -> Duration {
42 let secs = (val >> 16) as u64;
43 let frac = (val & 0xFFFF) as u64;
44 Duration::new(secs, (frac * 1_000_000_000 / 65536) as u32)
45}
46
47fn ntp_timestamp_to_duration(seconds: u32, fraction: u32) -> Option<Duration> {
49 if seconds == 0 && fraction == 0 { return None; }
50 let secs = seconds.saturating_sub(NTP_EPOCH_DELTA) as u64;
51 let nanos = (((fraction as u64) * 1_000_000_000) >> 32) as u32;
52 Some(Duration::new(secs, nanos))
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn parse_ntp_client() {
61 let mut data = [0u8; 48];
62 data[0] = 0x1B; data[1] = 0; let pkt = NtpPacket::new(&data).unwrap();
66 assert_eq!(pkt.leap(), NtpLeapIndicator::NoWarning);
67 assert_eq!(pkt.version(), 3);
68 assert_eq!(pkt.mode(), NtpMode::Client);
69 assert_eq!(pkt.stratum(), 0);
70 }
71
72 #[test]
73 fn parse_ntp_server() {
74 let mut data = [0u8; 48];
75 data[0] = 0x24; data[1] = 2; let pkt = NtpPacket::new(&data).unwrap();
79 assert_eq!(pkt.version(), 4);
80 assert_eq!(pkt.mode(), NtpMode::Server);
81 assert_eq!(pkt.stratum(), 2);
82 }
83
84 #[test]
85 fn parse_ntp_too_short() {
86 assert!(NtpPacket::new(&[]).is_none());
87 assert!(NtpPacket::new(&[0u8; 47]).is_none());
88 assert!(NtpPacket::new(&[0u8; 48]).is_some());
89 }
90
91 #[test]
92 fn ntp_timestamp_zero_returns_none() {
93 let data = [0u8; 48];
94 let pkt = NtpPacket::new(&data).unwrap();
95 assert_eq!(pkt.transmit_timestamp(), None);
96 }
97}