Skip to main content

wireforge_app/ntp/
parser.rs

1//! NTPv4 packet parser (RFC 5905).
2
3use core::time::Duration;
4
5use super::types::{NtpLeapIndicator, NtpMode};
6use wireforge_core::util::read_u32be;
7
8pub const NTP_PACKET_LEN: usize = 48;
9
10/// Seconds between NTP epoch (1900-01-01) and Unix epoch (1970-01-01).
11const NTP_EPOCH_DELTA: u32 = 2_208_988_800;
12
13/// Zero-copy NTP packet parser.
14#[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
40/// Convert NTP short format (32-bit: 16s + 16 fraction) to Duration.
41fn 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
47/// Convert NTP timestamp (64-bit: 32s + 32 fraction) to Duration since Unix epoch.
48fn 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; // LI=0, VN=3, Mode=3 (client)
63        data[1] = 0;    // stratum=0 (unspecified)
64
65        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; // LI=0, VN=4, Mode=4 (server)
76        data[1] = 2;    // stratum=2
77
78        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}