use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use crate::addr::ScopedAddr;
use crate::txt::TxtRecord;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InstanceKey {
pub service_type: String,
pub name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Slot {
Whole,
Link {
ifindex: u32,
v6: bool,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Sighting {
Seen {
key: InstanceKey,
slot: Slot,
server: String,
port: u16,
txt: TxtRecord,
addrs: Vec<ScopedAddr>,
},
Gone {
key: InstanceKey,
slot: Option<Slot>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Instance {
pub key: InstanceKey,
pub server: String,
pub port: u16,
pub txt: TxtRecord,
pub addrs: Vec<ScopedAddr>,
}
impl Instance {
pub fn new(
key: InstanceKey,
server: String,
port: u16,
txt: TxtRecord,
addrs: Vec<ScopedAddr>,
) -> Self {
Self {
key,
server,
port,
txt,
addrs,
}
}
}
#[derive(Clone, Debug)]
struct Entry {
server: String,
port: u16,
txt: TxtRecord,
slots: Vec<(Slot, Vec<ScopedAddr>)>,
}
#[derive(Clone, Debug, Default)]
pub struct InstanceTable {
entries: BTreeMap<InstanceKey, Entry>,
}
impl InstanceTable {
pub fn new() -> Self {
Self::default()
}
pub fn handle(&mut self, s: Sighting) -> bool {
match s {
Sighting::Seen {
key,
slot,
server,
port,
txt,
addrs,
} => {
let before = self.get(&key);
let entry = self.entries.entry(key.clone()).or_insert_with(|| Entry {
server: String::new(),
port: 0,
txt: TxtRecord::new(),
slots: Vec::new(),
});
entry.server = server;
entry.port = port;
entry.txt = txt;
match entry.slots.iter_mut().find(|(s, _)| *s == slot) {
Some((_, held)) => *held = addrs,
None => entry.slots.push((slot, addrs)),
}
before != self.get(&key)
}
Sighting::Gone { key, slot } => {
let before = self.get(&key);
let Some(entry) = self.entries.get_mut(&key) else {
return false;
};
match slot {
Some(slot) => entry.slots.retain(|(s, _)| *s != slot),
None => entry.slots.clear(),
}
if entry.slots.is_empty() {
self.entries.remove(&key);
}
before != self.get(&key)
}
}
}
pub fn get(&self, key: &InstanceKey) -> Option<Instance> {
self.entries.get(key).map(|e| instance(key, e))
}
pub fn instances(&self) -> impl Iterator<Item = Instance> + '_ {
self.entries.iter().map(|(k, e)| instance(k, e))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
fn instance(key: &InstanceKey, e: &Entry) -> Instance {
let mut addrs: Vec<ScopedAddr> = Vec::new();
for a in e.slots.iter().flat_map(|(_, a)| a) {
if !addrs.contains(a) {
addrs.push(*a);
}
}
Instance {
key: key.clone(),
server: e.server.clone(),
port: e.port,
txt: e.txt.clone(),
addrs,
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn key(name: &str) -> InstanceKey {
InstanceKey {
service_type: "_mqtt._tcp".into(),
name: name.into(),
}
}
fn a(s: &str, ifindex: Option<u32>) -> ScopedAddr {
ScopedAddr {
ip: s.parse().unwrap(),
ifindex,
}
}
fn seen(name: &str, slot: Slot, port: u16, addrs: Vec<ScopedAddr>) -> Sighting {
Sighting::Seen {
key: key(name),
slot,
server: "b.local.".into(),
port,
txt: TxtRecord::from_strings(["txtvers=1"]),
addrs,
}
}
const V4: Slot = Slot::Link {
ifindex: 2,
v6: false,
};
const V6: Slot = Slot::Link {
ifindex: 2,
v6: true,
};
#[test]
fn link_slots_merge_into_one_instance() {
let mut t = InstanceTable::new();
assert!(t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)])));
assert!(t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))])));
assert_eq!(t.len(), 1);
let i = t.get(&key("b")).unwrap();
assert_eq!(i.addrs, [a("192.168.1.5", None), a("fe80::1", Some(2))]);
assert!(!t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))])));
}
#[test]
fn address_replaced_in_its_slot() {
let mut t = InstanceTable::new();
t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))]));
assert!(t.handle(seen("b", V4, 1883, vec![a("192.168.1.6", None)])));
assert_eq!(
t.get(&key("b")).unwrap().addrs,
[a("192.168.1.6", None), a("fe80::1", Some(2))]
);
}
#[test]
fn latest_server_port_and_txt() {
let mut t = InstanceTable::new();
t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
assert!(t.handle(seen("b", V6, 1884, vec![])));
assert_eq!(t.get(&key("b")).unwrap().port, 1884);
}
#[test]
fn slot_and_whole_removal() {
let mut t = InstanceTable::new();
t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))]));
assert!(!t.handle(Sighting::Gone {
key: key("b"),
slot: Some(Slot::Link {
ifindex: 3,
v6: false
}),
}));
assert!(t.handle(Sighting::Gone {
key: key("b"),
slot: Some(V4),
}));
assert_eq!(t.get(&key("b")).unwrap().addrs, [a("fe80::1", Some(2))]);
assert!(t.handle(Sighting::Gone {
key: key("b"),
slot: Some(V6),
}));
assert!(t.is_empty());
assert!(!t.handle(Sighting::Gone {
key: key("b"),
slot: None,
}));
}
#[test]
fn whole_reports() {
let mut t = InstanceTable::new();
let both = vec![a("192.168.1.5", None), a("fd00::5", None)];
assert!(t.handle(seen("b", Slot::Whole, 1883, both.clone())));
assert!(t.handle(seen("a", Slot::Whole, 1883, both.clone())));
let names: Vec<String> = t.instances().map(|i| i.key.name).collect();
assert_eq!(names, ["a", "b"]);
assert!(t.handle(seen("b", Slot::Whole, 1883, vec![a("192.168.1.5", None)])));
assert!(t.handle(Sighting::Gone {
key: key("b"),
slot: None,
}));
assert_eq!(t.len(), 1);
}
#[test]
fn removing_a_slot_that_adds_nothing_changes_nothing() {
let mut t = InstanceTable::new();
t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
t.handle(seen(
"b",
Slot::Link {
ifindex: 3,
v6: false,
},
1883,
vec![a("192.168.1.5", None)],
));
assert!(!t.handle(Sighting::Gone {
key: key("b"),
slot: Some(V4),
}));
assert_eq!(t.len(), 1);
}
}