1use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use crate::addr::ScopedAddr;
8use crate::txt::TxtRecord;
9
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct InstanceKey {
13 pub service_type: String,
15 pub name: String,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum Slot {
22 Whole,
24 Link {
26 ifindex: u32,
28 v6: bool,
30 },
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum Sighting {
36 Seen {
38 key: InstanceKey,
40 slot: Slot,
42 server: String,
44 port: u16,
46 txt: TxtRecord,
48 addrs: Vec<ScopedAddr>,
50 },
51 Gone {
53 key: InstanceKey,
55 slot: Option<Slot>,
57 },
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct Instance {
66 pub key: InstanceKey,
68 pub server: String,
70 pub port: u16,
72 pub txt: TxtRecord,
74 pub addrs: Vec<ScopedAddr>,
77}
78
79impl Instance {
80 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#[derive(Clone, Debug, Default)]
113pub struct InstanceTable {
114 entries: BTreeMap<InstanceKey, Entry>,
115}
116
117impl InstanceTable {
118 pub fn new() -> Self {
120 Self::default()
121 }
122
123 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 pub fn get(&self, key: &InstanceKey) -> Option<Instance> {
169 self.entries.get(key).map(|e| instance(key, e))
170 }
171
172 pub fn instances(&self) -> impl Iterator<Item = Instance> + '_ {
174 self.entries.iter().map(|(k, e)| instance(k, e))
175 }
176
177 pub fn len(&self) -> usize {
179 self.entries.len()
180 }
181
182 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}