1pub use crate::common::discovery::*;
13
14use std::fs;
15use std::io;
16use std::path::PathBuf;
17
18use rns_core::msgpack::{self, Value};
19use rns_core::stamp::{stamp_valid, stamp_workblock};
20use rns_crypto::sha256::sha256;
21
22use crate::time;
23
24pub struct DiscoveredInterfaceStorage {
30 base_path: PathBuf,
31}
32
33impl DiscoveredInterfaceStorage {
34 pub fn new(base_path: PathBuf) -> Self {
36 Self { base_path }
37 }
38
39 pub fn store(&self, iface: &DiscoveredInterface) -> io::Result<()> {
41 let filename = hex_encode(&iface.discovery_hash);
42 let filepath = self.base_path.join(filename);
43
44 let data = self.serialize_interface(iface)?;
45 fs::write(&filepath, &data)
46 }
47
48 pub fn load(&self, discovery_hash: &[u8; 32]) -> io::Result<Option<DiscoveredInterface>> {
50 let filename = hex_encode(discovery_hash);
51 let filepath = self.base_path.join(filename);
52
53 if !filepath.exists() {
54 return Ok(None);
55 }
56
57 let data = fs::read(&filepath)?;
58 self.deserialize_interface(&data).map(Some)
59 }
60
61 pub fn list(&self) -> io::Result<Vec<DiscoveredInterface>> {
63 let mut interfaces = Vec::new();
64
65 let entries = match fs::read_dir(&self.base_path) {
66 Ok(e) => e,
67 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(interfaces),
68 Err(e) => return Err(e),
69 };
70
71 for entry in entries {
72 let entry = entry?;
73 let path = entry.path();
74
75 if !path.is_file() {
76 continue;
77 }
78
79 match fs::read(&path) {
80 Ok(data) => {
81 if let Ok(iface) = self.deserialize_interface(&data) {
82 interfaces.push(iface);
83 }
84 }
85 Err(_) => continue,
86 }
87 }
88
89 Ok(interfaces)
90 }
91
92 pub fn remove(&self, discovery_hash: &[u8; 32]) -> io::Result<()> {
94 let filename = hex_encode(discovery_hash);
95 let filepath = self.base_path.join(filename);
96
97 if filepath.exists() {
98 fs::remove_file(&filepath)?;
99 }
100 Ok(())
101 }
102
103 pub fn cleanup(&self) -> io::Result<usize> {
106 let mut removed = 0;
107 let now = time::now();
108
109 let interfaces = self.list()?;
110 for iface in interfaces {
111 if now - iface.last_heard > THRESHOLD_REMOVE {
112 self.remove(&iface.discovery_hash)?;
113 removed += 1;
114 }
115 }
116
117 Ok(removed)
118 }
119
120 fn serialize_interface(&self, iface: &DiscoveredInterface) -> io::Result<Vec<u8>> {
122 let mut entries: Vec<(Value, Value)> = Vec::new();
123
124 entries.push((
125 Value::Str("type".into()),
126 Value::Str(iface.interface_type.clone()),
127 ));
128 entries.push((Value::Str("transport".into()), Value::Bool(iface.transport)));
129 entries.push((Value::Str("name".into()), Value::Str(iface.name.clone())));
130 entries.push((
131 Value::Str("discovered".into()),
132 Value::Float(iface.discovered),
133 ));
134 entries.push((
135 Value::Str("last_heard".into()),
136 Value::Float(iface.last_heard),
137 ));
138 entries.push((
139 Value::Str("heard_count".into()),
140 Value::UInt(iface.heard_count as u64),
141 ));
142 entries.push((
143 Value::Str("status".into()),
144 Value::Str(iface.status.as_str().into()),
145 ));
146 entries.push((Value::Str("stamp".into()), Value::Bin(iface.stamp.clone())));
147 entries.push((
148 Value::Str("value".into()),
149 Value::UInt(iface.stamp_value as u64),
150 ));
151 entries.push((
152 Value::Str("transport_id".into()),
153 Value::Bin(iface.transport_id.to_vec()),
154 ));
155 entries.push((
156 Value::Str("network_id".into()),
157 Value::Bin(iface.network_id.to_vec()),
158 ));
159 entries.push((Value::Str("hops".into()), Value::UInt(iface.hops as u64)));
160
161 if let Some(v) = iface.latitude {
162 entries.push((Value::Str("latitude".into()), Value::Float(v)));
163 }
164 if let Some(v) = iface.longitude {
165 entries.push((Value::Str("longitude".into()), Value::Float(v)));
166 }
167 if let Some(v) = iface.height {
168 entries.push((Value::Str("height".into()), Value::Float(v)));
169 }
170 if let Some(ref v) = iface.reachable_on {
171 entries.push((Value::Str("reachable_on".into()), Value::Str(v.clone())));
172 }
173 if let Some(v) = iface.port {
174 entries.push((Value::Str("port".into()), Value::UInt(v as u64)));
175 }
176 if let Some(v) = iface.frequency {
177 entries.push((Value::Str("frequency".into()), Value::UInt(v as u64)));
178 }
179 if let Some(v) = iface.bandwidth {
180 entries.push((Value::Str("bandwidth".into()), Value::UInt(v as u64)));
181 }
182 if let Some(v) = iface.spreading_factor {
183 entries.push((Value::Str("sf".into()), Value::UInt(v as u64)));
184 }
185 if let Some(v) = iface.coding_rate {
186 entries.push((Value::Str("cr".into()), Value::UInt(v as u64)));
187 }
188 if let Some(ref v) = iface.modulation {
189 entries.push((Value::Str("modulation".into()), Value::Str(v.clone())));
190 }
191 if let Some(v) = iface.channel {
192 entries.push((Value::Str("channel".into()), Value::UInt(v as u64)));
193 }
194 if let Some(ref v) = iface.ifac_netname {
195 entries.push((Value::Str("ifac_netname".into()), Value::Str(v.clone())));
196 }
197 if let Some(ref v) = iface.ifac_netkey {
198 entries.push((Value::Str("ifac_netkey".into()), Value::Str(v.clone())));
199 }
200 if let Some(ref v) = iface.config_entry {
201 entries.push((Value::Str("config_entry".into()), Value::Str(v.clone())));
202 }
203
204 entries.push((
205 Value::Str("discovery_hash".into()),
206 Value::Bin(iface.discovery_hash.to_vec()),
207 ));
208
209 Ok(msgpack::pack(&Value::Map(entries)))
210 }
211
212 fn deserialize_interface(&self, data: &[u8]) -> io::Result<DiscoveredInterface> {
214 let (value, _) = msgpack::unpack(data).map_err(|e| {
215 io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e))
216 })?;
217
218 let get_str = |v: &Value, key: &str| -> io::Result<String> {
220 v.map_get(key)
221 .and_then(|val| val.as_str())
222 .map(|s| s.to_string())
223 .ok_or_else(|| {
224 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a string", key))
225 })
226 };
227
228 let get_opt_str = |v: &Value, key: &str| -> Option<String> {
229 v.map_get(key)
230 .and_then(|val| val.as_str().map(|s| s.to_string()))
231 };
232
233 let get_bool = |v: &Value, key: &str| -> io::Result<bool> {
234 v.map_get(key).and_then(|val| val.as_bool()).ok_or_else(|| {
235 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a bool", key))
236 })
237 };
238
239 let get_float = |v: &Value, key: &str| -> io::Result<f64> {
240 v.map_get(key)
241 .and_then(|val| val.as_float())
242 .ok_or_else(|| {
243 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a float", key))
244 })
245 };
246
247 let get_opt_float =
248 |v: &Value, key: &str| -> Option<f64> { v.map_get(key).and_then(|val| val.as_float()) };
249
250 let get_uint = |v: &Value, key: &str| -> io::Result<u64> {
251 v.map_get(key).and_then(|val| val.as_uint()).ok_or_else(|| {
252 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a uint", key))
253 })
254 };
255
256 let get_opt_uint =
257 |v: &Value, key: &str| -> Option<u64> { v.map_get(key).and_then(|val| val.as_uint()) };
258
259 let get_bytes = |v: &Value, key: &str| -> io::Result<Vec<u8>> {
260 v.map_get(key)
261 .and_then(|val| val.as_bin())
262 .map(|b| b.to_vec())
263 .ok_or_else(|| {
264 io::Error::new(io::ErrorKind::InvalidData, format!("{} not bytes", key))
265 })
266 };
267
268 let transport_id_bytes = get_bytes(&value, "transport_id")?;
269 let mut transport_id = [0u8; 16];
270 if transport_id_bytes.len() == 16 {
271 transport_id.copy_from_slice(&transport_id_bytes);
272 }
273
274 let network_id_bytes = get_bytes(&value, "network_id")?;
275 let mut network_id = [0u8; 16];
276 if network_id_bytes.len() == 16 {
277 network_id.copy_from_slice(&network_id_bytes);
278 }
279
280 let discovery_hash_bytes = get_bytes(&value, "discovery_hash")?;
281 let mut discovery_hash = [0u8; 32];
282 if discovery_hash_bytes.len() == 32 {
283 discovery_hash.copy_from_slice(&discovery_hash_bytes);
284 }
285
286 let status_str = get_str(&value, "status")?;
287 let status = match status_str.as_str() {
288 "available" => DiscoveredStatus::Available,
289 "unknown" => DiscoveredStatus::Unknown,
290 "stale" => DiscoveredStatus::Stale,
291 _ => DiscoveredStatus::Unknown,
292 };
293
294 Ok(DiscoveredInterface {
295 interface_type: get_str(&value, "type")?,
296 transport: get_bool(&value, "transport")?,
297 name: get_str(&value, "name")?,
298 discovered: get_float(&value, "discovered")?,
299 last_heard: get_float(&value, "last_heard")?,
300 heard_count: get_uint(&value, "heard_count")? as u32,
301 status,
302 stamp: get_bytes(&value, "stamp")?,
303 stamp_value: get_uint(&value, "value")? as u32,
304 transport_id,
305 network_id,
306 hops: get_uint(&value, "hops")? as u8,
307 latitude: get_opt_float(&value, "latitude"),
308 longitude: get_opt_float(&value, "longitude"),
309 height: get_opt_float(&value, "height"),
310 reachable_on: get_opt_str(&value, "reachable_on"),
311 port: get_opt_uint(&value, "port").map(|v| v as u16),
312 frequency: get_opt_uint(&value, "frequency").map(|v| v as u32),
313 bandwidth: get_opt_uint(&value, "bandwidth").map(|v| v as u32),
314 spreading_factor: get_opt_uint(&value, "sf").map(|v| v as u8),
315 coding_rate: get_opt_uint(&value, "cr").map(|v| v as u8),
316 modulation: get_opt_str(&value, "modulation"),
317 channel: get_opt_uint(&value, "channel").map(|v| v as u8),
318 ifac_netname: get_opt_str(&value, "ifac_netname"),
319 ifac_netkey: get_opt_str(&value, "ifac_netkey"),
320 config_entry: get_opt_str(&value, "config_entry"),
321 discovery_hash,
322 })
323 }
324}
325
326pub fn generate_discovery_stamp(packed_data: &[u8], stamp_cost: u8) -> ([u8; STAMP_SIZE], u32) {
334 use rns_crypto::{OsRng, Rng};
335 use std::sync::atomic::{AtomicBool, Ordering};
336 use std::sync::{Arc, Mutex};
337
338 let infohash = sha256(packed_data);
339 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
340
341 let found: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
342 let result: Arc<Mutex<Option<[u8; STAMP_SIZE]>>> = Arc::new(Mutex::new(None));
343
344 let num_threads = rayon::current_num_threads();
345
346 rayon::scope(|s| {
347 for _ in 0..num_threads {
348 let found = found.clone();
349 let result = result.clone();
350 let workblock = &workblock;
351 s.spawn(move |_| {
352 let mut rng = OsRng;
353 let mut nonce = [0u8; STAMP_SIZE];
354 loop {
355 if found.load(Ordering::Relaxed) {
356 return;
357 }
358 rng.fill_bytes(&mut nonce);
359 if stamp_valid(&nonce, stamp_cost, workblock) {
360 let mut r = result.lock().unwrap();
361 if r.is_none() {
362 *r = Some(nonce);
363 }
364 found.store(true, Ordering::Relaxed);
365 return;
366 }
367 }
368 });
369 }
370 });
371
372 let stamp = result
373 .lock()
374 .unwrap()
375 .take()
376 .expect("stamp search must find result");
377 let value = rns_core::stamp::stamp_value(&workblock, &stamp);
378 (stamp, value)
379}
380
381#[derive(Debug, Clone)]
387pub struct DiscoverableInterface {
388 pub interface_name: String,
390 pub config: DiscoveryConfig,
391 pub transport_enabled: bool,
393 pub ifac_netname: Option<String>,
395 pub ifac_netkey: Option<String>,
397}
398
399pub struct StampResult {
401 pub interface_name: String,
403 pub app_data: Vec<u8>,
405}
406
407pub struct InterfaceAnnouncer {
413 transport_id: [u8; 16],
415 interfaces: Vec<DiscoverableInterface>,
417 last_announced: Vec<f64>,
419 stamp_rx: std::sync::mpsc::Receiver<StampResult>,
421 stamp_tx: std::sync::mpsc::Sender<StampResult>,
423 stamp_pending: bool,
425}
426
427impl InterfaceAnnouncer {
428 pub fn new(transport_id: [u8; 16], interfaces: Vec<DiscoverableInterface>) -> Self {
430 let n = interfaces.len();
431 let (stamp_tx, stamp_rx) = std::sync::mpsc::channel();
432 InterfaceAnnouncer {
433 transport_id,
434 interfaces,
435 last_announced: vec![0.0; n],
436 stamp_rx,
437 stamp_tx,
438 stamp_pending: false,
439 }
440 }
441
442 pub fn maybe_start(&mut self, now: f64) {
446 if self.stamp_pending {
447 return;
448 }
449 let due_index = self.interfaces.iter().enumerate().find_map(|(i, iface)| {
450 let elapsed = now - self.last_announced[i];
451 if elapsed >= iface.config.announce_interval as f64 {
452 Some(i)
453 } else {
454 None
455 }
456 });
457
458 if let Some(idx) = due_index {
459 let packed = self.pack_interface_info(idx);
460 let stamp_cost = self.interfaces[idx].config.stamp_value;
461 let name = self.interfaces[idx].config.discovery_name.clone();
462 let interface_name = self.interfaces[idx].interface_name.clone();
463 let tx = self.stamp_tx.clone();
464
465 log::info!(
466 "Spawning discovery stamp generation (cost={}) for '{}'...",
467 stamp_cost,
468 name,
469 );
470
471 self.stamp_pending = true;
472 self.last_announced[idx] = now;
473
474 std::thread::spawn(move || {
475 let (stamp, value) = generate_discovery_stamp(&packed, stamp_cost);
476 log::info!("Discovery stamp generated (value={}) for '{}'", value, name,);
477
478 let flags: u8 = 0x00; let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
480 app_data.push(flags);
481 app_data.extend_from_slice(&packed);
482 app_data.extend_from_slice(&stamp);
483
484 let _ = tx.send(StampResult {
485 interface_name,
486 app_data,
487 });
488 });
489 }
490 }
491
492 pub fn poll_ready(&mut self) -> Option<StampResult> {
495 match self.stamp_rx.try_recv() {
496 Ok(result) => {
497 self.stamp_pending = false;
498 Some(result)
499 }
500 Err(_) => None,
501 }
502 }
503
504 pub fn contains_interface(&self, interface_name: &str) -> bool {
506 self.interfaces
507 .iter()
508 .any(|iface| iface.interface_name == interface_name)
509 }
510
511 pub fn upsert_interface(&mut self, iface: DiscoverableInterface) {
513 if let Some(index) = self
514 .interfaces
515 .iter()
516 .position(|existing| existing.interface_name == iface.interface_name)
517 {
518 self.interfaces[index] = iface;
519 return;
520 }
521 self.interfaces.push(iface);
522 self.last_announced.push(0.0);
523 }
524
525 pub fn remove_interface(&mut self, interface_name: &str) -> bool {
527 if let Some(index) = self
528 .interfaces
529 .iter()
530 .position(|iface| iface.interface_name == interface_name)
531 {
532 self.interfaces.remove(index);
533 self.last_announced.remove(index);
534 true
535 } else {
536 false
537 }
538 }
539
540 pub fn is_empty(&self) -> bool {
542 self.interfaces.is_empty()
543 }
544
545 fn pack_interface_info(&self, index: usize) -> Vec<u8> {
547 let iface = &self.interfaces[index];
548 let mut entries: Vec<(msgpack::Value, msgpack::Value)> = Vec::new();
549
550 entries.push((
551 msgpack::Value::UInt(INTERFACE_TYPE as u64),
552 msgpack::Value::Str(iface.config.interface_type.clone()),
553 ));
554 entries.push((
555 msgpack::Value::UInt(TRANSPORT as u64),
556 msgpack::Value::Bool(iface.transport_enabled),
557 ));
558 entries.push((
559 msgpack::Value::UInt(NAME as u64),
560 msgpack::Value::Str(iface.config.discovery_name.clone()),
561 ));
562 entries.push((
563 msgpack::Value::UInt(TRANSPORT_ID as u64),
564 msgpack::Value::Bin(self.transport_id.to_vec()),
565 ));
566 if let Some(ref reachable) = iface.config.reachable_on {
567 entries.push((
568 msgpack::Value::UInt(REACHABLE_ON as u64),
569 msgpack::Value::Str(reachable.clone()),
570 ));
571 }
572 if let Some(port) = iface.config.listen_port {
573 entries.push((
574 msgpack::Value::UInt(PORT as u64),
575 msgpack::Value::UInt(port as u64),
576 ));
577 }
578 if let Some(lat) = iface.config.latitude {
579 entries.push((
580 msgpack::Value::UInt(LATITUDE as u64),
581 msgpack::Value::Float(lat),
582 ));
583 }
584 if let Some(lon) = iface.config.longitude {
585 entries.push((
586 msgpack::Value::UInt(LONGITUDE as u64),
587 msgpack::Value::Float(lon),
588 ));
589 }
590 if let Some(h) = iface.config.height {
591 entries.push((
592 msgpack::Value::UInt(HEIGHT as u64),
593 msgpack::Value::Float(h),
594 ));
595 }
596 if let Some(ref netname) = iface.ifac_netname {
597 entries.push((
598 msgpack::Value::UInt(IFAC_NETNAME as u64),
599 msgpack::Value::Str(netname.clone()),
600 ));
601 }
602 if let Some(ref netkey) = iface.ifac_netkey {
603 entries.push((
604 msgpack::Value::UInt(IFAC_NETKEY as u64),
605 msgpack::Value::Str(netkey.clone()),
606 ));
607 }
608
609 msgpack::pack(&msgpack::Value::Map(entries))
610 }
611}
612
613#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn test_hex_encode() {
623 assert_eq!(hex_encode(&[0x00, 0xff, 0x12]), "00ff12");
624 assert_eq!(hex_encode(&[]), "");
625 }
626
627 #[test]
628 fn test_compute_discovery_hash() {
629 let transport_id = [0x42u8; 16];
630 let name = "TestInterface";
631 let hash = compute_discovery_hash(&transport_id, name);
632
633 let hash2 = compute_discovery_hash(&transport_id, name);
635 assert_eq!(hash, hash2);
636
637 let hash3 = compute_discovery_hash(&transport_id, "OtherInterface");
639 assert_ne!(hash, hash3);
640 }
641
642 #[test]
643 fn test_is_ip_address() {
644 assert!(is_ip_address("192.168.1.1"));
645 assert!(is_ip_address("::1"));
646 assert!(is_ip_address("2001:db8::1"));
647 assert!(!is_ip_address("not-an-ip"));
648 assert!(!is_ip_address("hostname.example.com"));
649 }
650
651 #[test]
652 fn test_is_hostname() {
653 assert!(is_hostname("example.com"));
654 assert!(is_hostname("sub.example.com"));
655 assert!(is_hostname("my-node"));
656 assert!(is_hostname("my-node.example.com"));
657 assert!(!is_hostname(""));
658 assert!(!is_hostname("-invalid"));
659 assert!(!is_hostname("invalid-"));
660 assert!(!is_hostname("a".repeat(300).as_str()));
661 }
662
663 #[test]
664 fn test_discovered_status() {
665 let now = time::now();
666
667 let mut iface = DiscoveredInterface {
668 interface_type: "TestInterface".into(),
669 transport: true,
670 name: "Test".into(),
671 discovered: now,
672 last_heard: now,
673 heard_count: 0,
674 status: DiscoveredStatus::Available,
675 stamp: vec![],
676 stamp_value: 14,
677 transport_id: [0u8; 16],
678 network_id: [0u8; 16],
679 hops: 0,
680 latitude: None,
681 longitude: None,
682 height: None,
683 reachable_on: None,
684 port: None,
685 frequency: None,
686 bandwidth: None,
687 spreading_factor: None,
688 coding_rate: None,
689 modulation: None,
690 channel: None,
691 ifac_netname: None,
692 ifac_netkey: None,
693 config_entry: None,
694 discovery_hash: [0u8; 32],
695 };
696
697 assert_eq!(iface.compute_status(), DiscoveredStatus::Available);
699
700 iface.last_heard = now - THRESHOLD_UNKNOWN - 3600.0;
702 assert_eq!(iface.compute_status(), DiscoveredStatus::Unknown);
703
704 iface.last_heard = now - THRESHOLD_STALE - 3600.0;
706 assert_eq!(iface.compute_status(), DiscoveredStatus::Stale);
707 }
708
709 #[test]
710 fn test_storage_roundtrip() {
711 use std::sync::atomic::{AtomicU64, Ordering};
712 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
713
714 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
715 let dir =
716 std::env::temp_dir().join(format!("rns-discovery-test-{}-{}", std::process::id(), id));
717 let _ = fs::remove_dir_all(&dir);
718 fs::create_dir_all(&dir).unwrap();
719
720 let storage = DiscoveredInterfaceStorage::new(dir.clone());
721
722 let iface = DiscoveredInterface {
723 interface_type: "BackboneInterface".into(),
724 transport: true,
725 name: "TestNode".into(),
726 discovered: 1700000000.0,
727 last_heard: 1700001000.0,
728 heard_count: 5,
729 status: DiscoveredStatus::Available,
730 stamp: vec![0x42u8; 64],
731 stamp_value: 18,
732 transport_id: [0x01u8; 16],
733 network_id: [0x02u8; 16],
734 hops: 2,
735 latitude: Some(45.0),
736 longitude: Some(9.0),
737 height: Some(100.0),
738 reachable_on: Some("example.com".into()),
739 port: Some(4242),
740 frequency: None,
741 bandwidth: None,
742 spreading_factor: None,
743 coding_rate: None,
744 modulation: None,
745 channel: None,
746 ifac_netname: Some("mynetwork".into()),
747 ifac_netkey: Some("secretkey".into()),
748 config_entry: Some("test config".into()),
749 discovery_hash: compute_discovery_hash(&[0x01u8; 16], "TestNode"),
750 };
751
752 storage.store(&iface).unwrap();
754
755 let loaded = storage.load(&iface.discovery_hash).unwrap().unwrap();
757
758 assert_eq!(loaded.interface_type, iface.interface_type);
759 assert_eq!(loaded.name, iface.name);
760 assert_eq!(loaded.stamp_value, iface.stamp_value);
761 assert_eq!(loaded.transport_id, iface.transport_id);
762 assert_eq!(loaded.hops, iface.hops);
763 assert_eq!(loaded.latitude, iface.latitude);
764 assert_eq!(loaded.reachable_on, iface.reachable_on);
765 assert_eq!(loaded.port, iface.port);
766
767 let list = storage.list().unwrap();
769 assert_eq!(list.len(), 1);
770
771 storage.remove(&iface.discovery_hash).unwrap();
773 let list = storage.list().unwrap();
774 assert!(list.is_empty());
775
776 let _ = fs::remove_dir_all(&dir);
777 }
778
779 #[test]
780 fn test_filter_and_sort() {
781 let now = time::now();
782
783 let ifaces = vec![
784 DiscoveredInterface {
785 interface_type: "A".into(),
786 transport: true,
787 name: "high-value-stale".into(),
788 discovered: now,
789 last_heard: now - THRESHOLD_STALE - 100.0, heard_count: 0,
791 status: DiscoveredStatus::Stale,
792 stamp: vec![],
793 stamp_value: 20,
794 transport_id: [0u8; 16],
795 network_id: [0u8; 16],
796 hops: 0,
797 latitude: None,
798 longitude: None,
799 height: None,
800 reachable_on: None,
801 port: None,
802 frequency: None,
803 bandwidth: None,
804 spreading_factor: None,
805 coding_rate: None,
806 modulation: None,
807 channel: None,
808 ifac_netname: None,
809 ifac_netkey: None,
810 config_entry: None,
811 discovery_hash: [0u8; 32],
812 },
813 DiscoveredInterface {
814 interface_type: "B".into(),
815 transport: true,
816 name: "low-value-available".into(),
817 discovered: now,
818 last_heard: now - 10.0, heard_count: 0,
820 status: DiscoveredStatus::Available,
821 stamp: vec![],
822 stamp_value: 10,
823 transport_id: [0u8; 16],
824 network_id: [0u8; 16],
825 hops: 0,
826 latitude: None,
827 longitude: None,
828 height: None,
829 reachable_on: None,
830 port: None,
831 frequency: None,
832 bandwidth: None,
833 spreading_factor: None,
834 coding_rate: None,
835 modulation: None,
836 channel: None,
837 ifac_netname: None,
838 ifac_netkey: None,
839 config_entry: None,
840 discovery_hash: [1u8; 32],
841 },
842 DiscoveredInterface {
843 interface_type: "C".into(),
844 transport: false,
845 name: "high-value-available".into(),
846 discovered: now,
847 last_heard: now - 10.0, heard_count: 0,
849 status: DiscoveredStatus::Available,
850 stamp: vec![],
851 stamp_value: 20,
852 transport_id: [0u8; 16],
853 network_id: [0u8; 16],
854 hops: 0,
855 latitude: None,
856 longitude: None,
857 height: None,
858 reachable_on: None,
859 port: None,
860 frequency: None,
861 bandwidth: None,
862 spreading_factor: None,
863 coding_rate: None,
864 modulation: None,
865 channel: None,
866 ifac_netname: None,
867 ifac_netkey: None,
868 config_entry: None,
869 discovery_hash: [2u8; 32],
870 },
871 ];
872
873 let mut result = ifaces.clone();
875 filter_and_sort_interfaces(&mut result, false, false);
876 assert_eq!(result.len(), 3);
877 assert_eq!(result[0].name, "high-value-available");
879 assert_eq!(result[1].name, "low-value-available");
880 assert_eq!(result[2].name, "high-value-stale");
881
882 let mut result = ifaces.clone();
884 filter_and_sort_interfaces(&mut result, true, false);
885 assert_eq!(result.len(), 2); let mut result = ifaces.clone();
889 filter_and_sort_interfaces(&mut result, false, true);
890 assert_eq!(result.len(), 2); }
892
893 #[test]
894 fn test_discovery_name_hash_deterministic() {
895 let h1 = discovery_name_hash();
896 let h2 = discovery_name_hash();
897 assert_eq!(h1, h2);
898 assert_ne!(h1, [0u8; 10]); }
900}