#[cfg(test)]
use std::sync::Mutex;
use std::{
fmt,
net::{IpAddr, SocketAddr},
ops::Deref,
path::PathBuf,
sync::Arc,
};
use ntp_proto::{PollInterval, PollIntervalLimits, SourceConfig};
use ntp_proto::{ProtocolVersion, tls_utils::Certificate};
use serde::{
Deserialize, Deserializer,
de::{self, Visitor},
};
use super::super::keyexchange::certificates_from_file;
fn deserialize_ntp_version<'de, D>(deserializer: D) -> Result<ProtocolVersion, D::Error>
where
D: Deserializer<'de>,
{
struct ProtocolVersionVisitor;
impl Visitor<'_> for ProtocolVersionVisitor {
type Value = ProtocolVersion;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(r#"4 or 5 as an integer, or the special string value "auto""#)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
match v {
4 => Ok(ProtocolVersion::V4),
5 => Ok(ProtocolVersion::V5),
_ => Err(E::custom(r#"Version must be 4, 5 or "auto""#)),
}
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_u64(v.try_into().map_err(E::custom)?)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match v {
"auto" => Ok(ProtocolVersion::v4_upgrading_to_v5_with_default_tries()),
_ => Err(E::custom(r#"Version must be 4, 5 or "auto""#)),
}
}
}
deserializer.deserialize_any(ProtocolVersionVisitor)
}
fn default_ntp_version() -> ProtocolVersion {
ProtocolVersion::V4
}
#[derive(Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct StandardSource {
pub address: NtpAddress,
#[serde(
default = "default_ntp_version",
deserialize_with = "deserialize_ntp_version"
)]
pub ntp_version: ProtocolVersion,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct NtsSourceConfig {
pub address: NtsKeAddress,
#[serde(
deserialize_with = "deserialize_certificate_authorities",
default = "default_certificate_authorities",
rename = "certificate-authority"
)]
pub certificate_authorities: Arc<[Certificate]>,
#[serde(
default = "default_ntp_version",
deserialize_with = "deserialize_ntp_version"
)]
pub ntp_version: ProtocolVersion,
}
fn deserialize_certificate_authorities<'de, D>(
deserializer: D,
) -> Result<Arc<[Certificate]>, D::Error>
where
D: Deserializer<'de>,
{
let certificate_path: PathBuf = PathBuf::deserialize(deserializer)?;
match certificates_from_file(&certificate_path) {
Ok(certificates) => Ok(Arc::from(certificates)),
Err(io_error) => {
let msg =
format!("error while parsing certificate file {certificate_path:?}: {io_error:?}");
Err(de::Error::custom(msg))
}
}
}
fn default_certificate_authorities() -> Arc<[Certificate]> {
Arc::from([])
}
#[derive(Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PoolSourceConfig {
#[serde(rename = "address")]
pub addr: NtpAddress,
#[serde(default = "max_sources_default")]
pub count: usize,
#[serde(default)]
pub ignore: Vec<IpAddr>,
#[serde(
default = "default_ntp_version",
deserialize_with = "deserialize_ntp_version"
)]
pub ntp_version: ProtocolVersion,
}
fn max_sources_default() -> usize {
4
}
#[derive(Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct NtsPoolSourceConfig {
#[serde(rename = "address")]
pub addr: NtsKeAddress,
#[serde(
deserialize_with = "deserialize_certificate_authorities",
default = "default_certificate_authorities",
rename = "certificate-authority"
)]
pub certificate_authorities: Arc<[Certificate]>,
#[serde(default = "max_sources_default")]
pub count: usize,
#[serde(
default = "default_ntp_version",
deserialize_with = "deserialize_ntp_version"
)]
pub ntp_version: ProtocolVersion,
}
#[derive(Debug, PartialEq, Clone)]
pub struct SockSourceConfig {
pub path: PathBuf,
pub precision: f64,
pub accuracy: f64,
}
impl<'de> Deserialize<'de> for SockSourceConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Path,
Precision,
Accuracy,
MeasurementNoiseEstimate,
}
struct SockSourceConfigVisitor;
impl<'de> serde::de::Visitor<'de> for SockSourceConfigVisitor {
type Value = SockSourceConfig;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("struct SockSourceConfig")
}
fn visit_map<V>(self, mut map: V) -> Result<SockSourceConfig, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut path = None;
let mut precision = None;
let mut accuracy = None;
while let Some(key) = map.next_key()? {
match key {
Field::Path => {
if path.is_some() {
return Err(de::Error::duplicate_field("path"));
}
path = Some(map.next_value()?);
}
Field::MeasurementNoiseEstimate => {
tracing::warn!(
"The measurement_noise_estimate field is deprecated. Please switch to using the precision field"
);
if precision.is_some() {
return Err(de::Error::duplicate_field(
"measurement_noise_estimate",
));
}
let variance: f64 = map.next_value()?;
if variance.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) {
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(variance),
&"measurement_noise_estimate should be positive",
));
}
precision = Some(variance.sqrt());
}
Field::Precision => {
if precision.is_some() {
return Err(de::Error::duplicate_field("precision"));
}
let precision_raw: f64 = map.next_value()?;
if precision_raw.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater)
{
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(precision_raw),
&"precision should be positive",
));
}
precision = Some(precision_raw);
}
Field::Accuracy => {
if accuracy.is_some() {
return Err(de::Error::duplicate_field("accuracy"));
}
let accuracy_raw: f64 = map.next_value()?;
if accuracy_raw.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater)
{
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(accuracy_raw),
&"precision should be positive",
));
}
accuracy = Some(accuracy_raw);
}
}
}
let path = path.ok_or_else(|| serde::de::Error::missing_field("path"))?;
let precision =
precision.ok_or_else(|| serde::de::Error::missing_field("precision"))?;
let accuracy = accuracy.unwrap_or(0.0);
Ok(SockSourceConfig {
path,
precision,
accuracy,
})
}
}
const FIELDS: &[&str] = &[
"path",
"precision",
"accuracy",
"measurement_noise_estimate",
];
deserializer.deserialize_struct("SockSourceConfig", FIELDS, SockSourceConfigVisitor)
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct PartialPollIntervalLimits {
pub min: Option<PollInterval>,
pub max: Option<PollInterval>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PartialSourceConfig {
#[serde(default)]
pub poll_interval_limits: PartialPollIntervalLimits,
pub initial_poll_interval: Option<PollInterval>,
}
impl PartialSourceConfig {
pub fn with_defaults(self, defaults: SourceConfig) -> SourceConfig {
SourceConfig {
poll_interval_limits: PollIntervalLimits {
min: self
.poll_interval_limits
.min
.unwrap_or(defaults.poll_interval_limits.min),
max: self
.poll_interval_limits
.max
.unwrap_or(defaults.poll_interval_limits.max),
},
initial_poll_interval: self
.initial_poll_interval
.unwrap_or(defaults.initial_poll_interval),
}
}
}
#[derive(Deserialize, Debug, PartialEq, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct FlattenedPair<T, U> {
#[serde(flatten)]
pub first: T,
#[serde(flatten)]
pub second: U,
}
#[derive(Debug, PartialEq, Clone)]
pub struct PpsSourceConfig {
pub path: PathBuf,
pub precision: f64,
pub accuracy: f64,
pub period: f64,
}
impl<'de> Deserialize<'de> for PpsSourceConfig {
#[expect(clippy::too_many_lines, reason = "Deserializers can be a bit wordy")]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Path,
Precision,
Accuracy,
MeasurementNoiseEstimate,
Period,
}
struct PpsSourceConfigVisitor;
impl<'de> serde::de::Visitor<'de> for PpsSourceConfigVisitor {
type Value = PpsSourceConfig;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("struct PpsSourceConfig")
}
fn visit_map<V>(self, mut map: V) -> Result<PpsSourceConfig, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut path = None;
let mut precision = None;
let mut accuracy = None;
let mut period = None;
while let Some(key) = map.next_key()? {
match key {
Field::Path => {
if path.is_some() {
return Err(de::Error::duplicate_field("path"));
}
path = Some(map.next_value()?);
}
Field::MeasurementNoiseEstimate => {
tracing::warn!(
"The measurement_noise_estimate field is deprecated. Please switch to using the precision field"
);
if precision.is_some() {
return Err(de::Error::duplicate_field(
"measurement_noise_estimate",
));
}
let variance: f64 = map.next_value()?;
if variance.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) {
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(variance),
&"measurement_noise_estimate should be positive",
));
}
precision = Some(variance.sqrt());
}
Field::Precision => {
if precision.is_some() {
return Err(de::Error::duplicate_field("precision"));
}
let precision_raw: f64 = map.next_value()?;
if precision_raw.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater)
{
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(precision_raw),
&"measurement_noise_estimate should be positive",
));
}
precision = Some(precision_raw);
}
Field::Accuracy => {
if accuracy.is_some() {
return Err(de::Error::duplicate_field("accuracy"));
}
let accuracy_raw: f64 = map.next_value()?;
if accuracy_raw.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater)
{
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(accuracy_raw),
&"precision should be positive",
));
}
accuracy = Some(accuracy_raw);
}
Field::Period => {
if period.is_some() {
return Err(de::Error::duplicate_field("period"));
}
let period_raw: f64 = map.next_value()?;
if period_raw.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) {
return Err(de::Error::invalid_value(
serde::de::Unexpected::Float(period_raw),
&"period should be positive",
));
}
period = Some(period_raw);
}
}
}
let path = path.ok_or_else(|| serde::de::Error::missing_field("path"))?;
let precision =
precision.ok_or_else(|| serde::de::Error::missing_field("precision"))?;
let accuracy = accuracy.unwrap_or(0.0);
let period = period.unwrap_or(1.0);
Ok(PpsSourceConfig {
path,
precision,
accuracy,
period,
})
}
}
const FIELDS: &[&str] = &["path", "precision", "measurement_noise_estimate"];
deserializer.deserialize_struct("PpsSourceConfig", FIELDS, PpsSourceConfigVisitor)
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(tag = "mode")]
pub enum NtpSourceConfig {
#[serde(rename = "server")]
Standard(FlattenedPair<StandardSource, PartialSourceConfig>),
#[serde(rename = "nts")]
Nts(FlattenedPair<NtsSourceConfig, PartialSourceConfig>),
#[serde(rename = "pool")]
Pool(FlattenedPair<PoolSourceConfig, PartialSourceConfig>),
#[serde(rename = "nts-pool")]
NtsPool(FlattenedPair<NtsPoolSourceConfig, PartialSourceConfig>),
#[serde(rename = "sock")]
Sock(SockSourceConfig),
#[cfg(feature = "pps")]
#[serde(rename = "pps")]
Pps(PpsSourceConfig),
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct NormalizedAddress {
pub(crate) server_name: String,
pub(crate) port: u16,
#[cfg(test)]
hardcoded_dns_resolve: HardcodedDnsResolve,
}
impl Eq for NormalizedAddress {}
impl PartialEq for NormalizedAddress {
fn eq(&self, other: &Self) -> bool {
self.server_name == other.server_name && self.port == other.port
}
}
#[cfg(test)]
#[derive(Deserialize, Debug, Clone, Default)]
struct HardcodedDnsResolve {
#[cfg_attr(not(test), allow(unused))]
#[serde(skip)]
addresses: Arc<Mutex<Vec<SocketAddr>>>,
}
#[cfg(test)]
impl From<Vec<SocketAddr>> for HardcodedDnsResolve {
fn from(value: Vec<SocketAddr>) -> Self {
Self {
addresses: Arc::new(Mutex::new(value)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NtpAddress(pub NormalizedAddress);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NtsKeAddress(pub NormalizedAddress);
impl<'de> Deserialize<'de> for NtpAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(NormalizedAddress::from_string_ntp(s)
.map_err(serde::de::Error::custom)?
.into())
}
}
impl<'de> Deserialize<'de> for NtsKeAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(NtsKeAddress(
NormalizedAddress::from_string_nts_ke(s).map_err(serde::de::Error::custom)?,
))
}
}
impl From<NormalizedAddress> for NtpAddress {
fn from(addr: NormalizedAddress) -> Self {
Self(addr)
}
}
impl From<NormalizedAddress> for NtsKeAddress {
fn from(addr: NormalizedAddress) -> Self {
Self(addr)
}
}
impl Deref for NtsKeAddress {
type Target = NormalizedAddress;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Deref for NtpAddress {
type Target = NormalizedAddress;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl NormalizedAddress {
const NTP_DEFAULT_PORT: u16 = 123;
const NTS_KE_DEFAULT_PORT: u16 = 4460;
pub(crate) fn from_string_ntp(address: String) -> std::io::Result<Self> {
let (server_name, port) = Self::from_string_help(address, Self::NTP_DEFAULT_PORT)?;
Ok(Self {
server_name,
port,
#[cfg(test)]
hardcoded_dns_resolve: HardcodedDnsResolve::default(),
})
}
fn from_string_nts_ke(address: String) -> std::io::Result<Self> {
let (server_name, port) = Self::from_string_help(address, Self::NTS_KE_DEFAULT_PORT)?;
Ok(Self {
server_name,
port,
#[cfg(test)]
hardcoded_dns_resolve: HardcodedDnsResolve::default(),
})
}
fn from_string_help(address: String, default_port: u16) -> std::io::Result<(String, u16)> {
if address.split(':').count() > 2 {
match address.parse::<SocketAddr>() {
Ok(socket_addr) => Ok((socket_addr.ip().to_string(), socket_addr.port())),
Err(e) => {
let address_with_port = format!("[{address}]:{default_port}");
if let Ok(socket_addr) = address_with_port.parse::<SocketAddr>() {
Ok((socket_addr.ip().to_string(), socket_addr.port()))
} else {
Err(std::io::Error::other(e))
}
}
}
} else if let Some((server_name, port)) = address.split_once(':') {
match port.parse::<u16>() {
Ok(port) => Ok((server_name.to_string(), port)),
Err(e) => Err(std::io::Error::other(e)),
}
} else {
Ok((address, default_port))
}
}
#[cfg(test)]
pub(crate) fn new_unchecked(server_name: &str, port: u16) -> Self {
Self {
server_name: server_name.to_string(),
port,
#[cfg(test)]
hardcoded_dns_resolve: HardcodedDnsResolve::default(),
}
}
#[cfg(test)]
pub(crate) fn with_hardcoded_dns(
server_name: &str,
port: u16,
hardcoded_dns_resolve: Vec<SocketAddr>,
) -> Self {
Self {
server_name: server_name.to_string(),
port,
hardcoded_dns_resolve: HardcodedDnsResolve::from(hardcoded_dns_resolve),
}
}
#[cfg(not(test))]
pub async fn lookup_host(&self) -> std::io::Result<impl Iterator<Item = SocketAddr> + '_> {
tokio::net::lookup_host((self.server_name.as_str(), self.port)).await
}
#[cfg(test)]
pub async fn lookup_host(&self) -> std::io::Result<impl Iterator<Item = SocketAddr> + '_> {
let mut addresses = self.hardcoded_dns_resolve.addresses.lock().unwrap();
if let Some(last) = addresses.pop() {
addresses.insert(0, last);
}
let addresses = addresses.to_vec();
Ok(addresses.into_iter())
}
}
impl std::fmt::Display for NormalizedAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.server_name.contains(':') {
write!(f, "[{}]:{}", self.server_name, self.port)
} else {
write!(f, "{}:{}", self.server_name, self.port)
}
}
}
impl TryFrom<&str> for StandardSource {
type Error = std::io::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self {
address: NormalizedAddress::from_string_ntp(value.to_string())?.into(),
ntp_version: default_ntp_version(),
})
}
}
impl<'a> TryFrom<&'a str> for NtpSourceConfig {
type Error = std::io::Error;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
StandardSource::try_from(value).map(|first| {
Self::Standard(FlattenedPair {
first,
second: PartialSourceConfig::default(),
})
})
}
}
#[cfg(test)]
#[expect(
clippy::too_many_lines,
reason = "Long tests are not really a big problem"
)]
mod tests {
use super::*;
#[derive(Deserialize, Debug)]
struct TestConfig {
source: NtpSourceConfig,
}
fn source_addr(config: &NtpSourceConfig) -> String {
match config {
NtpSourceConfig::Standard(c) => c.first.address.to_string(),
NtpSourceConfig::Nts(c) => c.first.address.to_string(),
NtpSourceConfig::Pool(c) => c.first.addr.to_string(),
NtpSourceConfig::NtsPool(c) => c.first.addr.to_string(),
NtpSourceConfig::Sock(_c) => "".to_string(),
#[cfg(feature = "pps")]
NtpSourceConfig::Pps(_c) => "".to_string(),
}
}
#[test]
fn test_deserialize_source() {
let test: TestConfig = toml::from_str(
r#"
[source]
mode = "server"
address = "example.com"
"#,
)
.unwrap();
assert_eq!(source_addr(&test.source), "example.com:123");
assert!(matches!(test.source, NtpSourceConfig::Standard(_)));
let test: TestConfig = toml::from_str(
r#"
[source]
mode = "server"
address = "example.com:5678"
"#,
)
.unwrap();
assert_eq!(source_addr(&test.source), "example.com:5678");
assert!(matches!(test.source, NtpSourceConfig::Standard(_)));
let test: TestConfig = toml::from_str(
r#"
[source]
mode = "server"
address = "example.com"
"#,
)
.unwrap();
assert_eq!(source_addr(&test.source), "example.com:123");
assert!(matches!(test.source, NtpSourceConfig::Standard(_)));
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "pool"
"#,
)
.unwrap();
assert!(matches!(test.source, NtpSourceConfig::Pool(_)));
assert_eq!(source_addr(&test.source), "example.com:123");
if let NtpSourceConfig::Pool(config) = test.source {
assert_eq!(config.first.count, 4);
}
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "pool"
count = 42
"#,
)
.unwrap();
assert!(matches!(test.source, NtpSourceConfig::Pool(_)));
assert_eq!(source_addr(&test.source), "example.com:123");
if let NtpSourceConfig::Pool(config) = test.source {
assert_eq!(config.first.count, 42);
}
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "nts"
"#,
)
.unwrap();
assert!(matches!(test.source, NtpSourceConfig::Nts(_)));
assert_eq!(source_addr(&test.source), "example.com:4460");
{
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "nts-pool"
"#,
)
.unwrap();
assert!(matches!(test.source, NtpSourceConfig::NtsPool(_)));
assert_eq!(source_addr(&test.source), "example.com:4460");
}
}
#[test]
fn test_deserialize_source_ntp_version() {
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "server"
ntp-version = "auto"
"#,
)
.unwrap();
let NtpSourceConfig::Standard(source) = test.source else {
panic!("Invalid source type");
};
assert_eq!(
source.first.ntp_version,
ProtocolVersion::v4_upgrading_to_v5_with_default_tries()
);
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "pool"
ntp-version = 5
"#,
)
.unwrap();
let NtpSourceConfig::Pool(source) = test.source else {
panic!("Invalid source type");
};
assert_eq!(source.first.ntp_version, ProtocolVersion::V5);
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "server"
ntp-version = 4
"#,
)
.unwrap();
let NtpSourceConfig::Standard(source) = test.source else {
panic!("Invalid source type");
};
assert_eq!(source.first.ntp_version, ProtocolVersion::V4);
let test: TestConfig = toml::from_str(
r#"
[source]
address = "example.com"
mode = "nts"
"#,
)
.unwrap();
let NtpSourceConfig::Nts(source) = test.source else {
panic!("Invalid source type");
};
assert_eq!(source.first.ntp_version, ProtocolVersion::V4);
}
#[test]
fn test_deserialize_source_pem_certificate() {
let contents = include_bytes!("../../../testdata/certificates/nos-nl.pem");
let path = std::env::temp_dir().join("nos-nl.pem");
std::fs::write(&path, contents).unwrap();
let test: TestConfig = toml::from_str(&format!(
r#"
[source]
address = "example.com"
certificate-authority = "{}"
mode = "nts"
"#,
path.display()
))
.unwrap();
assert!(matches!(test.source, NtpSourceConfig::Nts(_)));
assert_eq!(source_addr(&test.source), "example.com:4460");
}
#[test]
fn test_source_from_string() {
let source = NtpSourceConfig::try_from("example.com").unwrap();
assert_eq!(source_addr(&source), "example.com:123");
assert!(matches!(source, NtpSourceConfig::Standard(_)));
let source = NtpSourceConfig::try_from("example.com:5678").unwrap();
assert_eq!(source_addr(&source), "example.com:5678");
assert!(matches!(source, NtpSourceConfig::Standard(_)));
}
#[test]
fn test_source_config_parsing() {
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "server"
address = "example.com"
initial-poll-interval = 7
"#,
);
assert!(test.is_ok());
let test2: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "server"
address = "example.com"
does-not-exist = 7
"#,
);
assert!(test2.is_err());
}
#[test]
fn test_sock_config_parsing() {
let TestConfig {
source: NtpSourceConfig::Sock(test),
} = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
measurement_noise_estimate = 0.0625
"#,
)
.unwrap()
else {
panic!("Unexpected source type");
};
assert_eq!(test.precision, 0.25);
let TestConfig {
source: NtpSourceConfig::Sock(test),
} = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
precision = 0.25
"#,
)
.unwrap()
else {
panic!("Unexpected source type");
};
assert_eq!(test.precision, 0.25);
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
precision = 0.25
measurement_noise_estimate = 0.0625
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
precision = 0.25
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
precision = 0.25
unknown_field = 5
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
precision = -0.25
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
measurement_noise_estimate = -0.0625
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
precision = 0.0
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "sock"
path = "/test/path"
measurement_noise_estimate = 0.0
"#,
);
assert!(test.is_err());
}
#[cfg(feature = "pps")]
#[test]
fn test_pps_config_parsing() {
let TestConfig {
source: NtpSourceConfig::Pps(test),
} = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = 0.25
period = 1.5
"#,
)
.unwrap()
else {
panic!("Unexpected source type");
};
assert_eq!(test.precision, 0.25);
assert_eq!(test.period, 1.5);
let TestConfig {
source: NtpSourceConfig::Pps(test),
} = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
measurement_noise_estimate = 0.0625
"#,
)
.unwrap()
else {
panic!("Unexpected source type");
};
assert_eq!(test.precision, 0.25);
assert_eq!(test.period, 1.0);
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = 0.25
measurement_noise_estimate = 0.0625
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = 0.25
unknown_field = 5
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = -0.25
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
measurement_noise_estimate = -0.0625
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = -0.25
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = 0.25
period = -0.5
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
measurement_noise_estimate = 0.0
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = 0.0
"#,
);
assert!(test.is_err());
let test: Result<TestConfig, _> = toml::from_str(
r#"
[source]
mode = "pps"
path = "/test/path"
precision = 0.25
period = 0.0
"#,
);
assert!(test.is_err());
}
#[test]
fn test_normalize_addr() {
let addr = NormalizedAddress::from_string_ntp("[::1]:456".into()).unwrap();
assert_eq!(addr.to_string(), "[::1]:456");
let addr = NormalizedAddress::from_string_ntp("::1".into()).unwrap();
assert_eq!(addr.to_string(), "[::1]:123");
assert!(NormalizedAddress::from_string_ntp(":some:invalid:1".into()).is_err());
let addr = NormalizedAddress::from_string_ntp("127.0.0.1:456".into()).unwrap();
assert_eq!(addr.to_string(), "127.0.0.1:456");
let addr = NormalizedAddress::from_string_ntp("127.0.0.1".into()).unwrap();
assert_eq!(addr.to_string(), "127.0.0.1:123");
let addr = NormalizedAddress::from_string_ntp("1234567890.example.com".into()).unwrap();
assert_eq!(addr.to_string(), "1234567890.example.com:123");
}
}