use super::*;
fn wire_name(labels: &[&[u8]]) -> Vec<u8> {
let mut out = Vec::new();
for l in labels {
out.push(u8::try_from(l.len()).unwrap());
out.extend_from_slice(l);
}
out.push(0);
out
}
fn ptr_rdata(labels: &[&[u8]]) -> Vec<u8> {
wire_name(labels)
}
fn srv_rdata(port: u16, host: &[&[u8]]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&port.to_be_bytes());
out.extend_from_slice(&wire_name(host));
out
}
#[test]
fn name_reconstruction_rejects_unrepresentable_labels() {
assert_eq!(
parse_name(&ptr_rdata(&[b"MyPrinter", b"_ipp", b"_tcp", b"local"]))
.unwrap()
.as_str(),
"myprinter._ipp._tcp.local."
);
assert!(parse_name(&ptr_rdata(&[b"weird.name", b"_tcp", b"local"])).is_none());
assert!(parse_name(&ptr_rdata(&[b"caf\xc3\xa9", b"local"])).is_none());
}
#[test]
fn flood_is_capped_and_observable() {
let mut r = Resolver::new(2);
let mk = |n: &str| Name::try_from_str(n).unwrap();
assert_eq!(r.on_ptr(mk("a._x._tcp.local.")).len(), 2); assert_eq!(r.on_ptr(mk("b._x._tcp.local.")).len(), 2);
assert!(r.on_ptr(mk("c._x._tcp.local.")).is_empty());
assert_eq!(r.builders.len(), 2);
assert_eq!(r.dropped, 1);
assert!(r.on_ptr(mk("a._x._tcp.local.")).is_empty());
assert_eq!(r.dropped, 1);
}
#[test]
fn shared_host_srv_after_a_still_resolves() {
let mut r = Resolver::new(16);
let i1 = Name::try_from_str("i1._x._tcp.local.").unwrap();
let i2 = Name::try_from_str("i2._x._tcp.local.").unwrap();
let host = Name::try_from_str("h.local.").unwrap();
let k1 = fold(&i1);
let k2 = fold(&i2);
let hk = fold(&host);
r.on_ptr(i1);
r.on_ptr(i2);
r.on_srv(&k1, host.clone(), 8080);
r.on_txt(&k1, vec![b"a=1".to_vec().into()]);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
let first = r.take_ready().expect("i1 should complete");
assert_eq!(first.instance_name().as_str(), "i1._x._tcp.local.");
assert_eq!(first.ipv4_addresses(), [Ipv4Addr::new(10, 0, 0, 1)]);
r.on_srv(&k2, host, 8081);
r.on_txt(&k2, vec![b"b=2".to_vec().into()]);
let second = r
.take_ready()
.expect("i2 should complete from the host cache");
assert_eq!(second.instance_name().as_str(), "i2._x._tcp.local.");
assert_eq!(second.port(), 8081);
assert_eq!(second.ipv4_addresses(), [Ipv4Addr::new(10, 0, 0, 1)]);
}
#[test]
fn srv_parse_extracts_host_and_port() {
let (host, port) = parse_srv(&srv_rdata(631, &[b"printer", b"local"])).unwrap();
assert_eq!(host.as_str(), "printer.local.");
assert_eq!(port, 631);
}
#[test]
fn entry_incomplete_without_address_or_port_or_txt() {
let mut r = Resolver::new(16);
let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let host = Name::try_from_str("h.local.").unwrap();
let k = fold(&inst);
let hk = fold(&host);
r.on_ptr(inst);
r.on_srv(&k, host, 9000);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));
assert!(r.take_ready().is_none(), "must not emit without TXT");
r.on_txt(&k, vec![Bytes::new()]); assert!(r.take_ready().is_some(), "complete once TXT present");
}
#[test]
fn address_fans_out_to_all_instances_on_shared_host() {
let mut r = Resolver::new(16);
let host = Name::try_from_str("h.local.").unwrap();
let hk = fold(&host);
for label in ["i1", "i2", "i3"] {
let inst = Name::try_from_str(&format!("{label}._x._tcp.local.")).unwrap();
let k = fold(&inst);
r.on_ptr(inst);
r.on_srv(&k, host.clone(), 7000);
r.on_txt(&k, vec![b"k=v".to_vec().into()]);
}
assert!(
r.take_ready().is_none(),
"incomplete until an address arrives"
);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)));
let mut emitted = HashSet::new();
while let Some(e) = r.take_ready() {
assert_eq!(e.ipv4_addresses(), [Ipv4Addr::new(192, 0, 2, 1)]);
emitted.insert(e.instance_name().as_str().to_owned());
}
assert_eq!(emitted.len(), 3, "all three shared-host instances resolve");
}
#[test]
fn late_address_reemits_updated_entry() {
let mut r = Resolver::new(16);
let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let host = Name::try_from_str("h.local.").unwrap();
let k = fold(&inst);
let hk = fold(&host);
r.on_ptr(inst);
r.on_srv(&k, host, 7000);
r.on_txt(&k, vec![b"k=v".to_vec().into()]);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)));
let first = r.take_ready().expect("first emit on A");
assert_eq!(first.ipv4_addresses().len(), 1);
assert!(first.ipv6_addresses().is_empty());
r.on_addr(&hk, IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)));
let second = r.take_ready().expect("re-emit on late AAAA");
assert_eq!(second.ipv4_addresses().len(), 1);
assert_eq!(second.ipv6_addresses().len(), 1);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)));
assert!(
r.take_ready().is_none(),
"duplicate address must not re-emit"
);
}
#[test]
fn addresses_capped_per_host() {
let mut r = Resolver::new(16);
let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let host = Name::try_from_str("h.local.").unwrap();
let k = fold(&inst);
let hk = fold(&host);
r.on_ptr(inst);
r.on_srv(&k, host, 7000);
r.on_txt(&k, vec![b"k=v".to_vec().into()]);
for i in 0..(MAX_ADDRS_PER_HOST as u32 + 8) {
let o = i.to_be_bytes();
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(10, o[1], o[2], o[3])));
}
let mut last = None;
while let Some(e) = r.take_ready() {
last = Some(e);
}
assert_eq!(
last.expect("at least one emit").ipv4_addresses().len(),
MAX_ADDRS_PER_HOST
);
}
#[test]
fn srv_target_flood_is_capped() {
let mut r = Resolver::new(4); let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let k = fold(&inst);
r.on_ptr(inst);
let mut launched = 0usize;
for i in 0..20u16 {
let host = Name::try_from_str(&format!("h{i}.local.")).unwrap();
launched += r.on_srv(&k, host, 8000 + i).len();
}
assert_eq!(r.hosts_queried.len(), 4);
assert_eq!(r.host_addrs.len(), 0, "no addresses arrived yet");
assert_eq!(launched, 4 * 2);
assert_eq!(r.dropped, 16);
}
#[test]
fn srv_retarget_drops_old_host_addresses() {
let mut r = Resolver::new(16);
let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let h1 = Name::try_from_str("h1.local.").unwrap();
let h2 = Name::try_from_str("h2.local.").unwrap();
let k = fold(&inst);
let hk1 = fold(&h1);
let hk2 = fold(&h2);
r.on_ptr(inst);
r.on_srv(&k, h1, 8000);
r.on_txt(&k, vec![b"k=v".to_vec().into()]);
r.on_addr(&hk1, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
let first = r.take_ready().expect("resolves on h1");
assert_eq!(first.host().as_str(), "h1.local.");
assert_eq!(first.ipv4_addresses(), [Ipv4Addr::new(10, 0, 0, 1)]);
r.on_srv(&k, h2, 8000);
r.on_addr(&hk2, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)));
let second = r.take_ready().expect("re-emits on h2 address");
assert_eq!(second.host().as_str(), "h2.local.");
assert_eq!(
second.ipv4_addresses(),
[Ipv4Addr::new(10, 0, 0, 2)],
"stale h1 address must not survive the retarget"
);
}
#[test]
fn srv_port_zero_still_completes() {
let mut r = Resolver::new(16);
let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let host = Name::try_from_str("h.local.").unwrap();
let k = fold(&inst);
let hk = fold(&host);
r.on_ptr(inst);
r.on_srv(&k, host, 0);
r.on_txt(&k, vec![b"k=v".to_vec().into()]);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
let e = r
.take_ready()
.expect("an SRV with port 0 must still complete");
assert_eq!(e.port(), 0);
assert_eq!(e.ipv4_addresses(), [Ipv4Addr::new(10, 0, 0, 1)]);
}
#[test]
fn txt_change_after_emit_reemits() {
let mut r = Resolver::new(16);
let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let host = Name::try_from_str("h.local.").unwrap();
let k = fold(&inst);
let hk = fold(&host);
r.on_ptr(inst);
r.on_srv(&k, host, 7000);
r.on_txt(&k, vec![b"v=1".to_vec().into()]);
r.on_addr(&hk, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
let first = r.take_ready().expect("first emit");
assert_eq!(first.txt(), [Bytes::from_static(b"v=1")]);
r.on_txt(&k, vec![b"v=2".to_vec().into()]);
let second = r.take_ready().expect("re-emit on TXT change");
assert_eq!(second.txt(), [Bytes::from_static(b"v=2")]);
r.on_txt(&k, vec![b"v=2".to_vec().into()]);
assert!(r.take_ready().is_none(), "duplicate TXT must not re-emit");
}
#[test]
fn srv_retarget_to_cached_host_reemits() {
let mut r = Resolver::new(16);
let i1 = Name::try_from_str("i1._x._tcp.local.").unwrap();
let i2 = Name::try_from_str("i2._x._tcp.local.").unwrap();
let shared = Name::try_from_str("shared.local.").unwrap();
let other = Name::try_from_str("other.local.").unwrap();
let k1 = fold(&i1);
let k2 = fold(&i2);
let shk = fold(&shared);
let ohk = fold(&other);
r.on_ptr(i1);
r.on_srv(&k1, shared.clone(), 8000);
r.on_txt(&k1, vec![b"a=1".to_vec().into()]);
r.on_addr(&shk, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 9)));
r.take_ready().expect("i1 resolves on shared");
r.on_ptr(i2);
r.on_srv(&k2, other, 9000);
r.on_txt(&k2, vec![b"b=2".to_vec().into()]);
r.on_addr(&ohk, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 8)));
let i2_first = r.take_ready().expect("i2 resolves on other");
assert_eq!(i2_first.host().as_str(), "other.local.");
let starts = r.on_srv(&k2, shared, 9001);
assert!(
starts.is_empty(),
"shared host already queried — no new A/AAAA launched"
);
let i2_re = r
.take_ready()
.expect("i2 must re-emit on the cached host without a fresh address event");
assert_eq!(i2_re.host().as_str(), "shared.local.");
assert_eq!(i2_re.port(), 9001);
assert_eq!(
i2_re.ipv4_addresses(),
[Ipv4Addr::new(10, 0, 0, 9)],
"adopts the shared host's cached address, not the stale one"
);
}
#[test]
fn srv_txt_mutation_after_emit_stays_bounded() {
let mut r = Resolver::new(2); let inst = Name::try_from_str("i._x._tcp.local.").unwrap();
let h1 = Name::try_from_str("h1.local.").unwrap();
let k = fold(&inst);
let hk1 = fold(&h1);
r.on_ptr(inst);
r.on_srv(&k, h1, 8000);
r.on_txt(&k, vec![b"k=v".to_vec().into()]);
r.on_addr(&hk1, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
assert!(r.take_ready().is_some(), "instance completes on h1");
for i in 0..10u16 {
let h = Name::try_from_str(&format!("m{i}.local.")).unwrap();
r.on_srv(&k, h, 9000 + i);
r.on_txt(&k, vec![format!("v={i}").into_bytes().into()]);
}
assert!(r.hosts_queried.len() <= 2, "host set stays bounded");
assert!(r.host_addrs.len() <= 2, "host cache stays bounded");
}