use std::num::NonZeroU32;
use thiserror::Error;
#[cfg(all(unix, not(target_os = "macos")))]
pub use crate::linux::ServiceRegistration;
#[cfg(target_os = "macos")]
pub use crate::macos::ServiceRegistration;
#[cfg(target_os = "windows")]
pub use crate::windows::ServiceRegistration;
#[derive(Debug, Clone)]
pub struct ServiceRegistrationBuilder {
pub(crate) service_type: String,
pub(crate) port: u16,
pub(crate) name: Option<String>,
pub(crate) host: Option<String>,
pub(crate) domain: Option<String>,
pub(crate) interface_index: Option<NonZeroU32>,
pub(crate) txt_record: Vec<(String, TxtRecordValue)>,
}
impl ServiceRegistrationBuilder {
pub fn new(service_type: impl AsRef<str>, port: u16) -> Self {
Self {
service_type: service_type.as_ref().to_string(),
port,
name: None,
host: None,
domain: None,
interface_index: None,
txt_record: Vec::new(),
}
}
pub fn name(&mut self, name: impl AsRef<str>) -> &mut Self {
self.name = Some(name.as_ref().to_string());
self
}
fn add_txt_record_key_value(&mut self, key: String, value: TxtRecordValue) {
self.txt_record.retain_mut(|(k, _v)| *k != key);
self.txt_record.push((key, value));
}
pub fn add_txt_record_key_empty(&mut self, key: impl AsRef<str>) -> &mut Self {
let key = key.as_ref().to_string();
self.add_txt_record_key_value(key, TxtRecordValue::KeyOnly);
self
}
pub fn add_txt_record_key_string(
&mut self,
key: impl AsRef<str>,
value: impl AsRef<str>,
) -> &mut Self {
let key = key.as_ref().to_string();
let value = value.as_ref().to_string();
self.add_txt_record_key_value(key, TxtRecordValue::String(value));
self
}
#[cfg(not(target_os = "windows"))] pub fn add_txt_record_key_binary(
&mut self,
key: impl AsRef<str>,
value: impl AsRef<[u8]>,
) -> &mut Self {
let key = key.as_ref().to_string();
let value = value.as_ref().to_vec();
self.add_txt_record_key_value(key, TxtRecordValue::Binary(value));
self
}
pub fn interface_index(&mut self, index: NonZeroU32) -> &mut Self {
self.interface_index = Some(index);
self
}
pub fn host(&mut self, host: impl AsRef<str>) -> &mut Self {
self.host = Some(host.as_ref().to_string());
self
}
pub fn domain(&mut self, domain: impl AsRef<str>) -> &mut Self {
self.domain = Some(domain.as_ref().to_string());
self
}
pub async fn register(&self) -> Result<ServiceRegistration, ServiceRegistrationError> {
validate_txt_records(&self.txt_record)?;
ServiceRegistration::new(
&self.service_type,
self.port,
&self.name,
&self.host,
&self.domain,
self.interface_index,
&self.txt_record,
)
.await
}
}
#[derive(Error, Debug)]
pub enum ServiceRegistrationError {
#[error("invalid TXT record key {0:?}: {1}")]
InvalidTxtRecordKey(String, String),
#[error("invalid TXT record value for key {0:?}: {1}")]
InvalidTxtRecordValue(String, String),
#[error("parameter {0:?} contains interior nul byte at position {1}")]
ParameterContainsInteriorNulByte(String, usize),
#[error("interface index {0} is invalid")]
InvalidInterfaceIndex(u32),
#[error("hostname not set and could not be determined automatically: {0}")]
HostnameUnavailable(String),
#[error("DNS-SD not available on system: {0}")]
DnsSdUnavailable(String),
#[error("DNS-SD registration returned an error: {0}")]
RegistrationError(String),
#[error("registration failed: {0}")]
RegistrationFailed(String),
#[error("service name conflict")]
NameConflict,
}
#[derive(Debug, Clone)]
pub(crate) enum TxtRecordValue {
KeyOnly,
String(String),
#[cfg(not(target_os = "windows"))] Binary(Vec<u8>),
}
pub(crate) fn validate_txt_records(
records: &[(String, TxtRecordValue)],
) -> Result<(), ServiceRegistrationError> {
for (key, value) in records {
if key.is_empty() {
return Err(ServiceRegistrationError::InvalidTxtRecordKey(
key.clone(),
"key must not be empty".into(),
));
}
if key.contains('=') {
return Err(ServiceRegistrationError::InvalidTxtRecordKey(
key.clone(),
"key must not contain '='".into(),
));
}
if key.chars().any(|c| !(' '..='~').contains(&c)) {
return Err(ServiceRegistrationError::InvalidTxtRecordKey(
key.clone(),
"key must contain only printable US-ASCII characters (0x20-0x7E)".into(),
));
}
let value_len = match value {
TxtRecordValue::KeyOnly => 0,
TxtRecordValue::String(s) => 1 + s.len(),
#[cfg(not(target_os = "windows"))]
TxtRecordValue::Binary(b) => 1 + b.len(),
};
if key.len() + value_len > 255 {
return Err(ServiceRegistrationError::InvalidTxtRecordValue(
key.clone(),
"key + value must not exceed 255 bytes".into(),
));
}
}
Ok(())
}