1use wasm_bindgen::prelude::*;
31
32use ntp_proto::extension::iter_extension_fields;
33use ntp_proto::protocol::{
34 self, ConstPackedSizeBytes, FromBytes, LeapIndicator, Mode, Packet, Stratum, ToBytes, Version,
35};
36use ntp_proto::unix_time;
37
38#[wasm_bindgen(js_name = "ntpPacketSize")]
40pub fn ntp_packet_size() -> usize {
41 Packet::PACKED_SIZE_BYTES
42}
43
44#[wasm_bindgen]
46pub struct NtpPacket {
47 inner: Packet,
48}
49
50#[wasm_bindgen]
51impl NtpPacket {
52 #[wasm_bindgen(constructor)]
56 pub fn new(bytes: &[u8]) -> Result<NtpPacket, JsError> {
57 let (packet, _consumed) =
58 Packet::from_bytes(bytes).map_err(|e| JsError::new(&format!("{e}")))?;
59 Ok(NtpPacket { inner: packet })
60 }
61
62 #[wasm_bindgen(getter, js_name = "leapIndicator")]
64 pub fn leap_indicator(&self) -> u8 {
65 self.inner.leap_indicator as u8
66 }
67
68 #[wasm_bindgen(getter)]
70 pub fn version(&self) -> u8 {
71 self.inner.version.value()
72 }
73
74 #[wasm_bindgen(getter)]
76 pub fn mode(&self) -> u8 {
77 self.inner.mode as u8
78 }
79
80 #[wasm_bindgen(getter)]
82 pub fn stratum(&self) -> u8 {
83 self.inner.stratum.0
84 }
85
86 #[wasm_bindgen(getter)]
88 pub fn poll(&self) -> i8 {
89 self.inner.poll
90 }
91
92 #[wasm_bindgen(getter)]
94 pub fn precision(&self) -> i8 {
95 self.inner.precision
96 }
97
98 #[wasm_bindgen(getter, js_name = "rootDelay")]
100 pub fn root_delay(&self) -> f64 {
101 let sf = &self.inner.root_delay;
102 sf.seconds as f64 + sf.fraction as f64 / 65536.0
103 }
104
105 #[wasm_bindgen(getter, js_name = "rootDispersion")]
107 pub fn root_dispersion(&self) -> f64 {
108 let sf = &self.inner.root_dispersion;
109 sf.seconds as f64 + sf.fraction as f64 / 65536.0
110 }
111
112 #[wasm_bindgen(getter, js_name = "referenceId")]
114 pub fn reference_id(&self) -> Vec<u8> {
115 self.inner.reference_id.as_bytes().to_vec()
116 }
117
118 #[wasm_bindgen(getter, js_name = "referenceTimestamp")]
120 pub fn reference_timestamp(&self) -> Vec<u32> {
121 let ts = &self.inner.reference_timestamp;
122 vec![ts.seconds, ts.fraction]
123 }
124
125 #[wasm_bindgen(getter, js_name = "originTimestamp")]
127 pub fn origin_timestamp(&self) -> Vec<u32> {
128 let ts = &self.inner.origin_timestamp;
129 vec![ts.seconds, ts.fraction]
130 }
131
132 #[wasm_bindgen(getter, js_name = "receiveTimestamp")]
134 pub fn receive_timestamp(&self) -> Vec<u32> {
135 let ts = &self.inner.receive_timestamp;
136 vec![ts.seconds, ts.fraction]
137 }
138
139 #[wasm_bindgen(getter, js_name = "transmitTimestamp")]
141 pub fn transmit_timestamp(&self) -> Vec<u32> {
142 let ts = &self.inner.transmit_timestamp;
143 vec![ts.seconds, ts.fraction]
144 }
145
146 #[wasm_bindgen(js_name = "toBytes")]
148 pub fn to_bytes_js(&self) -> Result<Vec<u8>, JsError> {
149 let mut buf = vec![0u8; Packet::PACKED_SIZE_BYTES];
150 self.inner
151 .to_bytes(&mut buf)
152 .map_err(|e| JsError::new(&format!("{e}")))?;
153 Ok(buf)
154 }
155
156 #[wasm_bindgen(js_name = "toString")]
158 pub fn to_string_js(&self) -> String {
159 format!("{:?}", self.inner)
160 }
161
162 #[wasm_bindgen(js_name = "clientRequest")]
164 pub fn client_request() -> NtpPacket {
165 NtpPacket {
166 inner: Packet::default(),
167 }
168 }
169
170 #[wasm_bindgen(js_name = "setVersion")]
172 pub fn set_version(&mut self, v: u8) -> Result<(), JsError> {
173 self.inner.version =
174 Version::new(v).ok_or_else(|| JsError::new(&format!("invalid version: {v}")))?;
175 Ok(())
176 }
177
178 #[wasm_bindgen(js_name = "setMode")]
180 pub fn set_mode(&mut self, m: u8) -> Result<(), JsError> {
181 self.inner.mode = match m {
182 0 => Mode::Reserved,
183 1 => Mode::SymmetricActive,
184 2 => Mode::SymmetricPassive,
185 3 => Mode::Client,
186 4 => Mode::Server,
187 5 => Mode::Broadcast,
188 6 => Mode::NtpControlMessage,
189 7 => Mode::ReservedForPrivateUse,
190 _ => return Err(JsError::new(&format!("invalid mode: {m}"))),
191 };
192 Ok(())
193 }
194
195 #[wasm_bindgen(js_name = "setStratum")]
197 pub fn set_stratum(&mut self, s: u8) {
198 self.inner.stratum = Stratum(s);
199 }
200
201 #[wasm_bindgen(js_name = "setPoll")]
203 pub fn set_poll(&mut self, p: i8) {
204 self.inner.poll = p;
205 }
206
207 #[wasm_bindgen(js_name = "setPrecision")]
209 pub fn set_precision(&mut self, p: i8) {
210 self.inner.precision = p;
211 }
212
213 #[wasm_bindgen(js_name = "setTransmitTimestamp")]
215 pub fn set_transmit_timestamp(&mut self, seconds: u32, fraction: u32) {
216 self.inner.transmit_timestamp = protocol::TimestampFormat { seconds, fraction };
217 }
218
219 #[wasm_bindgen(js_name = "setOriginTimestamp")]
221 pub fn set_origin_timestamp(&mut self, seconds: u32, fraction: u32) {
222 self.inner.origin_timestamp = protocol::TimestampFormat { seconds, fraction };
223 }
224
225 #[wasm_bindgen(js_name = "setReceiveTimestamp")]
227 pub fn set_receive_timestamp(&mut self, seconds: u32, fraction: u32) {
228 self.inner.receive_timestamp = protocol::TimestampFormat { seconds, fraction };
229 }
230
231 #[wasm_bindgen(js_name = "setReferenceTimestamp")]
233 pub fn set_reference_timestamp(&mut self, seconds: u32, fraction: u32) {
234 self.inner.reference_timestamp = protocol::TimestampFormat { seconds, fraction };
235 }
236
237 #[wasm_bindgen(js_name = "setLeapIndicator")]
239 pub fn set_leap_indicator(&mut self, li: u8) -> Result<(), JsError> {
240 self.inner.leap_indicator = match li {
241 0 => LeapIndicator::NoWarning,
242 1 => LeapIndicator::AddOne,
243 2 => LeapIndicator::SubOne,
244 3 => LeapIndicator::Unknown,
245 _ => return Err(JsError::new(&format!("invalid leap indicator: {li}"))),
246 };
247 Ok(())
248 }
249}
250
251#[wasm_bindgen(js_name = "buildClientRequest")]
253pub fn build_client_request() -> Result<Vec<u8>, JsError> {
254 let request = Packet::default();
255 let mut buf = vec![0u8; Packet::PACKED_SIZE_BYTES];
256 request
257 .to_bytes(&mut buf)
258 .map_err(|e| JsError::new(&format!("{e}")))?;
259 Ok(buf)
260}
261
262#[wasm_bindgen(js_name = "ntpTimestampToUnixSeconds")]
268pub fn ntp_timestamp_to_unix_seconds(
269 ntp_seconds: u32,
270 ntp_fraction: u32,
271 pivot_unix_seconds: f64,
272) -> f64 {
273 let pivot = unix_time::Instant::new(pivot_unix_seconds as i64, 0)
274 .expect("pivot with zero nanos is always valid");
275 let ts = protocol::TimestampFormat {
276 seconds: ntp_seconds,
277 fraction: ntp_fraction,
278 };
279 let instant = unix_time::timestamp_to_instant(ts, &pivot);
280 instant.secs() as f64 + (instant.subsec_nanos() as f64 / 1e9)
281}
282
283#[wasm_bindgen(js_name = "unixSecondsToNtpTimestamp")]
287pub fn unix_seconds_to_ntp_timestamp(unix_seconds: f64) -> Vec<u32> {
288 let secs = unix_seconds.trunc() as i64;
289 let nanos = ((unix_seconds.fract()) * 1e9) as i32;
290 let instant = unix_time::Instant::new(secs, nanos)
291 .expect("trunc/fract decomposition produces same-sign components");
292 let ts: protocol::TimestampFormat = instant.into();
293 vec![ts.seconds, ts.fraction]
294}
295
296#[wasm_bindgen(js_name = "parseExtensionFields")]
300pub fn parse_extension_fields_js(data: &[u8]) -> Result<JsValue, JsError> {
301 let arr = js_sys::Array::new();
302 for result in iter_extension_fields(data) {
303 let ef: ntp_proto::extension::ExtensionFieldRef<'_> =
304 result.map_err(|e| JsError::new(&format!("{e}")))?;
305 let obj = js_sys::Object::new();
306 js_sys::Reflect::set(&obj, &"fieldType".into(), &ef.field_type.into())
307 .map_err(|e| JsError::new(&format!("{e:?}")))?;
308 let value = js_sys::Uint8Array::from(ef.value);
309 js_sys::Reflect::set(&obj, &"value".into(), &value.into())
310 .map_err(|e| JsError::new(&format!("{e:?}")))?;
311 arr.push(&obj.into());
312 }
313 Ok(arr.into())
314}
315
316#[wasm_bindgen(js_name = "computeOffsetDelay")]
327#[allow(clippy::too_many_arguments)]
328pub fn compute_offset_delay(
329 t1_seconds: u32,
330 t1_fraction: u32,
331 t2_seconds: u32,
332 t2_fraction: u32,
333 t3_seconds: u32,
334 t3_fraction: u32,
335 t4_seconds: u32,
336 t4_fraction: u32,
337 pivot_unix_seconds: f64,
338) -> Result<JsValue, JsError> {
339 let pivot = unix_time::Instant::new(pivot_unix_seconds as i64, 0)
340 .expect("pivot with zero nanos is always valid");
341
342 let t1 = unix_time::timestamp_to_instant(
343 protocol::TimestampFormat {
344 seconds: t1_seconds,
345 fraction: t1_fraction,
346 },
347 &pivot,
348 );
349 let t2 = unix_time::timestamp_to_instant(
350 protocol::TimestampFormat {
351 seconds: t2_seconds,
352 fraction: t2_fraction,
353 },
354 &pivot,
355 );
356 let t3 = unix_time::timestamp_to_instant(
357 protocol::TimestampFormat {
358 seconds: t3_seconds,
359 fraction: t3_fraction,
360 },
361 &pivot,
362 );
363 let t4 = unix_time::timestamp_to_instant(
364 protocol::TimestampFormat {
365 seconds: t4_seconds,
366 fraction: t4_fraction,
367 },
368 &pivot,
369 );
370
371 let t1f = t1.secs() as f64 + t1.subsec_nanos() as f64 / 1e9;
373 let t2f = t2.secs() as f64 + t2.subsec_nanos() as f64 / 1e9;
374 let t3f = t3.secs() as f64 + t3.subsec_nanos() as f64 / 1e9;
375 let t4f = t4.secs() as f64 + t4.subsec_nanos() as f64 / 1e9;
376
377 let offset = ((t2f - t1f) + (t3f - t4f)) / 2.0;
378 let delay = (t4f - t1f) - (t3f - t2f);
379
380 let obj = js_sys::Object::new();
381 js_sys::Reflect::set(&obj, &"offset".into(), &offset.into())
382 .map_err(|e| JsError::new(&format!("{e:?}")))?;
383 js_sys::Reflect::set(&obj, &"delay".into(), &delay.into())
384 .map_err(|e| JsError::new(&format!("{e:?}")))?;
385 Ok(obj.into())
386}
387
388#[wasm_bindgen(js_name = "validateResponse")]
404pub fn validate_response(
405 bytes: &[u8],
406 origin_t1_seconds: Option<u32>,
407 origin_t1_fraction: Option<u32>,
408) -> Result<JsValue, JsError> {
409 if bytes.len() < Packet::PACKED_SIZE_BYTES {
411 return Ok(JsValue::from_str(&format!(
412 "packet too short: {} bytes (need {})",
413 bytes.len(),
414 Packet::PACKED_SIZE_BYTES
415 )));
416 }
417 let (packet, _) = match Packet::from_bytes(bytes) {
418 Ok(p) => p,
419 Err(e) => return Ok(JsValue::from_str(&format!("parse error: {e}"))),
420 };
421
422 if packet.mode != Mode::Server {
424 return Ok(JsValue::from_str(&format!(
425 "unexpected mode: {} (expected 4/Server)",
426 packet.mode as u8
427 )));
428 }
429
430 if packet.stratum.0 == 0 {
432 let code = packet.reference_id.as_bytes();
433 let kiss_code = core::str::from_utf8(&code)
434 .unwrap_or("????")
435 .trim_end_matches('\0');
436 return Ok(JsValue::from_str(&format!("Kiss-o'-Death: {kiss_code}")));
437 }
438
439 if packet.transmit_timestamp.seconds == 0 && packet.transmit_timestamp.fraction == 0 {
441 return Ok(JsValue::from_str("transmit timestamp is zero"));
442 }
443
444 if let (Some(t1s), Some(t1f)) = (origin_t1_seconds, origin_t1_fraction)
446 && (packet.origin_timestamp.seconds != t1s || packet.origin_timestamp.fraction != t1f)
447 {
448 return Ok(JsValue::from_str(&format!(
449 "origin timestamp mismatch: expected [{t1s}, {t1f}], got [{}, {}]",
450 packet.origin_timestamp.seconds, packet.origin_timestamp.fraction
451 )));
452 }
453
454 Ok(JsValue::NULL)
456}