Skip to main content

ebus_discovery/
instance.rs

1//! Merging what an mDNS backend reports into service instances.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use crate::addr::ScopedAddr;
8use crate::txt::TxtRecord;
9
10/// A service instance's identity: its type and instance name.
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct InstanceKey {
13    /// The service type without a domain, such as `_mqtt._tcp`.
14    pub service_type: String,
15    /// The instance label alone.
16    pub name: String,
17}
18
19/// Where a backend resolved an instance.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum Slot {
22    /// A backend that reports an instance's whole address set at once (mdns-sd).
23    Whole,
24    /// A backend that resolves an instance per interface and protocol (Avahi).
25    Link {
26        /// The interface index.
27        ifindex: u32,
28        /// True for IPv6, false for IPv4.
29        v6: bool,
30    },
31}
32
33/// One report from a backend.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum Sighting {
36    /// An instance resolved, or resolved again, in one slot.
37    Seen {
38        /// The instance.
39        key: InstanceKey,
40        /// Where it was resolved.
41        slot: Slot,
42        /// The SRV target.
43        server: String,
44        /// The SRV port.
45        port: u16,
46        /// The TXT record.
47        txt: TxtRecord,
48        /// The addresses of this report; they replace the slot's previous ones.
49        addrs: Vec<ScopedAddr>,
50    },
51    /// An instance withdrawn from one slot, or from every slot.
52    Gone {
53        /// The instance.
54        key: InstanceKey,
55        /// The slot; `None` for every slot.
56        slot: Option<Slot>,
57    },
58}
59
60/// A service instance as the table holds it.
61///
62/// Built with [`Instance::new`] outside this crate.
63#[derive(Clone, Debug, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct Instance {
66    /// The instance.
67    pub key: InstanceKey,
68    /// The SRV target of the latest report.
69    pub server: String,
70    /// The SRV port of the latest report.
71    pub port: u16,
72    /// The TXT record of the latest report.
73    pub txt: TxtRecord,
74    /// The union of every slot's addresses, without duplicates, in the order
75    /// the slots and addresses were first reported.
76    pub addrs: Vec<ScopedAddr>,
77}
78
79impl Instance {
80    /// An instance with the given SRV target, port, TXT record and addresses.
81    pub fn new(
82        key: InstanceKey,
83        server: String,
84        port: u16,
85        txt: TxtRecord,
86        addrs: Vec<ScopedAddr>,
87    ) -> Self {
88        Self {
89            key,
90            server,
91            port,
92            txt,
93            addrs,
94        }
95    }
96}
97
98#[derive(Clone, Debug)]
99struct Entry {
100    server: String,
101    port: u16,
102    txt: TxtRecord,
103    slots: Vec<(Slot, Vec<ScopedAddr>)>,
104}
105
106/// The instances a backend currently reports, keyed by (service type,
107/// instance name), with no I/O.
108///
109/// Each slot holds the addresses of its latest report, so an address changed
110/// in place replaces the old one; an instance's server, port and TXT are the
111/// latest reported, and it leaves the table with its last slot.
112#[derive(Clone, Debug, Default)]
113pub struct InstanceTable {
114    entries: BTreeMap<InstanceKey, Entry>,
115}
116
117impl InstanceTable {
118    /// An empty table.
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// Apply one report; true when the instances it yields changed.
124    pub fn handle(&mut self, s: Sighting) -> bool {
125        match s {
126            Sighting::Seen {
127                key,
128                slot,
129                server,
130                port,
131                txt,
132                addrs,
133            } => {
134                let before = self.get(&key);
135                let entry = self.entries.entry(key.clone()).or_insert_with(|| Entry {
136                    server: String::new(),
137                    port: 0,
138                    txt: TxtRecord::new(),
139                    slots: Vec::new(),
140                });
141                entry.server = server;
142                entry.port = port;
143                entry.txt = txt;
144                match entry.slots.iter_mut().find(|(s, _)| *s == slot) {
145                    Some((_, held)) => *held = addrs,
146                    None => entry.slots.push((slot, addrs)),
147                }
148                before != self.get(&key)
149            }
150            Sighting::Gone { key, slot } => {
151                let before = self.get(&key);
152                let Some(entry) = self.entries.get_mut(&key) else {
153                    return false;
154                };
155                match slot {
156                    Some(slot) => entry.slots.retain(|(s, _)| *s != slot),
157                    None => entry.slots.clear(),
158                }
159                if entry.slots.is_empty() {
160                    self.entries.remove(&key);
161                }
162                before != self.get(&key)
163            }
164        }
165    }
166
167    /// One instance.
168    pub fn get(&self, key: &InstanceKey) -> Option<Instance> {
169        self.entries.get(key).map(|e| instance(key, e))
170    }
171
172    /// Every instance, ordered by service type, then instance name.
173    pub fn instances(&self) -> impl Iterator<Item = Instance> + '_ {
174        self.entries.iter().map(|(k, e)| instance(k, e))
175    }
176
177    /// The number of instances.
178    pub fn len(&self) -> usize {
179        self.entries.len()
180    }
181
182    /// True when the table holds no instance.
183    pub fn is_empty(&self) -> bool {
184        self.entries.is_empty()
185    }
186}
187
188fn instance(key: &InstanceKey, e: &Entry) -> Instance {
189    let mut addrs: Vec<ScopedAddr> = Vec::new();
190    for a in e.slots.iter().flat_map(|(_, a)| a) {
191        if !addrs.contains(a) {
192            addrs.push(*a);
193        }
194    }
195    Instance {
196        key: key.clone(),
197        server: e.server.clone(),
198        port: e.port,
199        txt: e.txt.clone(),
200        addrs,
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use alloc::vec;
208
209    fn key(name: &str) -> InstanceKey {
210        InstanceKey {
211            service_type: "_mqtt._tcp".into(),
212            name: name.into(),
213        }
214    }
215
216    fn a(s: &str, ifindex: Option<u32>) -> ScopedAddr {
217        ScopedAddr {
218            ip: s.parse().unwrap(),
219            ifindex,
220        }
221    }
222
223    fn seen(name: &str, slot: Slot, port: u16, addrs: Vec<ScopedAddr>) -> Sighting {
224        Sighting::Seen {
225            key: key(name),
226            slot,
227            server: "b.local.".into(),
228            port,
229            txt: TxtRecord::from_strings(["txtvers=1"]),
230            addrs,
231        }
232    }
233
234    const V4: Slot = Slot::Link {
235        ifindex: 2,
236        v6: false,
237    };
238    const V6: Slot = Slot::Link {
239        ifindex: 2,
240        v6: true,
241    };
242
243    #[test]
244    fn link_slots_merge_into_one_instance() {
245        let mut t = InstanceTable::new();
246        assert!(t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)])));
247        assert!(t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))])));
248        assert_eq!(t.len(), 1);
249        let i = t.get(&key("b")).unwrap();
250        assert_eq!(i.addrs, [a("192.168.1.5", None), a("fe80::1", Some(2))]);
251        assert!(!t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))])));
252    }
253
254    #[test]
255    fn address_replaced_in_its_slot() {
256        let mut t = InstanceTable::new();
257        t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
258        t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))]));
259        assert!(t.handle(seen("b", V4, 1883, vec![a("192.168.1.6", None)])));
260        assert_eq!(
261            t.get(&key("b")).unwrap().addrs,
262            [a("192.168.1.6", None), a("fe80::1", Some(2))]
263        );
264    }
265
266    #[test]
267    fn latest_server_port_and_txt() {
268        let mut t = InstanceTable::new();
269        t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
270        assert!(t.handle(seen("b", V6, 1884, vec![])));
271        assert_eq!(t.get(&key("b")).unwrap().port, 1884);
272    }
273
274    #[test]
275    fn slot_and_whole_removal() {
276        let mut t = InstanceTable::new();
277        t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
278        t.handle(seen("b", V6, 1883, vec![a("fe80::1", Some(2))]));
279        assert!(!t.handle(Sighting::Gone {
280            key: key("b"),
281            slot: Some(Slot::Link {
282                ifindex: 3,
283                v6: false
284            }),
285        }));
286        assert!(t.handle(Sighting::Gone {
287            key: key("b"),
288            slot: Some(V4),
289        }));
290        assert_eq!(t.get(&key("b")).unwrap().addrs, [a("fe80::1", Some(2))]);
291        assert!(t.handle(Sighting::Gone {
292            key: key("b"),
293            slot: Some(V6),
294        }));
295        assert!(t.is_empty());
296        assert!(!t.handle(Sighting::Gone {
297            key: key("b"),
298            slot: None,
299        }));
300    }
301
302    #[test]
303    fn whole_reports() {
304        let mut t = InstanceTable::new();
305        let both = vec![a("192.168.1.5", None), a("fd00::5", None)];
306        assert!(t.handle(seen("b", Slot::Whole, 1883, both.clone())));
307        assert!(t.handle(seen("a", Slot::Whole, 1883, both.clone())));
308        let names: Vec<String> = t.instances().map(|i| i.key.name).collect();
309        assert_eq!(names, ["a", "b"]);
310        assert!(t.handle(seen("b", Slot::Whole, 1883, vec![a("192.168.1.5", None)])));
311        assert!(t.handle(Sighting::Gone {
312            key: key("b"),
313            slot: None,
314        }));
315        assert_eq!(t.len(), 1);
316    }
317
318    #[test]
319    fn removing_a_slot_that_adds_nothing_changes_nothing() {
320        let mut t = InstanceTable::new();
321        t.handle(seen("b", V4, 1883, vec![a("192.168.1.5", None)]));
322        t.handle(seen(
323            "b",
324            Slot::Link {
325                ifindex: 3,
326                v6: false,
327            },
328            1883,
329            vec![a("192.168.1.5", None)],
330        ));
331        assert!(!t.handle(Sighting::Gone {
332            key: key("b"),
333            slot: Some(V4),
334        }));
335        assert_eq!(t.len(), 1);
336    }
337}