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((Value::Str("type".into()), Value::Str(iface.interface_type.clone())));
125 entries.push((Value::Str("transport".into()), Value::Bool(iface.transport)));
126 entries.push((Value::Str("name".into()), Value::Str(iface.name.clone())));
127 entries.push((Value::Str("discovered".into()), Value::Float(iface.discovered)));
128 entries.push((Value::Str("last_heard".into()), Value::Float(iface.last_heard)));
129 entries.push((Value::Str("heard_count".into()), Value::UInt(iface.heard_count as u64)));
130 entries.push((Value::Str("status".into()), Value::Str(iface.status.as_str().into())));
131 entries.push((Value::Str("stamp".into()), Value::Bin(iface.stamp.clone())));
132 entries.push((Value::Str("value".into()), Value::UInt(iface.stamp_value as u64)));
133 entries.push((Value::Str("transport_id".into()), Value::Bin(iface.transport_id.to_vec())));
134 entries.push((Value::Str("network_id".into()), Value::Bin(iface.network_id.to_vec())));
135 entries.push((Value::Str("hops".into()), Value::UInt(iface.hops as u64)));
136
137 if let Some(v) = iface.latitude {
138 entries.push((Value::Str("latitude".into()), Value::Float(v)));
139 }
140 if let Some(v) = iface.longitude {
141 entries.push((Value::Str("longitude".into()), Value::Float(v)));
142 }
143 if let Some(v) = iface.height {
144 entries.push((Value::Str("height".into()), Value::Float(v)));
145 }
146 if let Some(ref v) = iface.reachable_on {
147 entries.push((Value::Str("reachable_on".into()), Value::Str(v.clone())));
148 }
149 if let Some(v) = iface.port {
150 entries.push((Value::Str("port".into()), Value::UInt(v as u64)));
151 }
152 if let Some(v) = iface.frequency {
153 entries.push((Value::Str("frequency".into()), Value::UInt(v as u64)));
154 }
155 if let Some(v) = iface.bandwidth {
156 entries.push((Value::Str("bandwidth".into()), Value::UInt(v as u64)));
157 }
158 if let Some(v) = iface.spreading_factor {
159 entries.push((Value::Str("sf".into()), Value::UInt(v as u64)));
160 }
161 if let Some(v) = iface.coding_rate {
162 entries.push((Value::Str("cr".into()), Value::UInt(v as u64)));
163 }
164 if let Some(ref v) = iface.modulation {
165 entries.push((Value::Str("modulation".into()), Value::Str(v.clone())));
166 }
167 if let Some(v) = iface.channel {
168 entries.push((Value::Str("channel".into()), Value::UInt(v as u64)));
169 }
170 if let Some(ref v) = iface.ifac_netname {
171 entries.push((Value::Str("ifac_netname".into()), Value::Str(v.clone())));
172 }
173 if let Some(ref v) = iface.ifac_netkey {
174 entries.push((Value::Str("ifac_netkey".into()), Value::Str(v.clone())));
175 }
176 if let Some(ref v) = iface.config_entry {
177 entries.push((Value::Str("config_entry".into()), Value::Str(v.clone())));
178 }
179
180 entries.push((Value::Str("discovery_hash".into()), Value::Bin(iface.discovery_hash.to_vec())));
181
182 Ok(msgpack::pack(&Value::Map(entries)))
183 }
184
185 fn deserialize_interface(&self, data: &[u8]) -> io::Result<DiscoveredInterface> {
187 let (value, _) = msgpack::unpack(data).map_err(|e| {
188 io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e))
189 })?;
190
191 let get_str = |v: &Value, key: &str| -> io::Result<String> {
193 v.map_get(key)
194 .and_then(|val| val.as_str())
195 .map(|s| s.to_string())
196 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("{} not a string", key)))
197 };
198
199 let get_opt_str = |v: &Value, key: &str| -> Option<String> {
200 v.map_get(key).and_then(|val| val.as_str().map(|s| s.to_string()))
201 };
202
203 let get_bool = |v: &Value, key: &str| -> io::Result<bool> {
204 v.map_get(key)
205 .and_then(|val| val.as_bool())
206 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("{} not a bool", key)))
207 };
208
209 let get_float = |v: &Value, key: &str| -> io::Result<f64> {
210 v.map_get(key)
211 .and_then(|val| val.as_float())
212 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("{} not a float", key)))
213 };
214
215 let get_opt_float = |v: &Value, key: &str| -> Option<f64> {
216 v.map_get(key).and_then(|val| val.as_float())
217 };
218
219 let get_uint = |v: &Value, key: &str| -> io::Result<u64> {
220 v.map_get(key)
221 .and_then(|val| val.as_uint())
222 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("{} not a uint", key)))
223 };
224
225 let get_opt_uint = |v: &Value, key: &str| -> Option<u64> {
226 v.map_get(key).and_then(|val| val.as_uint())
227 };
228
229 let get_bytes = |v: &Value, key: &str| -> io::Result<Vec<u8>> {
230 v.map_get(key)
231 .and_then(|val| val.as_bin())
232 .map(|b| b.to_vec())
233 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("{} not bytes", key)))
234 };
235
236 let transport_id_bytes = get_bytes(&value, "transport_id")?;
237 let mut transport_id = [0u8; 16];
238 if transport_id_bytes.len() == 16 {
239 transport_id.copy_from_slice(&transport_id_bytes);
240 }
241
242 let network_id_bytes = get_bytes(&value, "network_id")?;
243 let mut network_id = [0u8; 16];
244 if network_id_bytes.len() == 16 {
245 network_id.copy_from_slice(&network_id_bytes);
246 }
247
248 let discovery_hash_bytes = get_bytes(&value, "discovery_hash")?;
249 let mut discovery_hash = [0u8; 32];
250 if discovery_hash_bytes.len() == 32 {
251 discovery_hash.copy_from_slice(&discovery_hash_bytes);
252 }
253
254 let status_str = get_str(&value, "status")?;
255 let status = match status_str.as_str() {
256 "available" => DiscoveredStatus::Available,
257 "unknown" => DiscoveredStatus::Unknown,
258 "stale" => DiscoveredStatus::Stale,
259 _ => DiscoveredStatus::Unknown,
260 };
261
262 Ok(DiscoveredInterface {
263 interface_type: get_str(&value, "type")?,
264 transport: get_bool(&value, "transport")?,
265 name: get_str(&value, "name")?,
266 discovered: get_float(&value, "discovered")?,
267 last_heard: get_float(&value, "last_heard")?,
268 heard_count: get_uint(&value, "heard_count")? as u32,
269 status,
270 stamp: get_bytes(&value, "stamp")?,
271 stamp_value: get_uint(&value, "value")? as u32,
272 transport_id,
273 network_id,
274 hops: get_uint(&value, "hops")? as u8,
275 latitude: get_opt_float(&value, "latitude"),
276 longitude: get_opt_float(&value, "longitude"),
277 height: get_opt_float(&value, "height"),
278 reachable_on: get_opt_str(&value, "reachable_on"),
279 port: get_opt_uint(&value, "port").map(|v| v as u16),
280 frequency: get_opt_uint(&value, "frequency").map(|v| v as u32),
281 bandwidth: get_opt_uint(&value, "bandwidth").map(|v| v as u32),
282 spreading_factor: get_opt_uint(&value, "sf").map(|v| v as u8),
283 coding_rate: get_opt_uint(&value, "cr").map(|v| v as u8),
284 modulation: get_opt_str(&value, "modulation"),
285 channel: get_opt_uint(&value, "channel").map(|v| v as u8),
286 ifac_netname: get_opt_str(&value, "ifac_netname"),
287 ifac_netkey: get_opt_str(&value, "ifac_netkey"),
288 config_entry: get_opt_str(&value, "config_entry"),
289 discovery_hash,
290 })
291 }
292}
293
294pub fn generate_discovery_stamp(
302 packed_data: &[u8],
303 stamp_cost: u8,
304) -> ([u8; STAMP_SIZE], u32) {
305 use std::sync::atomic::{AtomicBool, Ordering};
306 use std::sync::{Arc, Mutex};
307 use rns_crypto::{OsRng, Rng};
308
309 let infohash = sha256(packed_data);
310 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
311
312 let found: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
313 let result: Arc<Mutex<Option<[u8; STAMP_SIZE]>>> = Arc::new(Mutex::new(None));
314
315 let num_threads = rayon::current_num_threads();
316
317 rayon::scope(|s| {
318 for _ in 0..num_threads {
319 let found = found.clone();
320 let result = result.clone();
321 let workblock = &workblock;
322 s.spawn(move |_| {
323 let mut rng = OsRng;
324 let mut nonce = [0u8; STAMP_SIZE];
325 loop {
326 if found.load(Ordering::Relaxed) {
327 return;
328 }
329 rng.fill_bytes(&mut nonce);
330 if stamp_valid(&nonce, stamp_cost, workblock) {
331 let mut r = result.lock().unwrap();
332 if r.is_none() {
333 *r = Some(nonce);
334 }
335 found.store(true, Ordering::Relaxed);
336 return;
337 }
338 }
339 });
340 }
341 });
342
343 let stamp = result.lock().unwrap().take().expect("stamp search must find result");
344 let value = rns_core::stamp::stamp_value(&workblock, &stamp);
345 (stamp, value)
346}
347
348#[derive(Debug, Clone)]
354pub struct DiscoverableInterface {
355 pub config: DiscoveryConfig,
356 pub transport_enabled: bool,
358 pub ifac_netname: Option<String>,
360 pub ifac_netkey: Option<String>,
362}
363
364pub struct StampResult {
366 pub index: usize,
368 pub app_data: Vec<u8>,
370}
371
372pub struct InterfaceAnnouncer {
378 transport_id: [u8; 16],
380 interfaces: Vec<DiscoverableInterface>,
382 last_announced: Vec<f64>,
384 stamp_rx: std::sync::mpsc::Receiver<StampResult>,
386 stamp_tx: std::sync::mpsc::Sender<StampResult>,
388 stamp_pending: bool,
390}
391
392impl InterfaceAnnouncer {
393 pub fn new(transport_id: [u8; 16], interfaces: Vec<DiscoverableInterface>) -> Self {
395 let n = interfaces.len();
396 let (stamp_tx, stamp_rx) = std::sync::mpsc::channel();
397 InterfaceAnnouncer {
398 transport_id,
399 interfaces,
400 last_announced: vec![0.0; n],
401 stamp_rx,
402 stamp_tx,
403 stamp_pending: false,
404 }
405 }
406
407 pub fn maybe_start(&mut self, now: f64) {
411 if self.stamp_pending {
412 return;
413 }
414 let due_index = self.interfaces.iter().enumerate().find_map(|(i, iface)| {
415 let elapsed = now - self.last_announced[i];
416 if elapsed >= iface.config.announce_interval as f64 {
417 Some(i)
418 } else {
419 None
420 }
421 });
422
423 if let Some(idx) = due_index {
424 let packed = self.pack_interface_info(idx);
425 let stamp_cost = self.interfaces[idx].config.stamp_value;
426 let name = self.interfaces[idx].config.discovery_name.clone();
427 let tx = self.stamp_tx.clone();
428
429 log::info!(
430 "Spawning discovery stamp generation (cost={}) for '{}'...",
431 stamp_cost,
432 name,
433 );
434
435 self.stamp_pending = true;
436 self.last_announced[idx] = now;
437
438 std::thread::spawn(move || {
439 let (stamp, value) = generate_discovery_stamp(&packed, stamp_cost);
440 log::info!(
441 "Discovery stamp generated (value={}) for '{}'",
442 value,
443 name,
444 );
445
446 let flags: u8 = 0x00; let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
448 app_data.push(flags);
449 app_data.extend_from_slice(&packed);
450 app_data.extend_from_slice(&stamp);
451
452 let _ = tx.send(StampResult {
453 index: idx,
454 app_data,
455 });
456 });
457 }
458 }
459
460 pub fn poll_ready(&mut self) -> Option<StampResult> {
463 match self.stamp_rx.try_recv() {
464 Ok(result) => {
465 self.stamp_pending = false;
466 Some(result)
467 }
468 Err(_) => None,
469 }
470 }
471
472 fn pack_interface_info(&self, index: usize) -> Vec<u8> {
474 let iface = &self.interfaces[index];
475 let mut entries: Vec<(msgpack::Value, msgpack::Value)> = Vec::new();
476
477 entries.push((
478 msgpack::Value::UInt(INTERFACE_TYPE as u64),
479 msgpack::Value::Str(iface.config.interface_type.clone()),
480 ));
481 entries.push((
482 msgpack::Value::UInt(TRANSPORT as u64),
483 msgpack::Value::Bool(iface.transport_enabled),
484 ));
485 entries.push((
486 msgpack::Value::UInt(NAME as u64),
487 msgpack::Value::Str(iface.config.discovery_name.clone()),
488 ));
489 entries.push((
490 msgpack::Value::UInt(TRANSPORT_ID as u64),
491 msgpack::Value::Bin(self.transport_id.to_vec()),
492 ));
493 if let Some(ref reachable) = iface.config.reachable_on {
494 entries.push((
495 msgpack::Value::UInt(REACHABLE_ON as u64),
496 msgpack::Value::Str(reachable.clone()),
497 ));
498 }
499 if let Some(port) = iface.config.listen_port {
500 entries.push((
501 msgpack::Value::UInt(PORT as u64),
502 msgpack::Value::UInt(port as u64),
503 ));
504 }
505 if let Some(lat) = iface.config.latitude {
506 entries.push((
507 msgpack::Value::UInt(LATITUDE as u64),
508 msgpack::Value::Float(lat),
509 ));
510 }
511 if let Some(lon) = iface.config.longitude {
512 entries.push((
513 msgpack::Value::UInt(LONGITUDE as u64),
514 msgpack::Value::Float(lon),
515 ));
516 }
517 if let Some(h) = iface.config.height {
518 entries.push((
519 msgpack::Value::UInt(HEIGHT as u64),
520 msgpack::Value::Float(h),
521 ));
522 }
523 if let Some(ref netname) = iface.ifac_netname {
524 entries.push((
525 msgpack::Value::UInt(IFAC_NETNAME as u64),
526 msgpack::Value::Str(netname.clone()),
527 ));
528 }
529 if let Some(ref netkey) = iface.ifac_netkey {
530 entries.push((
531 msgpack::Value::UInt(IFAC_NETKEY as u64),
532 msgpack::Value::Str(netkey.clone()),
533 ));
534 }
535
536 msgpack::pack(&msgpack::Value::Map(entries))
537 }
538
539}
540
541#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_hex_encode() {
551 assert_eq!(hex_encode(&[0x00, 0xff, 0x12]), "00ff12");
552 assert_eq!(hex_encode(&[]), "");
553 }
554
555 #[test]
556 fn test_compute_discovery_hash() {
557 let transport_id = [0x42u8; 16];
558 let name = "TestInterface";
559 let hash = compute_discovery_hash(&transport_id, name);
560
561 let hash2 = compute_discovery_hash(&transport_id, name);
563 assert_eq!(hash, hash2);
564
565 let hash3 = compute_discovery_hash(&transport_id, "OtherInterface");
567 assert_ne!(hash, hash3);
568 }
569
570 #[test]
571 fn test_is_ip_address() {
572 assert!(is_ip_address("192.168.1.1"));
573 assert!(is_ip_address("::1"));
574 assert!(is_ip_address("2001:db8::1"));
575 assert!(!is_ip_address("not-an-ip"));
576 assert!(!is_ip_address("hostname.example.com"));
577 }
578
579 #[test]
580 fn test_is_hostname() {
581 assert!(is_hostname("example.com"));
582 assert!(is_hostname("sub.example.com"));
583 assert!(is_hostname("my-node"));
584 assert!(is_hostname("my-node.example.com"));
585 assert!(!is_hostname(""));
586 assert!(!is_hostname("-invalid"));
587 assert!(!is_hostname("invalid-"));
588 assert!(!is_hostname("a".repeat(300).as_str()));
589 }
590
591 #[test]
592 fn test_discovered_status() {
593 let now = time::now();
594
595 let mut iface = DiscoveredInterface {
596 interface_type: "TestInterface".into(),
597 transport: true,
598 name: "Test".into(),
599 discovered: now,
600 last_heard: now,
601 heard_count: 0,
602 status: DiscoveredStatus::Available,
603 stamp: vec![],
604 stamp_value: 14,
605 transport_id: [0u8; 16],
606 network_id: [0u8; 16],
607 hops: 0,
608 latitude: None,
609 longitude: None,
610 height: None,
611 reachable_on: None,
612 port: None,
613 frequency: None,
614 bandwidth: None,
615 spreading_factor: None,
616 coding_rate: None,
617 modulation: None,
618 channel: None,
619 ifac_netname: None,
620 ifac_netkey: None,
621 config_entry: None,
622 discovery_hash: [0u8; 32],
623 };
624
625 assert_eq!(iface.compute_status(), DiscoveredStatus::Available);
627
628 iface.last_heard = now - THRESHOLD_UNKNOWN - 3600.0;
630 assert_eq!(iface.compute_status(), DiscoveredStatus::Unknown);
631
632 iface.last_heard = now - THRESHOLD_STALE - 3600.0;
634 assert_eq!(iface.compute_status(), DiscoveredStatus::Stale);
635 }
636
637 #[test]
638 fn test_storage_roundtrip() {
639 use std::sync::atomic::{AtomicU64, Ordering};
640 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
641
642 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
643 let dir = std::env::temp_dir().join(format!("rns-discovery-test-{}-{}", std::process::id(), id));
644 let _ = fs::remove_dir_all(&dir);
645 fs::create_dir_all(&dir).unwrap();
646
647 let storage = DiscoveredInterfaceStorage::new(dir.clone());
648
649 let iface = DiscoveredInterface {
650 interface_type: "BackboneInterface".into(),
651 transport: true,
652 name: "TestNode".into(),
653 discovered: 1700000000.0,
654 last_heard: 1700001000.0,
655 heard_count: 5,
656 status: DiscoveredStatus::Available,
657 stamp: vec![0x42u8; 64],
658 stamp_value: 18,
659 transport_id: [0x01u8; 16],
660 network_id: [0x02u8; 16],
661 hops: 2,
662 latitude: Some(45.0),
663 longitude: Some(9.0),
664 height: Some(100.0),
665 reachable_on: Some("example.com".into()),
666 port: Some(4242),
667 frequency: None,
668 bandwidth: None,
669 spreading_factor: None,
670 coding_rate: None,
671 modulation: None,
672 channel: None,
673 ifac_netname: Some("mynetwork".into()),
674 ifac_netkey: Some("secretkey".into()),
675 config_entry: Some("test config".into()),
676 discovery_hash: compute_discovery_hash(&[0x01u8; 16], "TestNode"),
677 };
678
679 storage.store(&iface).unwrap();
681
682 let loaded = storage.load(&iface.discovery_hash).unwrap().unwrap();
684
685 assert_eq!(loaded.interface_type, iface.interface_type);
686 assert_eq!(loaded.name, iface.name);
687 assert_eq!(loaded.stamp_value, iface.stamp_value);
688 assert_eq!(loaded.transport_id, iface.transport_id);
689 assert_eq!(loaded.hops, iface.hops);
690 assert_eq!(loaded.latitude, iface.latitude);
691 assert_eq!(loaded.reachable_on, iface.reachable_on);
692 assert_eq!(loaded.port, iface.port);
693
694 let list = storage.list().unwrap();
696 assert_eq!(list.len(), 1);
697
698 storage.remove(&iface.discovery_hash).unwrap();
700 let list = storage.list().unwrap();
701 assert!(list.is_empty());
702
703 let _ = fs::remove_dir_all(&dir);
704 }
705
706 #[test]
707 fn test_filter_and_sort() {
708 let now = time::now();
709
710 let ifaces = vec![
711 DiscoveredInterface {
712 interface_type: "A".into(),
713 transport: true,
714 name: "high-value-stale".into(),
715 discovered: now,
716 last_heard: now - THRESHOLD_STALE - 100.0, heard_count: 0,
718 status: DiscoveredStatus::Stale,
719 stamp: vec![],
720 stamp_value: 20,
721 transport_id: [0u8; 16],
722 network_id: [0u8; 16],
723 hops: 0,
724 latitude: None,
725 longitude: None,
726 height: None,
727 reachable_on: None,
728 port: None,
729 frequency: None,
730 bandwidth: None,
731 spreading_factor: None,
732 coding_rate: None,
733 modulation: None,
734 channel: None,
735 ifac_netname: None,
736 ifac_netkey: None,
737 config_entry: None,
738 discovery_hash: [0u8; 32],
739 },
740 DiscoveredInterface {
741 interface_type: "B".into(),
742 transport: true,
743 name: "low-value-available".into(),
744 discovered: now,
745 last_heard: now - 10.0, heard_count: 0,
747 status: DiscoveredStatus::Available,
748 stamp: vec![],
749 stamp_value: 10,
750 transport_id: [0u8; 16],
751 network_id: [0u8; 16],
752 hops: 0,
753 latitude: None,
754 longitude: None,
755 height: None,
756 reachable_on: None,
757 port: None,
758 frequency: None,
759 bandwidth: None,
760 spreading_factor: None,
761 coding_rate: None,
762 modulation: None,
763 channel: None,
764 ifac_netname: None,
765 ifac_netkey: None,
766 config_entry: None,
767 discovery_hash: [1u8; 32],
768 },
769 DiscoveredInterface {
770 interface_type: "C".into(),
771 transport: false,
772 name: "high-value-available".into(),
773 discovered: now,
774 last_heard: now - 10.0, heard_count: 0,
776 status: DiscoveredStatus::Available,
777 stamp: vec![],
778 stamp_value: 20,
779 transport_id: [0u8; 16],
780 network_id: [0u8; 16],
781 hops: 0,
782 latitude: None,
783 longitude: None,
784 height: None,
785 reachable_on: None,
786 port: None,
787 frequency: None,
788 bandwidth: None,
789 spreading_factor: None,
790 coding_rate: None,
791 modulation: None,
792 channel: None,
793 ifac_netname: None,
794 ifac_netkey: None,
795 config_entry: None,
796 discovery_hash: [2u8; 32],
797 },
798 ];
799
800 let mut result = ifaces.clone();
802 filter_and_sort_interfaces(&mut result, false, false);
803 assert_eq!(result.len(), 3);
804 assert_eq!(result[0].name, "high-value-available");
806 assert_eq!(result[1].name, "low-value-available");
807 assert_eq!(result[2].name, "high-value-stale");
808
809 let mut result = ifaces.clone();
811 filter_and_sort_interfaces(&mut result, true, false);
812 assert_eq!(result.len(), 2); let mut result = ifaces.clone();
816 filter_and_sort_interfaces(&mut result, false, true);
817 assert_eq!(result.len(), 2); }
819
820 #[test]
821 fn test_discovery_name_hash_deterministic() {
822 let h1 = discovery_name_hash();
823 let h2 = discovery_name_hash();
824 assert_eq!(h1, h2);
825 assert_ne!(h1, [0u8; 10]); }
827}