use chrono::{DateTime, Local, Utc};
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
#[cfg(feature = "nts")]
use crate::adapters::nts_client;
use crate::adapters::{ntp_client, resolver};
use crate::domain::ntp::{ProbeResult, Target};
use crate::error::RkikError;
use rsntp::ReferenceIdentifier;
use tracing::instrument;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedTarget<'a> {
pub host: &'a str,
pub port: Option<u16>,
pub is_ipv6_literal: bool,
}
fn parse_port_strict(s: &str) -> Result<u16, RkikError> {
let raw = u32::from_str(s).map_err(|_| RkikError::Other(format!("invalid port: '{s}'")))?;
if raw == 0 || raw > u16::MAX as u32 {
return Err(RkikError::Other(format!(
"port out of range [1..65535]: {raw}"
)));
}
Ok(raw as u16)
}
#[inline]
fn colon_count(s: &str) -> usize {
s.as_bytes().iter().filter(|&&b| b == b':').count()
}
pub fn parse_target(input: &str) -> Result<ParsedTarget<'_>, RkikError> {
let s = input.trim();
if s.is_empty() {
return Err(RkikError::Other("empty target".into()));
}
if let Some(rest) = s.strip_prefix('[') {
let Some(bracket_pos) = rest.find(']') else {
return Err(RkikError::Other(format!("missing closing ']' in '{s}'")));
};
let host = &rest[..bracket_pos]; let tail = &rest[bracket_pos + 1..];
let port = if let Some(p) = tail.strip_prefix(':') {
Some(parse_port_strict(p)?)
} else if tail.is_empty() {
None
} else {
return Err(RkikError::Other(format!(
"unexpected trailing characters in '{s}'"
)));
};
return Ok(ParsedTarget {
host,
port,
is_ipv6_literal: true,
});
}
match colon_count(s) {
0 => Ok(ParsedTarget {
host: s,
port: None,
is_ipv6_literal: false,
}),
1 => {
let mut it = s.rsplitn(2, ':');
let port_str = it.next().unwrap();
let host = it.next().unwrap_or("");
if host.is_empty() {
return Err(RkikError::Other(format!(
"missing host before port in '{s}'"
)));
}
let port = parse_port_strict(port_str)?;
Ok(ParsedTarget {
host,
port: Some(port),
is_ipv6_literal: false,
})
}
_ => Ok(ParsedTarget {
host: s,
port: None,
is_ipv6_literal: true,
}),
}
}
fn format_reference_id(reference_id: &ReferenceIdentifier) -> String {
reference_id.to_string()
}
#[instrument(skip(timeout))]
pub async fn query_one(
target: &str,
mut ipv6: bool,
timeout: Duration,
use_nts: bool,
nts_port: u16,
) -> Result<ProbeResult, RkikError> {
#[cfg(feature = "nts")]
if use_nts {
let parsed = parse_target(target).map_err(|e| e.with_target(target))?;
let nts_result = nts_client::query_nts(parsed.host, Some(nts_port), timeout)
.await
.map_err(|e| e.with_target(target))?;
let ip: IpAddr =
resolver::resolve_ip(parsed.host, ipv6).map_err(|e| e.with_target(target))?;
let local: DateTime<Local> = DateTime::from(nts_result.network_time);
let timestamp = nts_result.network_time.timestamp();
return Ok(ProbeResult {
target: Target {
name: target.to_string(),
ip,
port: parsed.port.unwrap_or(123),
},
offset_ms: nts_result.offset_ms,
rtt_ms: nts_result.rtt_ms,
stratum: 0, ref_id: nts_result.server.clone(),
utc: nts_result.network_time,
local,
timestamp,
authenticated: nts_result.authenticated,
#[cfg(feature = "nts")]
nts_ke_data: nts_result.nts_ke_data,
#[cfg(feature = "nts")]
nts_validation: Some(nts_result.nts_validation),
});
}
#[cfg(not(feature = "nts"))]
if use_nts {
return Err(RkikError::Other(
"NTS support not enabled. Compile with --features nts".to_string(),
)
.with_target(target));
}
let parsed = parse_target(target).map_err(|e| e.with_target(target))?;
let ip: IpAddr = resolver::resolve_ip(parsed.host, ipv6).map_err(|e| e.with_target(target))?;
let port: u16 = parsed.port.unwrap_or(123);
if parsed.is_ipv6_literal {
ipv6 = true;
}
let res = ntp_client::query(ip, ipv6, timeout, port)
.await
.map_err(|e| e.with_target(target))?;
let utc: DateTime<Utc> = match res.datetime().try_into() {
Ok(dt) => dt,
Err(e) => return Err(RkikError::Other(e.to_string()).with_target(target)),
};
let local: DateTime<Local> = DateTime::from(utc);
let offset_ms = res.clock_offset().as_secs_f64() * 1000.0;
let rtt_ms = res.round_trip_delay().as_secs_f64() * 1000.0;
let stratum = res.stratum();
let ref_id = format_reference_id(res.reference_identifier());
let timestamp = utc.timestamp();
Ok(ProbeResult {
target: Target {
name: target.to_string(),
ip,
port,
},
offset_ms,
rtt_ms,
stratum,
ref_id,
utc,
local,
timestamp,
authenticated: false, #[cfg(feature = "nts")]
nts_ke_data: None, #[cfg(feature = "nts")]
nts_validation: None, })
}