use std::net::{IpAddr, SocketAddr};
use std::num::NonZeroU32;
use thiserror::Error;
use tokio::sync::mpsc;
#[cfg(all(unix, not(target_os = "macos")))]
use crate::linux::{BrowseGuard, browse_start};
#[cfg(target_os = "macos")]
use crate::macos::{BrowseGuard, browse_start};
#[cfg(target_os = "windows")]
use crate::windows::{BrowseGuard, browse_start};
pub(crate) type BrowseEventSender = mpsc::UnboundedSender<Result<BrowseEvent, ServiceBrowseError>>;
pub(crate) type BrowseEventReceiver =
mpsc::UnboundedReceiver<Result<BrowseEvent, ServiceBrowseError>>;
#[derive(Debug, Clone, Default)]
pub struct ServiceBrowserBuilder {
pub(crate) service_type: Option<String>,
pub(crate) domain: Option<String>,
pub(crate) interface_index: Option<NonZeroU32>,
}
impl ServiceBrowserBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn service_type(&mut self, service_type: impl AsRef<str>) -> &mut Self {
self.service_type = Some(service_type.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 fn interface_index(&mut self, index: NonZeroU32) -> &mut Self {
self.interface_index = Some(index);
self
}
pub async fn browse(&self) -> Result<ServiceBrowser, ServiceBrowseError> {
let (rx, guard) =
browse_start(&self.service_type, &self.domain, self.interface_index).await?;
Ok(ServiceBrowser { rx, _guard: guard })
}
}
pub struct ServiceBrowser {
rx: BrowseEventReceiver,
_guard: BrowseGuard,
}
impl ServiceBrowser {
pub async fn recv(&mut self) -> Option<Result<BrowseEvent, ServiceBrowseError>> {
self.rx.recv().await
}
}
#[derive(Debug, Clone)]
pub enum BrowseEvent {
Found(DiscoveredService),
Removed(RemovedService),
}
#[derive(Debug, Clone)]
pub struct DiscoveredService {
pub name: String,
pub service_type: String,
pub domain: String,
pub host_name: String,
pub port: u16,
pub addresses: Vec<IpAddr>,
pub txt_records: Vec<TxtRecord>,
pub interface_index: Option<NonZeroU32>,
}
impl DiscoveredService {
pub fn socket_addrs(&self) -> impl Iterator<Item = SocketAddr> + '_ {
self.addresses
.iter()
.map(move |&ip| SocketAddr::new(ip, self.port))
}
pub fn txt(&self, key: &str) -> Option<&[u8]> {
self.txt_records
.iter()
.find(|r| r.key == key)
.and_then(|r| r.value.as_deref())
}
}
#[derive(Debug, Clone)]
pub struct RemovedService {
pub name: String,
pub service_type: String,
pub domain: String,
pub interface_index: Option<NonZeroU32>,
}
#[derive(Debug, Clone)]
pub struct TxtRecord {
pub key: String,
pub value: Option<Vec<u8>>,
}
#[derive(Error, Debug)]
pub enum ServiceBrowseError {
#[error("DNS-SD not available on system: {0}")]
DnsSdUnavailable(String),
#[error("parameter {0:?} contains interior nul byte at position {1}")]
ParameterContainsInteriorNulByte(String, usize),
#[error("interface index {0} is invalid")]
InvalidInterfaceIndex(u32),
#[error("browse operation failed: {0}")]
BrowseFailed(String),
#[error("failed to resolve service {0:?}: {1}")]
ResolveFailed(String, String),
}
#[cfg(unix)]
pub(crate) fn parse_txt_entry(entry: &[u8]) -> TxtRecord {
match entry.iter().position(|&b| b == b'=') {
Some(pos) => TxtRecord {
key: String::from_utf8_lossy(&entry[..pos]).into_owned(),
value: Some(entry[pos + 1..].to_vec()),
},
None => TxtRecord {
key: String::from_utf8_lossy(entry).into_owned(),
value: None,
},
}
}
#[cfg(target_os = "macos")]
pub(crate) fn parse_txt_buffer(buf: &[u8]) -> Vec<TxtRecord> {
let mut records = Vec::new();
let mut i = 0;
while i < buf.len() {
let len = buf[i] as usize;
i += 1;
if i + len > buf.len() {
break;
}
if len > 0 {
records.push(parse_txt_entry(&buf[i..i + len]));
}
i += len;
}
records
}