#[cfg(feature = "logging")]
use crate::log::debug;
use crate::{
dns_parser::{DnsRecordBox, DnsRecordExt, DnsSrv, RRType, ScopedIp},
Error, InterfaceId, Result,
};
use if_addrs::IfAddr;
use std::{
cmp,
collections::{HashMap, HashSet},
convert::TryInto,
fmt,
net::{IpAddr, Ipv4Addr},
str::FromStr,
};
const DNS_HOST_TTL: u32 = 120; const DNS_OTHER_TTL: u32 = 4500;
#[derive(Debug)]
pub(crate) struct MyIntf {
pub(crate) name: String,
pub(crate) index: u32,
pub(crate) addrs: HashSet<IfAddr>,
}
impl MyIntf {
pub(crate) fn next_ifaddr_v4(&self) -> Option<&IfAddr> {
self.addrs.iter().find(|a| a.ip().is_ipv4())
}
pub(crate) fn next_ifaddr_v6(&self) -> Option<&IfAddr> {
self.addrs.iter().find(|a| a.ip().is_ipv6())
}
}
impl From<&MyIntf> for InterfaceId {
fn from(my_intf: &MyIntf) -> Self {
InterfaceId {
name: my_intf.name.clone(),
index: my_intf.index,
}
}
}
#[derive(Debug, Clone)]
pub struct ServiceInfo {
ty_domain: String,
sub_domain: Option<String>,
fullname: String, server: String, addresses: HashSet<IpAddr>,
port: u16,
host_ttl: u32, other_ttl: u32, priority: u16,
weight: u16,
txt_properties: TxtProperties,
addr_auto: bool,
status: HashMap<u32, ServiceStatus>,
requires_probe: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ServiceStatus {
Probing,
Announced,
Unknown,
}
impl ServiceInfo {
pub fn new<Ip: AsIpAddrs, P: IntoTxtProperties>(
ty_domain: &str,
my_name: &str,
host_name: &str,
ip: Ip,
port: u16,
properties: P,
) -> Result<Self> {
let (ty_domain, sub_domain) = split_sub_domain(ty_domain);
let fullname = format!("{my_name}.{ty_domain}");
let ty_domain = ty_domain.to_string();
let sub_domain = sub_domain.map(str::to_string);
let server = normalize_hostname(host_name.to_string());
let addresses = ip.as_ip_addrs()?;
let txt_properties = properties.into_txt_properties();
for prop in txt_properties.iter() {
let key = prop.key();
if !key.is_ascii() {
return Err(Error::Msg(format!(
"TXT property key {} is not ASCII",
prop.key()
)));
}
if key.contains('=') {
return Err(Error::Msg(format!(
"TXT property key {} contains '='",
prop.key()
)));
}
}
let this = Self {
ty_domain,
sub_domain,
fullname,
server,
addresses,
port,
host_ttl: DNS_HOST_TTL,
other_ttl: DNS_OTHER_TTL,
priority: 0,
weight: 0,
txt_properties,
addr_auto: false,
status: HashMap::new(),
requires_probe: true,
};
Ok(this)
}
pub const fn enable_addr_auto(mut self) -> Self {
self.addr_auto = true;
self
}
pub const fn is_addr_auto(&self) -> bool {
self.addr_auto
}
pub fn set_requires_probe(&mut self, enable: bool) {
self.requires_probe = enable;
}
pub const fn requires_probe(&self) -> bool {
self.requires_probe
}
#[inline]
pub fn get_type(&self) -> &str {
&self.ty_domain
}
#[inline]
pub const fn get_subtype(&self) -> &Option<String> {
&self.sub_domain
}
#[inline]
pub fn get_fullname(&self) -> &str {
&self.fullname
}
#[inline]
pub const fn get_properties(&self) -> &TxtProperties {
&self.txt_properties
}
pub fn get_property(&self, key: &str) -> Option<&TxtProperty> {
self.txt_properties.get(key)
}
pub fn get_property_val(&self, key: &str) -> Option<Option<&[u8]>> {
self.txt_properties.get_property_val(key)
}
pub fn get_property_val_str(&self, key: &str) -> Option<&str> {
self.txt_properties.get_property_val_str(key)
}
#[inline]
pub fn get_hostname(&self) -> &str {
&self.server
}
#[inline]
pub const fn get_port(&self) -> u16 {
self.port
}
#[inline]
pub const fn get_addresses(&self) -> &HashSet<IpAddr> {
&self.addresses
}
pub fn get_addresses_v4(&self) -> HashSet<&Ipv4Addr> {
let mut ipv4_addresses = HashSet::new();
for ip in &self.addresses {
if let IpAddr::V4(ipv4) = ip {
ipv4_addresses.insert(ipv4);
}
}
ipv4_addresses
}
#[inline]
pub const fn get_host_ttl(&self) -> u32 {
self.host_ttl
}
#[inline]
pub const fn get_other_ttl(&self) -> u32 {
self.other_ttl
}
#[inline]
pub const fn get_priority(&self) -> u16 {
self.priority
}
#[inline]
pub const fn get_weight(&self) -> u16 {
self.weight
}
pub(crate) fn get_addrs_on_my_intf_v4(&self, my_intf: &MyIntf) -> Vec<IpAddr> {
self.addresses
.iter()
.filter(|a| a.is_ipv4() && my_intf.addrs.iter().any(|x| valid_ip_on_intf(a, x)))
.copied()
.collect()
}
pub(crate) fn get_addrs_on_my_intf_v6(&self, my_intf: &MyIntf) -> Vec<IpAddr> {
self.addresses
.iter()
.filter(|a| a.is_ipv6() && my_intf.addrs.iter().any(|x| valid_ip_on_intf(a, x)))
.copied()
.collect()
}
pub(crate) fn _is_ready(&self) -> bool {
let some_missing = self.ty_domain.is_empty()
|| self.fullname.is_empty()
|| self.server.is_empty()
|| self.addresses.is_empty();
!some_missing
}
pub(crate) fn insert_ipaddr(&mut self, addr: IpAddr) {
self.addresses.insert(addr);
}
pub(crate) fn remove_ipaddr(&mut self, addr: &IpAddr) {
self.addresses.remove(addr);
}
pub(crate) fn generate_txt(&self) -> Vec<u8> {
encode_txt(self.get_properties().iter())
}
pub(crate) fn _set_port(&mut self, port: u16) {
self.port = port;
}
pub(crate) fn _set_hostname(&mut self, hostname: String) {
self.server = normalize_hostname(hostname);
}
pub(crate) fn _set_properties_from_txt(&mut self, txt: &[u8]) -> bool {
let properties = decode_txt_unique(txt);
if self.txt_properties.properties != properties {
self.txt_properties = TxtProperties { properties };
true
} else {
false
}
}
pub(crate) fn _set_subtype(&mut self, subtype: String) {
self.sub_domain = Some(subtype);
}
pub(crate) fn _set_host_ttl(&mut self, ttl: u32) {
self.host_ttl = ttl;
}
pub(crate) fn _set_other_ttl(&mut self, ttl: u32) {
self.other_ttl = ttl;
}
pub(crate) fn set_status(&mut self, if_index: u32, status: ServiceStatus) {
match self.status.get_mut(&if_index) {
Some(service_status) => {
*service_status = status;
}
None => {
self.status.entry(if_index).or_insert(status);
}
}
}
pub(crate) fn get_status(&self, intf: u32) -> ServiceStatus {
self.status
.get(&intf)
.cloned()
.unwrap_or(ServiceStatus::Unknown)
}
pub fn as_resolved_service(self) -> ResolvedService {
let addresses: HashSet<ScopedIp> = self.addresses.into_iter().map(|a| a.into()).collect();
ResolvedService {
ty_domain: self.ty_domain,
sub_ty_domain: self.sub_domain,
fullname: self.fullname,
host: self.server,
port: self.port,
addresses,
txt_properties: self.txt_properties,
}
}
}
fn normalize_hostname(mut hostname: String) -> String {
if hostname.ends_with(".local.local.") {
let new_len = hostname.len() - "local.".len();
hostname.truncate(new_len);
}
hostname
}
pub trait AsIpAddrs {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>>;
}
impl<T: AsIpAddrs> AsIpAddrs for &T {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
(*self).as_ip_addrs()
}
}
impl AsIpAddrs for &str {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
let mut addrs = HashSet::new();
if !self.is_empty() {
let iter = self.split(',').map(str::trim).map(IpAddr::from_str);
for addr in iter {
let addr = addr.map_err(|err| Error::ParseIpAddr(err.to_string()))?;
addrs.insert(addr);
}
}
Ok(addrs)
}
}
impl AsIpAddrs for String {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
self.as_str().as_ip_addrs()
}
}
impl<I: AsIpAddrs> AsIpAddrs for &[I] {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
let mut addrs = HashSet::new();
for result in self.iter().map(I::as_ip_addrs) {
addrs.extend(result?);
}
Ok(addrs)
}
}
impl AsIpAddrs for () {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
Ok(HashSet::new())
}
}
impl AsIpAddrs for std::net::IpAddr {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
let mut ips = HashSet::new();
ips.insert(*self);
Ok(ips)
}
}
impl AsIpAddrs for Box<dyn AsIpAddrs> {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
self.as_ref().as_ip_addrs()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxtProperties {
properties: Vec<TxtProperty>,
}
impl Default for TxtProperties {
fn default() -> Self {
TxtProperties::new()
}
}
impl TxtProperties {
pub fn new() -> Self {
TxtProperties {
properties: Vec::new(),
}
}
pub fn iter(&self) -> impl Iterator<Item = &TxtProperty> {
self.properties.iter()
}
pub fn len(&self) -> usize {
self.properties.len()
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
pub fn get(&self, key: &str) -> Option<&TxtProperty> {
let key = key.to_lowercase();
self.properties
.iter()
.find(|&prop| prop.key.to_lowercase() == key)
}
pub fn get_property_val(&self, key: &str) -> Option<Option<&[u8]>> {
self.get(key).map(|x| x.val())
}
pub fn get_property_val_str(&self, key: &str) -> Option<&str> {
self.get(key).map(|x| x.val_str())
}
pub fn into_property_map_str(self) -> HashMap<String, String> {
self.properties
.into_iter()
.filter_map(|property| {
let val_string = property.val.map_or(Some(String::new()), |val| {
String::from_utf8(val)
.map_err(|e| {
debug!("Property value contains invalid UTF-8: {e}");
})
.ok()
})?;
Some((property.key, val_string))
})
.collect()
}
}
impl fmt::Display for TxtProperties {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let delimiter = ", ";
let props: Vec<String> = self.properties.iter().map(|p| p.to_string()).collect();
write!(f, "({})", props.join(delimiter))
}
}
impl From<&[u8]> for TxtProperties {
fn from(txt: &[u8]) -> Self {
let properties = decode_txt_unique(txt);
TxtProperties { properties }
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct TxtProperty {
key: String,
val: Option<Vec<u8>>,
}
impl TxtProperty {
pub fn key(&self) -> &str {
&self.key
}
pub fn val(&self) -> Option<&[u8]> {
self.val.as_deref()
}
pub fn val_str(&self) -> &str {
self.val
.as_ref()
.map_or("", |v| std::str::from_utf8(&v[..]).unwrap_or_default())
}
}
impl<K, V> From<&(K, V)> for TxtProperty
where
K: ToString,
V: ToString,
{
fn from(prop: &(K, V)) -> Self {
Self {
key: prop.0.to_string(),
val: Some(prop.1.to_string().into_bytes()),
}
}
}
impl<K, V> From<(K, V)> for TxtProperty
where
K: ToString,
V: AsRef<[u8]>,
{
fn from(prop: (K, V)) -> Self {
Self {
key: prop.0.to_string(),
val: Some(prop.1.as_ref().into()),
}
}
}
impl From<&str> for TxtProperty {
fn from(key: &str) -> Self {
Self {
key: key.to_string(),
val: None,
}
}
}
impl fmt::Display for TxtProperty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}={}", self.key, self.val_str())
}
}
impl fmt::Debug for TxtProperty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let val_string = self.val.as_ref().map_or_else(
|| "None".to_string(),
|v| {
std::str::from_utf8(&v[..]).map_or_else(
|_| format!("Some({})", u8_slice_to_hex(&v[..])),
|s| format!("Some(\"{s}\")"),
)
},
);
write!(
f,
"TxtProperty {{key: \"{}\", val: {}}}",
&self.key, &val_string,
)
}
}
const HEX_TABLE: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
fn u8_slice_to_hex(slice: &[u8]) -> String {
let mut hex = String::with_capacity(slice.len() * 2 + 2);
hex.push_str("0x");
for b in slice {
hex.push(HEX_TABLE[(b >> 4) as usize]);
hex.push(HEX_TABLE[(b & 0x0F) as usize]);
}
hex
}
pub trait IntoTxtProperties {
fn into_txt_properties(self) -> TxtProperties;
}
impl IntoTxtProperties for HashMap<String, String> {
fn into_txt_properties(mut self) -> TxtProperties {
let properties = self
.drain()
.map(|(key, val)| TxtProperty {
key,
val: Some(val.into_bytes()),
})
.collect();
TxtProperties { properties }
}
}
impl IntoTxtProperties for Option<HashMap<String, String>> {
fn into_txt_properties(self) -> TxtProperties {
self.map_or_else(
|| TxtProperties {
properties: Vec::new(),
},
|h| h.into_txt_properties(),
)
}
}
impl<'a, T: 'a> IntoTxtProperties for &'a [T]
where
TxtProperty: From<&'a T>,
{
fn into_txt_properties(self) -> TxtProperties {
let mut properties = Vec::new();
let mut keys = HashSet::new();
for t in self.iter() {
let prop = TxtProperty::from(t);
let key = prop.key.to_lowercase();
if keys.insert(key) {
properties.push(prop);
}
}
TxtProperties { properties }
}
}
impl IntoTxtProperties for Vec<TxtProperty> {
fn into_txt_properties(self) -> TxtProperties {
TxtProperties { properties: self }
}
}
fn encode_txt<'a>(properties: impl Iterator<Item = &'a TxtProperty>) -> Vec<u8> {
let mut bytes = Vec::new();
for prop in properties {
let mut s = prop.key.clone().into_bytes();
if let Some(v) = &prop.val {
s.extend(b"=");
s.extend(v);
}
let sz: u8 = s.len().try_into().unwrap_or_else(|_| {
debug!("Property {} is too long, truncating to 255 bytes", prop.key);
s.resize(u8::MAX as usize, 0);
u8::MAX
});
bytes.push(sz);
bytes.extend(s);
}
if bytes.is_empty() {
bytes.push(0);
}
bytes
}
pub(crate) fn decode_txt(txt: &[u8]) -> Vec<TxtProperty> {
let mut properties = Vec::new();
let mut offset = 0;
while offset < txt.len() {
let length = txt[offset] as usize;
if length == 0 {
break; }
offset += 1;
let offset_end = offset + length;
if offset_end > txt.len() {
debug!("DNS TXT record contains invalid data: Size given for property would be out of range. (offset={}, length={}, offset_end={}, record length={})", offset, length, offset_end, txt.len());
break; }
let kv_bytes = &txt[offset..offset_end];
let (k, v) = kv_bytes.iter().position(|&x| x == b'=').map_or_else(
|| (kv_bytes.to_vec(), None),
|idx| (kv_bytes[..idx].to_vec(), Some(kv_bytes[idx + 1..].to_vec())),
);
match String::from_utf8(k) {
Ok(k_string) => {
properties.push(TxtProperty {
key: k_string,
val: v,
});
}
Err(e) => debug!("failed to convert to String from key: {}", e),
}
offset += length;
}
properties
}
fn decode_txt_unique(txt: &[u8]) -> Vec<TxtProperty> {
let mut properties = decode_txt(txt);
let mut keys = HashSet::new();
properties.retain(|p| {
let key = p.key().to_lowercase();
keys.insert(key) });
properties
}
pub(crate) fn valid_ip_on_intf(addr: &IpAddr, if_addr: &IfAddr) -> bool {
match (addr, if_addr) {
(IpAddr::V4(addr), IfAddr::V4(if_v4)) => {
let netmask = u32::from(if_v4.netmask);
let intf_net = u32::from(if_v4.ip) & netmask;
let addr_net = u32::from(*addr) & netmask;
addr_net == intf_net
}
(IpAddr::V6(addr), IfAddr::V6(if_v6)) => {
let netmask = u128::from(if_v6.netmask);
let intf_net = u128::from(if_v6.ip) & netmask;
let addr_net = u128::from(*addr) & netmask;
addr_net == intf_net
}
_ => false,
}
}
#[derive(Debug)]
pub(crate) struct Probe {
pub(crate) records: Vec<DnsRecordBox>,
pub(crate) waiting_services: HashSet<String>,
pub(crate) start_time: u64,
pub(crate) next_send: u64,
}
impl Probe {
pub(crate) fn new(start_time: u64) -> Self {
let next_send = start_time;
Self {
records: Vec::new(),
waiting_services: HashSet::new(),
start_time,
next_send,
}
}
pub(crate) fn insert_record(&mut self, record: DnsRecordBox) {
let insert_position = self
.records
.binary_search_by(
|existing| match existing.get_class().cmp(&record.get_class()) {
std::cmp::Ordering::Equal => existing.get_type().cmp(&record.get_type()),
other => other,
},
)
.unwrap_or_else(|pos| pos);
self.records.insert(insert_position, record);
}
pub(crate) fn tiebreaking(&mut self, incoming: &[&DnsRecordBox], now: u64, probe_name: &str) {
let min_len = self.records.len().min(incoming.len());
let mut cmp_result = cmp::Ordering::Equal;
for (i, incoming_record) in incoming.iter().enumerate().take(min_len) {
match self.records[i].compare(incoming_record.as_ref()) {
cmp::Ordering::Equal => continue,
other => {
cmp_result = other;
break; }
}
}
if cmp_result == cmp::Ordering::Equal {
cmp_result = self.records.len().cmp(&incoming.len());
}
match cmp_result {
cmp::Ordering::Less => {
debug!("tiebreaking '{probe_name}': LOST, will wait for one second",);
self.start_time = now + 1000; self.next_send = now + 1000;
}
ordering => {
debug!("tiebreaking '{probe_name}': {:?}", ordering);
}
}
}
pub(crate) fn update_next_send(&mut self, now: u64) {
self.next_send = now + 250;
}
pub(crate) fn expired(&self, now: u64) -> bool {
now >= self.start_time + 750
}
}
pub(crate) struct DnsRegistry {
pub(crate) probing: HashMap<String, Probe>,
pub(crate) active: HashMap<String, Vec<DnsRecordBox>>,
pub(crate) new_timers: Vec<u64>,
pub(crate) name_changes: HashMap<String, String>,
}
impl DnsRegistry {
pub(crate) fn new() -> Self {
Self {
probing: HashMap::new(),
active: HashMap::new(),
new_timers: Vec::new(),
name_changes: HashMap::new(),
}
}
pub(crate) fn is_probing_done<T>(
&mut self,
answer: &T,
service_name: &str,
start_time: u64,
) -> bool
where
T: DnsRecordExt + Send + 'static,
{
if let Some(active_records) = self.active.get(answer.get_name()) {
for record in active_records.iter() {
if answer.matches(record.as_ref()) {
debug!(
"found active record {} {}",
answer.get_type(),
answer.get_name(),
);
return true;
}
}
}
let probe = self
.probing
.entry(answer.get_name().to_string())
.or_insert_with(|| {
debug!("new probe of {}", answer.get_name());
Probe::new(start_time)
});
self.new_timers.push(probe.next_send);
for record in probe.records.iter() {
if answer.matches(record.as_ref()) {
debug!(
"found existing record {} in probe of '{}'",
answer.get_type(),
answer.get_name(),
);
probe.waiting_services.insert(service_name.to_string());
return false; }
}
debug!(
"insert record {} into probe of {}",
answer.get_type(),
answer.get_name(),
);
probe.insert_record(answer.clone_box());
probe.waiting_services.insert(service_name.to_string());
false
}
pub(crate) fn update_hostname(
&mut self,
original: &str,
new_name: &str,
probe_time: u64,
) -> bool {
let mut found_records = Vec::new();
let mut new_timer_added = false;
for (_name, probe) in self.probing.iter_mut() {
probe.records.retain(|record| {
if record.get_type() == RRType::SRV {
if let Some(srv) = record.any().downcast_ref::<DnsSrv>() {
if srv.host() == original {
let mut new_record = srv.clone();
new_record.set_host(new_name.to_string());
found_records.push(new_record);
return false;
}
}
}
true
});
}
for (_name, records) in self.active.iter_mut() {
records.retain(|record| {
if record.get_type() == RRType::SRV {
if let Some(srv) = record.any().downcast_ref::<DnsSrv>() {
if srv.host() == original {
let mut new_record = srv.clone();
new_record.set_host(new_name.to_string());
found_records.push(new_record);
return false;
}
}
}
true
});
}
for record in found_records {
let probe = match self.probing.get_mut(record.get_name()) {
Some(p) => {
p.start_time = probe_time; p
}
None => {
let new_probe = self
.probing
.entry(record.get_name().to_string())
.or_insert_with(|| Probe::new(probe_time));
new_timer_added = true;
new_probe
}
};
debug!(
"insert record {} with new hostname {new_name} into probe for: {}",
record.get_type(),
record.get_name()
);
probe.insert_record(record.boxed());
}
new_timer_added
}
}
pub(crate) fn split_sub_domain(domain: &str) -> (&str, Option<&str>) {
if let Some((_, ty_domain)) = domain.rsplit_once("._sub.") {
(ty_domain, Some(domain))
} else {
(domain, None)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ResolvedService {
pub ty_domain: String,
pub sub_ty_domain: Option<String>,
pub fullname: String,
pub host: String,
pub port: u16,
pub addresses: HashSet<ScopedIp>,
pub txt_properties: TxtProperties,
}
impl ResolvedService {
pub fn is_valid(&self) -> bool {
let some_missing = self.ty_domain.is_empty()
|| self.fullname.is_empty()
|| self.host.is_empty()
|| self.addresses.is_empty();
!some_missing
}
#[inline]
pub const fn get_subtype(&self) -> &Option<String> {
&self.sub_ty_domain
}
#[inline]
pub fn get_fullname(&self) -> &str {
&self.fullname
}
#[inline]
pub fn get_hostname(&self) -> &str {
&self.host
}
#[inline]
pub fn get_port(&self) -> u16 {
self.port
}
#[inline]
pub fn get_addresses(&self) -> &HashSet<ScopedIp> {
&self.addresses
}
pub fn get_addresses_v4(&self) -> HashSet<Ipv4Addr> {
self.addresses
.iter()
.filter_map(|ip| match ip {
ScopedIp::V4(ipv4) => Some(*ipv4.addr()),
_ => None,
})
.collect()
}
#[inline]
pub fn get_properties(&self) -> &TxtProperties {
&self.txt_properties
}
#[inline]
pub fn get_property(&self, key: &str) -> Option<&TxtProperty> {
self.txt_properties.get(key)
}
pub fn get_property_val(&self, key: &str) -> Option<Option<&[u8]>> {
self.txt_properties.get_property_val(key)
}
pub fn get_property_val_str(&self, key: &str) -> Option<&str> {
self.txt_properties.get_property_val_str(key)
}
}
#[cfg(test)]
mod tests {
use super::{decode_txt, encode_txt, u8_slice_to_hex, ServiceInfo, TxtProperty};
#[test]
fn test_txt_encode_decode() {
let properties = vec![
TxtProperty::from(&("key1", "value1")),
TxtProperty::from(&("key2", "value2")),
];
let property_count = properties.len();
let encoded = encode_txt(properties.iter());
assert_eq!(
encoded.len(),
"key1=value1".len() + "key2=value2".len() + property_count
);
assert_eq!(encoded[0] as usize, "key1=value1".len());
let decoded = decode_txt(&encoded);
assert!(properties[..] == decoded[..]);
let properties = vec![TxtProperty::from(&("key3", ""))];
let property_count = properties.len();
let encoded = encode_txt(properties.iter());
assert_eq!(encoded.len(), "key3=".len() + property_count);
let decoded = decode_txt(&encoded);
assert_eq!(properties, decoded);
let binary_val: Vec<u8> = vec![123, 234, 0];
let binary_len = binary_val.len();
let properties = vec![TxtProperty::from(("key4", binary_val))];
let property_count = properties.len();
let encoded = encode_txt(properties.iter());
assert_eq!(encoded.len(), "key4=".len() + binary_len + property_count);
let decoded = decode_txt(&encoded);
assert_eq!(properties, decoded);
let properties = vec![TxtProperty::from(("key5", "val=5"))];
let property_count = properties.len();
let encoded = encode_txt(properties.iter());
assert_eq!(
encoded.len(),
"key5=".len() + "val=5".len() + property_count
);
let decoded = decode_txt(&encoded);
assert_eq!(properties, decoded);
let properties = vec![TxtProperty::from("key6")];
let property_count = properties.len();
let encoded = encode_txt(properties.iter());
assert_eq!(encoded.len(), "key6".len() + property_count);
let decoded = decode_txt(&encoded);
assert_eq!(properties, decoded);
let properties = vec![TxtProperty::from(
String::from_utf8(vec![0x30; 1024]).unwrap().as_str(), )];
let property_count = properties.len();
let encoded = encode_txt(properties.iter());
assert_eq!(encoded.len(), 255 + property_count);
let decoded = decode_txt(&encoded);
assert_eq!(
vec![TxtProperty::from(
String::from_utf8(vec![0x30; 255]).unwrap().as_str()
)],
decoded
);
}
#[test]
fn test_set_properties_from_txt() {
let properties = vec![
TxtProperty::from(&("one", "1")),
TxtProperty::from(&("ONE", "2")),
TxtProperty::from(&("One", "3")),
];
let encoded = encode_txt(properties.iter());
let decoded = decode_txt(&encoded);
assert_eq!(decoded.len(), 3);
let mut service_info =
ServiceInfo::new("_test._tcp", "prop_test", "localhost", "", 1234, None).unwrap();
service_info._set_properties_from_txt(&encoded);
assert_eq!(service_info.get_properties().len(), 1);
let prop = service_info.get_properties().iter().next().unwrap();
assert_eq!(prop.key, "one");
assert_eq!(prop.val_str(), "1");
}
#[test]
fn test_u8_slice_to_hex() {
let bytes = [0x01u8, 0x02u8, 0x03u8];
let hex = u8_slice_to_hex(&bytes);
assert_eq!(hex.as_str(), "0x010203");
let slice = "abcdefghijklmnopqrstuvwxyz";
let hex = u8_slice_to_hex(slice.as_bytes());
assert_eq!(hex.len(), slice.len() * 2 + 2);
assert_eq!(
hex.as_str(),
"0x6162636465666768696a6b6c6d6e6f707172737475767778797a"
);
}
#[test]
fn test_txt_property_debug() {
let prop_1 = TxtProperty {
key: "key1".to_string(),
val: Some("val1".to_string().into()),
};
let prop_1_debug = format!("{:?}", &prop_1);
assert_eq!(
prop_1_debug,
"TxtProperty {key: \"key1\", val: Some(\"val1\")}"
);
let prop_2 = TxtProperty {
key: "key2".to_string(),
val: Some(vec![150u8, 151u8, 152u8]),
};
let prop_2_debug = format!("{:?}", &prop_2);
assert_eq!(
prop_2_debug,
"TxtProperty {key: \"key2\", val: Some(0x969798)}"
);
}
#[test]
fn test_txt_decode_property_size_out_of_bounds() {
let encoded: Vec<u8> = vec![
0x0b, b'k', b'e', b'y', b'1', b'=', b'v', b'a', b'l', b'u', b'e',
b'1', 0x10, b'k', b'e', b'y', b'2', b'=', b'v', b'a', b'l', b'u', b'e',
b'2', ];
let decoded = decode_txt(&encoded);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].key, "key1");
}
}