1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use alloc::sync::Arc;
4use alloc::vec;
5use alloc::vec::Vec;
6
7#[cfg(feature = "std")]
8use crate::E2eEncryptionPolicy;
9#[cfg(feature = "std")]
10use crate::config::export_schema;
11use crate::router::encode_slice_le;
12use crate::{
13 DataEndpoint, DataType, MessageElement, TelemetryError, TelemetryResult,
14 config::{
15 OwnedDataTypeDefinition, OwnedEndpointDefinition, OwnedRuntimeSchemaSnapshot,
16 RuntimeSchemaSnapshot, e2e_encryption_policy_code, e2e_encryption_policy_from_code,
17 message_class_code, message_class_from_code, message_data_type_code,
18 message_data_type_from_code, reliable_code, reliable_from_code,
19 },
20 packet::Packet,
21};
22
23pub const DISCOVERY_ROUTE_TTL_MS: u64 = 30_000;
24pub const DISCOVERY_FAST_INTERVAL_MS: u64 = 250;
25pub const DISCOVERY_SLOW_INTERVAL_MS: u64 = 5_000;
26pub const DISCOVERY_SLOW_LINK_CAPACITY_BPS: u64 = 512;
27pub const DISCOVERY_SLOW_LINK_PING_INTERVAL_MS: u64 = 15_000;
28pub const DISCOVERY_SLOW_LINK_FULL_INTERVAL_MS: u64 = 120_000;
29pub const DISCOVERY_BASELINE_RETRY_MS: u64 = 5_000;
31pub const DISCOVERY_BASELINE_GRACE_MS: u64 = 1_000;
33pub const DISCOVERY_INCREMENTAL_RETRY_COUNT: u8 = 3;
38pub const TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS: u64 = 30_000;
39const DISCOVERY_TOPOLOGY_DELTA_MARKER: u32 = 0;
43
44fn bounded_wire_vec<T>(
51 payload: &[u8],
52 cursor: usize,
53 count: usize,
54 minimum_encoded_bytes: usize,
55 label: &'static str,
56) -> TelemetryResult<Vec<T>> {
57 let remaining = payload.len().saturating_sub(cursor);
58 if minimum_encoded_bytes == 0 || count > remaining / minimum_encoded_bytes {
59 return Err(TelemetryError::Unpack(label));
60 }
61 let mut values = Vec::new();
62 values
63 .try_reserve_exact(count)
64 .map_err(|_| TelemetryError::Unpack(label))?;
65 Ok(values)
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct DiscoveryCadenceState {
70 pub current_interval_ms: u64,
71 pub next_announce_ms: u64,
72}
73
74impl Default for DiscoveryCadenceState {
75 fn default() -> Self {
76 Self {
77 current_interval_ms: DISCOVERY_FAST_INTERVAL_MS,
78 next_announce_ms: 0,
79 }
80 }
81}
82
83impl DiscoveryCadenceState {
84 pub fn on_topology_change(&mut self, now_ms: u64) {
86 self.current_interval_ms = DISCOVERY_FAST_INTERVAL_MS;
87 self.next_announce_ms = now_ms;
88 }
89
90 pub fn on_announce_sent(&mut self, now_ms: u64) {
92 self.next_announce_ms = now_ms.saturating_add(self.current_interval_ms);
93 self.current_interval_ms = core::cmp::min(
94 self.current_interval_ms.saturating_mul(2),
95 DISCOVERY_SLOW_INTERVAL_MS,
96 );
97 }
98
99 pub fn due(&self, now_ms: u64) -> bool {
101 now_ms >= self.next_announce_ms
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct TopologyBoardNode {
107 pub sender_id: String,
108 pub reachable_endpoints: Vec<DataEndpoint>,
109 pub reachable_timesync_sources: Vec<String>,
110 pub connections: Vec<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct TopologyLink {
115 pub source: String,
116 pub target: String,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TopologyAnnouncerRoute {
121 pub sender_id: String,
122 pub reachable_endpoints: Vec<DataEndpoint>,
123 pub reachable_network_variables: Vec<DataType>,
124 pub reachable_timesync_sources: Vec<String>,
125 pub routers: Vec<TopologyBoardNode>,
126 pub last_seen_ms: u64,
127 pub age_ms: u64,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct TopologySideRoute {
132 pub side_id: usize,
133 pub side_name: String,
134 pub reachable_endpoints: Vec<DataEndpoint>,
135 pub reachable_network_variables: Vec<DataType>,
136 pub reachable_timesync_sources: Vec<String>,
137 pub announcers: Vec<TopologyAnnouncerRoute>,
138 pub last_seen_ms: u64,
139 pub age_ms: u64,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct TopologySnapshot {
144 pub advertised_endpoints: Vec<DataEndpoint>,
145 pub advertised_timesync_sources: Vec<String>,
146 pub routers: Vec<TopologyBoardNode>,
147 pub links: Vec<TopologyLink>,
148 pub routes: Vec<TopologySideRoute>,
149 pub current_announce_interval_ms: u64,
150 pub next_announce_ms: u64,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct ClientStatsSnapshot {
155 pub sender_id: String,
156 pub connected: bool,
157 pub side_ids: Vec<usize>,
158 pub side_names: Vec<String>,
159 pub last_seen_ms: Option<u64>,
160 pub age_ms: Option<u64>,
161 pub reachable_endpoints: Vec<DataEndpoint>,
162 pub reachable_timesync_sources: Vec<String>,
163 pub packets_sent: u64,
164 pub packets_received: u64,
165 pub bytes_sent: u64,
166 pub bytes_received: u64,
167}
168
169pub const LINK_CAPABILITY_HEADER_TEMPLATES: u32 = 0x0000_0001;
170pub const LINK_CAPABILITY_CHUNKING: u32 = 0x0000_0002;
171pub const LINK_CAPABILITY_RELIABILITY: u32 = 0x0000_0004;
172pub const LINK_CAPABILITY_CRYPTO: u32 = 0x0000_0008;
173pub const LINK_CAPABILITY_END_TO_END_RELIABILITY: u32 = 0x0000_0010;
174pub const LINK_CAPABILITY_OMIT_UNCHANGED_TIMESTAMPS: u32 = 0x0000_0020;
175
176pub const LINK_PROFILE_CANONICAL: u8 = 0;
177pub const LINK_PROFILE_TEMPLATE: u8 = 1;
178pub const LINK_PROFILE_IPV6_LIKE: u8 = 2;
179pub const LINK_PROFILE_IPV4_LIKE: u8 = 3;
180
181pub const ADDRESS_MODE_DYNAMIC: u8 = 0;
182pub const ADDRESS_MODE_REQUESTED: u8 = 1;
183pub const ADDRESS_MODE_STATIC: u8 = 2;
184pub const ADDRESS_STATE_REQUEST: u8 = 0;
185pub const ADDRESS_STATE_APPROVED: u8 = 1;
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub struct LinkCapabilities {
189 pub version: u8,
190 pub flags: u32,
191 pub profile: u8,
192 pub max_frame_bytes: u32,
193 pub compact_header_target_bytes: u32,
194 pub max_side_transport_templates: u32,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct AddressAdvertisement {
199 pub hostname: String,
200 pub address: u32,
201 pub requested_address: u32,
202 pub mode: u8,
203 pub state: u8,
204 pub birth_ms: u64,
205 pub owner_hash: u64,
206 pub reachable_endpoints: Vec<DataEndpoint>,
207 pub reachable_network_variables: Vec<DataType>,
211 pub reachable_timesync_sources: Vec<String>,
212 pub link_capabilities: LinkCapabilities,
213}
214
215#[inline]
216pub const fn is_router_control_endpoint(ep: DataEndpoint) -> bool {
217 matches!(
218 ep,
219 DataEndpoint::TelemetryError | DataEndpoint::TimeSync | DataEndpoint::Discovery
220 )
221}
222
223#[inline]
225pub const fn is_discovery_endpoint(ep: DataEndpoint) -> bool {
226 matches!(ep, DataEndpoint::Discovery)
227}
228
229#[inline]
231pub const fn is_discovery_type(ty: DataType) -> bool {
232 matches!(
233 ty,
234 DataType::DiscoveryAnnounce
235 | DataType::DiscoveryTimeSyncSources
236 | DataType::DiscoveryTopology
237 | DataType::DiscoverySchema
238 | DataType::DiscoveryTopologyRequest
239 | DataType::DiscoverySchemaRequest
240 | DataType::ManagedVariableRequest
241 | DataType::ManagedVariableValue
242 | DataType::DiscoveryLeave
243 | DataType::DiscoveryLinkCapabilities
244 | DataType::DiscoveryAddress
245 )
246}
247
248#[inline]
249pub const fn is_discovery_request_type(ty: DataType) -> bool {
250 matches!(
251 ty,
252 DataType::DiscoveryTopologyRequest
253 | DataType::DiscoverySchemaRequest
254 | DataType::ManagedVariableRequest
255 | DataType::DiscoveryLeave
256 | DataType::DiscoveryAddress
257 )
258}
259
260fn sort_dedup_strings(items: &mut Vec<String>) {
261 items.sort_unstable();
262 items.dedup();
263}
264
265pub fn normalize_topology_boards(boards: &mut Vec<TopologyBoardNode>) {
267 for board in boards.iter_mut() {
268 board
269 .reachable_endpoints
270 .retain(|ep| !is_router_control_endpoint(*ep));
271 crate::small_sort::sort(&mut board.reachable_endpoints);
272 board.reachable_endpoints.dedup();
273 sort_dedup_strings(&mut board.reachable_timesync_sources);
274 board.connections.retain(|peer| peer != &board.sender_id);
275 sort_dedup_strings(&mut board.connections);
276 }
277 boards.sort_unstable_by(|a, b| a.sender_id.cmp(&b.sender_id));
278 boards.dedup_by(|a, b| a.sender_id == b.sender_id);
279}
280
281pub fn topology_links_from_boards(boards: &[TopologyBoardNode]) -> Vec<TopologyLink> {
282 let mut links = Vec::new();
283 for board in boards {
284 for peer in &board.connections {
285 if peer == &board.sender_id {
286 continue;
287 }
288 let (source, target) = if board.sender_id <= *peer {
289 (board.sender_id.clone(), peer.clone())
290 } else {
291 (peer.clone(), board.sender_id.clone())
292 };
293 links.push(TopologyLink { source, target });
294 }
295 }
296 links.sort_unstable_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target)));
297 links.dedup_by(|a, b| a.source == b.source && a.target == b.target);
298 links
299}
300
301pub fn merge_topology_boards(dst: &mut Vec<TopologyBoardNode>, src: &[TopologyBoardNode]) {
303 let mut index = 0;
308 while index < dst.len() {
309 if dst[index + 1..]
310 .iter()
311 .any(|node| node.sender_id == dst[index].sender_id)
312 {
313 dst.remove(index);
314 } else {
315 index += 1;
316 }
317 }
318 for board in src {
319 if let Some(entry) = dst
320 .iter_mut()
321 .find(|node| node.sender_id == board.sender_id)
322 {
323 for endpoint in &board.reachable_endpoints {
324 if !entry.reachable_endpoints.contains(endpoint) {
325 entry.reachable_endpoints.push(*endpoint);
326 }
327 }
328 for source in &board.reachable_timesync_sources {
329 if !entry.reachable_timesync_sources.contains(source) {
330 entry.reachable_timesync_sources.push(source.clone());
331 }
332 }
333 for connection in &board.connections {
334 if !entry.connections.contains(connection) {
335 entry.connections.push(connection.clone());
336 }
337 }
338 } else {
339 dst.push(board.clone());
340 }
341 }
342 normalize_topology_boards(dst);
343}
344
345pub fn summarize_topology_boards(boards: &[TopologyBoardNode]) -> (Vec<DataEndpoint>, Vec<String>) {
347 let mut reachable_endpoints = Vec::new();
348 let mut reachable_timesync_sources = Vec::new();
349 for board in boards {
350 for endpoint in &board.reachable_endpoints {
351 if !is_router_control_endpoint(*endpoint) && !reachable_endpoints.contains(endpoint) {
352 reachable_endpoints.push(*endpoint);
353 }
354 }
355 for source in &board.reachable_timesync_sources {
356 if !reachable_timesync_sources.contains(source) {
357 reachable_timesync_sources.push(source.clone());
358 }
359 }
360 }
361 crate::small_sort::sort(&mut reachable_endpoints);
362 reachable_endpoints.dedup();
363 reachable_endpoints.retain(|ep| !is_router_control_endpoint(*ep));
364 sort_dedup_strings(&mut reachable_timesync_sources);
365 (reachable_endpoints, reachable_timesync_sources)
366}
367
368pub fn build_discovery_announce(
370 sender: &str,
371 timestamp_ms: u64,
372 endpoints: &[DataEndpoint],
373) -> TelemetryResult<Packet> {
374 let payload_words: Vec<u32> = endpoints.iter().copied().map(|ep| ep.as_u32()).collect();
375 Packet::new(
376 DataType::DiscoveryAnnounce,
377 &[DataEndpoint::Discovery],
378 sender,
379 timestamp_ms,
380 encode_slice_le(payload_words.as_slice()),
381 )
382}
383
384pub fn decode_discovery_announce(pkt: &Packet) -> TelemetryResult<Vec<DataEndpoint>> {
386 if pkt.data_type() != DataType::DiscoveryAnnounce {
387 return Err(TelemetryError::InvalidType);
388 }
389 decode_discovery_payload(pkt.payload())
390}
391
392pub fn decode_discovery_payload(payload: &[u8]) -> TelemetryResult<Vec<DataEndpoint>> {
394 if !payload.len().is_multiple_of(4) {
395 return Err(TelemetryError::Unpack("discovery payload width"));
396 }
397
398 let mut endpoints = Vec::with_capacity(payload.len() / 4);
399 for chunk in payload.as_chunks::<4>().0 {
400 let raw = u32::from_le_bytes(*chunk);
401 let ep = DataEndpoint(raw);
402 if is_discovery_endpoint(ep) {
403 continue;
404 }
405 endpoints.push(ep);
406 }
407 crate::small_sort::sort(&mut endpoints);
408 endpoints.dedup();
409 Ok(endpoints)
410}
411
412pub fn build_discovery_timesync_sources<S: AsRef<str>>(
414 sender: &str,
415 timestamp_ms: u64,
416 sources: &[S],
417) -> TelemetryResult<Packet> {
418 let mut payload = Vec::new();
419 let mut deduped: Vec<&str> = sources.iter().map(|s| s.as_ref()).collect();
420 crate::small_sort::sort(&mut deduped);
421 deduped.dedup();
422
423 payload.extend_from_slice(&(deduped.len() as u32).to_le_bytes());
424 for source in deduped {
425 let bytes = source.as_bytes();
426 let len = u32::try_from(bytes.len())
427 .map_err(|_| TelemetryError::Pack("discovery source id too long"))?;
428 payload.extend_from_slice(&len.to_le_bytes());
429 payload.extend_from_slice(bytes);
430 }
431
432 Packet::new(
433 DataType::DiscoveryTimeSyncSources,
434 &[DataEndpoint::Discovery],
435 sender,
436 timestamp_ms,
437 payload.into(),
438 )
439}
440
441pub fn build_discovery_topology_request(
442 sender: &str,
443 timestamp_ms: u64,
444) -> TelemetryResult<Packet> {
445 Packet::new(
446 DataType::DiscoveryTopologyRequest,
447 &[DataEndpoint::Discovery],
448 sender,
449 timestamp_ms,
450 Vec::<u8>::new().into(),
451 )
452}
453
454pub fn build_discovery_schema_request(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
455 Packet::new(
456 DataType::DiscoverySchemaRequest,
457 &[DataEndpoint::Discovery],
458 sender,
459 timestamp_ms,
460 Vec::<u8>::new().into(),
461 )
462}
463
464pub fn build_discovery_leave(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
465 Packet::new(
466 DataType::DiscoveryLeave,
467 &[DataEndpoint::Discovery],
468 sender,
469 timestamp_ms,
470 Vec::<u8>::new().into(),
471 )
472}
473
474pub fn build_discovery_link_capabilities(
475 sender: &str,
476 timestamp_ms: u64,
477 capabilities: LinkCapabilities,
478) -> TelemetryResult<Packet> {
479 let mut payload = Vec::with_capacity(18);
480 payload.push(capabilities.version);
481 payload.extend_from_slice(&capabilities.flags.to_le_bytes());
482 payload.push(capabilities.profile);
483 payload.extend_from_slice(&capabilities.max_frame_bytes.to_le_bytes());
484 payload.extend_from_slice(&capabilities.compact_header_target_bytes.to_le_bytes());
485 payload.extend_from_slice(&capabilities.max_side_transport_templates.to_le_bytes());
486 Packet::new(
487 DataType::DiscoveryLinkCapabilities,
488 &[DataEndpoint::Discovery],
489 sender,
490 timestamp_ms,
491 payload.into(),
492 )
493}
494
495pub fn decode_discovery_link_capabilities(pkt: &Packet) -> TelemetryResult<LinkCapabilities> {
496 if pkt.data_type() != DataType::DiscoveryLinkCapabilities {
497 return Err(TelemetryError::InvalidType);
498 }
499 let payload = pkt.payload();
500 if payload.len() != 18 {
501 return Err(TelemetryError::Unpack("discovery link capabilities width"));
502 }
503 Ok(LinkCapabilities {
504 version: payload[0],
505 flags: u32::from_le_bytes(payload[1..5].try_into().expect("4-byte flags")),
506 profile: payload[5],
507 max_frame_bytes: u32::from_le_bytes(payload[6..10].try_into().expect("4-byte max frame")),
508 compact_header_target_bytes: u32::from_le_bytes(
509 payload[10..14].try_into().expect("4-byte target"),
510 ),
511 max_side_transport_templates: u32::from_le_bytes(
512 payload[14..18].try_into().expect("4-byte templates"),
513 ),
514 })
515}
516
517pub fn build_discovery_address(
518 sender: &str,
519 timestamp_ms: u64,
520 ad: &AddressAdvertisement,
521) -> TelemetryResult<Packet> {
522 let mut payload = Vec::new();
523 payload.push(2);
524 payload.push(ad.mode);
525 payload.push(ad.state);
526 payload.extend_from_slice(&ad.address.to_le_bytes());
527 payload.extend_from_slice(&ad.requested_address.to_le_bytes());
528 payload.extend_from_slice(&ad.birth_ms.to_le_bytes());
529 payload.extend_from_slice(&ad.owner_hash.to_le_bytes());
530 encode_string(&mut payload, &ad.hostname)?;
531 let mut endpoints = ad.reachable_endpoints.clone();
532 endpoints.retain(|ep| !is_discovery_endpoint(*ep));
533 crate::small_sort::sort(&mut endpoints);
534 endpoints.dedup();
535 let endpoint_count = u32::try_from(endpoints.len())
536 .map_err(|_| TelemetryError::Pack("discovery address endpoint count"))?;
537 payload.extend_from_slice(&endpoint_count.to_le_bytes());
538 for ep in endpoints {
539 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
540 }
541 let mut network_variables = ad.reachable_network_variables.clone();
542 crate::small_sort::sort(&mut network_variables);
543 network_variables.dedup();
544 let network_variable_count = u32::try_from(network_variables.len())
545 .map_err(|_| TelemetryError::Pack("discovery address network variable count"))?;
546 payload.extend_from_slice(&network_variable_count.to_le_bytes());
547 for ty in network_variables {
548 payload.extend_from_slice(&ty.as_u32().to_le_bytes());
549 }
550 let mut sources = ad.reachable_timesync_sources.clone();
551 sort_dedup_strings(&mut sources);
552 let source_count = u32::try_from(sources.len())
553 .map_err(|_| TelemetryError::Pack("discovery address source count"))?;
554 payload.extend_from_slice(&source_count.to_le_bytes());
555 for source in sources {
556 encode_string(&mut payload, &source)?;
557 }
558 payload.push(ad.link_capabilities.version);
559 payload.extend_from_slice(&ad.link_capabilities.flags.to_le_bytes());
560 payload.push(ad.link_capabilities.profile);
561 payload.extend_from_slice(&ad.link_capabilities.max_frame_bytes.to_le_bytes());
562 payload.extend_from_slice(
563 &ad.link_capabilities
564 .compact_header_target_bytes
565 .to_le_bytes(),
566 );
567 payload.extend_from_slice(
568 &ad.link_capabilities
569 .max_side_transport_templates
570 .to_le_bytes(),
571 );
572 Packet::new(
573 DataType::DiscoveryAddress,
574 &[DataEndpoint::Discovery],
575 sender,
576 timestamp_ms,
577 payload.into(),
578 )
579}
580
581pub fn decode_discovery_address(pkt: &Packet) -> TelemetryResult<AddressAdvertisement> {
582 if pkt.data_type() != DataType::DiscoveryAddress {
583 return Err(TelemetryError::InvalidType);
584 }
585 let payload = pkt.payload();
586 let mut cursor = 0usize;
587 let version = read_u8(payload, &mut cursor, "discovery address version")?;
588 if version != 1 && version != 2 {
589 return Err(TelemetryError::Unpack("discovery address version"));
590 }
591 let mode = read_u8(payload, &mut cursor, "discovery address mode")?;
592 let state = read_u8(payload, &mut cursor, "discovery address state")?;
593 let address = read_u32(payload, &mut cursor, "discovery address current")?;
594 let requested_address = read_u32(payload, &mut cursor, "discovery address requested")?;
595 if payload.len().saturating_sub(cursor) < 8 {
596 return Err(TelemetryError::Unpack("discovery address birth"));
597 }
598 let birth_ms = u64::from_le_bytes(
599 payload[cursor..cursor + 8]
600 .try_into()
601 .expect("8-byte birth"),
602 );
603 cursor += 8;
604 if payload.len().saturating_sub(cursor) < 8 {
605 return Err(TelemetryError::Unpack("discovery address owner"));
606 }
607 let owner_hash = u64::from_le_bytes(
608 payload[cursor..cursor + 8]
609 .try_into()
610 .expect("8-byte owner"),
611 );
612 cursor += 8;
613 let hostname = decode_string(payload, &mut cursor, "discovery address hostname")?;
614 let endpoint_count =
615 read_u32(payload, &mut cursor, "discovery address endpoint count")? as usize;
616 let mut reachable_endpoints = bounded_wire_vec(
617 payload,
618 cursor,
619 endpoint_count,
620 4,
621 "discovery address endpoint count",
622 )?;
623 for _ in 0..endpoint_count {
624 let raw = read_u32(payload, &mut cursor, "discovery address endpoint")?;
625 let ep = DataEndpoint(raw);
626 if !is_discovery_endpoint(ep) {
627 reachable_endpoints.push(ep);
628 }
629 }
630 crate::small_sort::sort(&mut reachable_endpoints);
631 reachable_endpoints.dedup();
632 let mut reachable_network_variables = Vec::new();
633 if version >= 2 {
634 let count = read_u32(
635 payload,
636 &mut cursor,
637 "discovery address network variable count",
638 )? as usize;
639 reachable_network_variables = bounded_wire_vec(
640 payload,
641 cursor,
642 count,
643 4,
644 "discovery address network variable count",
645 )?;
646 for _ in 0..count {
647 let raw = read_u32(payload, &mut cursor, "discovery address network variable")?;
648 let ty = DataType(raw);
653 if !is_discovery_type(ty) {
654 reachable_network_variables.push(ty);
655 }
656 }
657 crate::small_sort::sort(&mut reachable_network_variables);
658 reachable_network_variables.dedup();
659 }
660 let source_count = read_u32(payload, &mut cursor, "discovery address source count")? as usize;
661 let mut reachable_timesync_sources = bounded_wire_vec(
662 payload,
663 cursor,
664 source_count,
665 4,
666 "discovery address source count",
667 )?;
668 for _ in 0..source_count {
669 let source = decode_string(payload, &mut cursor, "discovery address source")?;
670 if !source.is_empty() {
671 reachable_timesync_sources.push(source);
672 }
673 }
674 sort_dedup_strings(&mut reachable_timesync_sources);
675 let version = read_u8(payload, &mut cursor, "discovery address link version")?;
676 let flags = read_u32(payload, &mut cursor, "discovery address link flags")?;
677 let profile = read_u8(payload, &mut cursor, "discovery address link profile")?;
678 let max_frame_bytes = read_u32(payload, &mut cursor, "discovery address link max frame")?;
679 let compact_header_target_bytes = read_u32(
680 payload,
681 &mut cursor,
682 "discovery address link compact target",
683 )?;
684 let max_side_transport_templates =
685 read_u32(payload, &mut cursor, "discovery address link templates")?;
686 if cursor != payload.len() {
687 return Err(TelemetryError::Unpack("discovery address trailing bytes"));
688 }
689 if hostname.is_empty() || address == 0 {
690 return Err(TelemetryError::Unpack("bad discovery address"));
691 }
692 Ok(AddressAdvertisement {
693 hostname,
694 address,
695 requested_address,
696 mode,
697 state,
698 birth_ms,
699 owner_hash,
700 reachable_endpoints,
701 reachable_network_variables,
702 reachable_timesync_sources,
703 link_capabilities: LinkCapabilities {
704 version,
705 flags,
706 profile,
707 max_frame_bytes,
708 compact_header_target_bytes,
709 max_side_transport_templates,
710 },
711 })
712}
713
714pub fn build_managed_variable_request(
715 sender: &str,
716 timestamp_ms: u64,
717 ty: DataType,
718) -> TelemetryResult<Packet> {
719 Packet::new(
720 DataType::ManagedVariableRequest,
721 &[DataEndpoint::Discovery],
722 sender,
723 timestamp_ms,
724 encode_slice_le(&[ty.as_u32()]),
725 )
726}
727
728pub fn decode_managed_variable_request(pkt: &Packet) -> TelemetryResult<DataType> {
729 if pkt.data_type() != DataType::ManagedVariableRequest {
730 return Err(TelemetryError::InvalidType);
731 }
732 let payload = pkt.payload();
733 if payload.len() != 4 {
734 return Err(TelemetryError::Unpack("managed variable request width"));
735 }
736 let raw = u32::from_le_bytes(payload.try_into().expect("4-byte payload"));
737 Ok(DataType(raw))
740}
741
742pub fn decode_discovery_timesync_sources(pkt: &Packet) -> TelemetryResult<Vec<String>> {
744 if pkt.data_type() != DataType::DiscoveryTimeSyncSources {
745 return Err(TelemetryError::InvalidType);
746 }
747 decode_discovery_timesync_sources_payload(pkt.payload())
748}
749
750pub fn decode_discovery_timesync_sources_payload(payload: &[u8]) -> TelemetryResult<Vec<String>> {
752 if payload.len() < 4 {
753 return Err(TelemetryError::Unpack("discovery timesync source count"));
754 }
755
756 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
757 let mut cursor = 4usize;
758 let mut out = bounded_wire_vec(payload, cursor, count, 4, "discovery timesync source count")?;
759
760 for _ in 0..count {
761 if payload.len().saturating_sub(cursor) < 4 {
762 return Err(TelemetryError::Unpack("discovery timesync source len"));
763 }
764 let len = u32::from_le_bytes(payload[cursor..cursor + 4].try_into().expect("4-byte len"))
765 as usize;
766 cursor += 4;
767 if payload.len().saturating_sub(cursor) < len {
768 return Err(TelemetryError::Unpack("discovery timesync source bytes"));
769 }
770 let raw = &payload[cursor..cursor + len];
771 cursor += len;
772 let source = core::str::from_utf8(raw)
773 .map_err(|_| TelemetryError::Unpack("discovery timesync source utf8"))?;
774 if !source.is_empty() {
775 out.push(source.to_string());
776 }
777 }
778
779 if cursor != payload.len() {
780 return Err(TelemetryError::Unpack("discovery timesync trailing bytes"));
781 }
782
783 out.sort_unstable();
784 out.dedup();
785 Ok(out)
786}
787
788pub(crate) fn topology_payload_is_incremental(payload: &[u8]) -> bool {
789 payload.starts_with(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes())
790}
791
792pub(crate) fn apply_shared_topology_update(
793 retained: &mut Arc<Vec<TopologyBoardNode>>,
794 update: DiscoveryTopologyUpdate,
795) -> bool {
796 if !update.incremental {
797 if retained.as_slice() == update.boards {
798 return false;
799 }
800 *retained = Arc::new(update.boards);
801 true
802 } else {
803 apply_topology_update(Arc::make_mut(retained), update)
804 }
805}
806
807#[derive(Default)]
811pub(crate) struct CloneScratch {
812 pub bytes: usize,
813 pub largest: usize,
814}
815impl CloneScratch {
816 pub fn buffer(&mut self, bytes: usize) {
817 if bytes != 0 {
818 let owned = bytes.saturating_add(32);
819 self.bytes = self.bytes.saturating_add(owned);
820 self.largest = self.largest.max(owned);
821 }
822 }
823 pub fn strings(&mut self, strings: &[String]) {
824 self.buffer(strings.len().saturating_mul(core::mem::size_of::<String>()));
825 for value in strings {
826 self.buffer(value.len());
827 }
828 }
829 pub fn graph(&mut self, boards: &[TopologyBoardNode], new_nodes: usize) {
830 let node_size = core::mem::size_of::<TopologyBoardNode>();
831 self.buffer(boards.len().saturating_mul(node_size));
833 self.buffer(
834 boards
835 .len()
836 .saturating_add(new_nodes)
837 .saturating_mul(node_size)
838 .saturating_mul(2),
839 );
840 for board in boards {
841 self.buffer(board.sender_id.len());
842 self.buffer(board.reachable_endpoints.len().saturating_mul(4));
843 self.strings(&board.reachable_timesync_sources);
844 self.strings(&board.connections);
845 }
846 }
847}
848
849type TopologyScratchPeer<'a> = (
850 &'a str,
851 &'a [DataEndpoint],
852 &'a [String],
853 &'a [TopologyBoardNode],
854);
855
856pub(crate) fn topology_snapshot_scratch<'a>(
859 sender: &str,
860 local_endpoints: usize,
861 variables: usize,
862 graph_copies: usize,
863 include_wire: bool,
864 peers: impl Iterator<Item = TopologyScratchPeer<'a>> + Clone,
865) -> (usize, usize) {
866 topology_snapshot_scratch_impl(
867 sender,
868 local_endpoints,
869 variables,
870 graph_copies,
871 include_wire,
872 &|index| peers.clone().nth(index),
873 )
874}
875
876#[inline(never)]
877fn topology_snapshot_scratch_impl<'a>(
878 sender: &str,
879 local_endpoints: usize,
880 variables: usize,
881 graph_copies: usize,
882 include_wire: bool,
883 peer_at: &dyn Fn(usize) -> Option<TopologyScratchPeer<'a>>,
884) -> (usize, usize) {
885 let peers = core::iter::from_fn({
888 let mut index = 0usize;
889 move || {
890 let peer = peer_at(index);
891 index = index.saturating_add(1);
892 peer
893 }
894 });
895 #[derive(Clone, Copy)]
896 struct View<'a> {
897 name: &'a str,
898 endpoints: &'a [DataEndpoint],
899 sources: &'a [String],
900 connections: &'a [String],
901 backlink: bool,
902 }
903 let node_size = core::mem::size_of::<TopologyBoardNode>();
904 let string_size = core::mem::size_of::<String>();
905 let mut nodes = 1usize;
906 let mut graph = node_size
907 .saturating_add(sender.len().saturating_mul(2))
908 .saturating_add(local_endpoints.saturating_add(1).saturating_mul(4))
909 .saturating_add(string_size);
910 let mut wire = 24usize
911 .saturating_add(sender.len().saturating_mul(2))
912 .saturating_add(local_endpoints.saturating_add(1).saturating_mul(4));
913 for (index, (name, _, _, _)) in peers.clone().enumerate() {
914 if name != sender && !peers.clone().take(index).any(|(old, _, _, _)| old == name) {
915 graph = graph.saturating_add(string_size).saturating_add(name.len());
916 wire = wire.saturating_add(4).saturating_add(name.len());
917 }
918 }
919 let views = peers.flat_map(|(name, endpoints, sources, boards)| {
922 core::iter::once(View {
923 name,
924 endpoints,
925 sources,
926 connections: &[],
927 backlink: true,
928 })
929 .filter(move |_| boards.is_empty())
930 .chain(boards.iter().map(move |board| View {
931 name: &board.sender_id,
932 endpoints: &board.reachable_endpoints,
933 sources: &board.reachable_timesync_sources,
934 connections: &board.connections,
935 backlink: board.sender_id == name,
936 }))
937 });
938 for (index, node) in views.clone().enumerate() {
939 let prior = || {
940 views
941 .clone()
942 .take(index)
943 .filter(|old| old.name == node.name)
944 };
945 if node.name != sender && prior().next().is_none() {
946 nodes = nodes.saturating_add(1);
947 graph = graph
948 .saturating_add(node_size)
949 .saturating_add(node.name.len());
950 wire = wire.saturating_add(20).saturating_add(node.name.len());
951 }
952 for endpoint in node.endpoints {
953 if !prior().any(|old| old.endpoints.contains(endpoint)) {
954 graph = graph.saturating_add(4);
955 wire = wire.saturating_add(4);
956 }
957 }
958 for source in node.sources {
959 if !prior().any(|old| old.sources.contains(source)) {
960 graph = graph
961 .saturating_add(string_size)
962 .saturating_add(source.len());
963 wire = wire.saturating_add(4).saturating_add(source.len());
964 }
965 }
966 for connection in node.connections {
967 if connection != node.name
968 && !prior().any(|old| {
969 old.connections.contains(connection) || (old.backlink && connection == sender)
970 })
971 {
972 graph = graph
973 .saturating_add(string_size)
974 .saturating_add(connection.len());
975 wire = wire.saturating_add(4).saturating_add(connection.len());
976 }
977 }
978 if node.backlink
979 && node.name != sender
980 && !node.connections.iter().any(|value| value == sender)
981 && !prior()
982 .any(|old| old.backlink || old.connections.iter().any(|value| value == sender))
983 {
984 graph = graph
985 .saturating_add(string_size)
986 .saturating_add(sender.len());
987 wire = wire.saturating_add(4).saturating_add(sender.len());
988 }
989 }
990 let grow = |bytes: usize| bytes.checked_next_power_of_two().unwrap_or(usize::MAX);
991 let largest = 1024usize
992 .max(grow(nodes).saturating_mul(node_size))
993 .max(if include_wire { grow(wire) } else { 0 });
994 let additional = 2048usize
998 .saturating_add(graph.saturating_mul(graph_copies))
999 .saturating_add(variables.saturating_mul(32));
1000 (additional, largest)
1001}
1002
1003#[inline(never)]
1007fn topology_payload(
1008 boards: &[TopologyBoardNode],
1009 removed: Option<&[String]>,
1010) -> TelemetryResult<Vec<u8>> {
1011 let canonical = boards
1012 .windows(2)
1013 .all(|pair| pair[0].sender_id < pair[1].sender_id)
1014 && boards.iter().all(|board| {
1015 board
1016 .reachable_endpoints
1017 .iter()
1018 .all(|ep| !is_router_control_endpoint(*ep))
1019 && board
1020 .reachable_endpoints
1021 .windows(2)
1022 .all(|pair| pair[0] < pair[1])
1023 && board
1024 .reachable_timesync_sources
1025 .windows(2)
1026 .all(|pair| pair[0] < pair[1])
1027 && board
1028 .connections
1029 .iter()
1030 .all(|peer| peer != &board.sender_id)
1031 && board.connections.windows(2).all(|pair| pair[0] < pair[1])
1032 });
1033 let mut owned = Vec::new();
1034 let normalized = if canonical {
1035 boards
1036 } else {
1037 owned.extend_from_slice(boards);
1038 normalize_topology_boards(&mut owned);
1039 &owned
1040 };
1041 let board_count = normalized.len();
1042 let mut length = 4usize;
1043 let mut add = |bytes: usize| {
1044 length = length.saturating_add(bytes);
1045 };
1046 for board in normalized {
1047 add(16);
1048 add(board.sender_id.len());
1049 add(board.reachable_endpoints.len().saturating_mul(4));
1050 for value in board
1051 .reachable_timesync_sources
1052 .iter()
1053 .chain(&board.connections)
1054 {
1055 add(4);
1056 add(value.len());
1057 }
1058 }
1059 if let Some(removed) = removed {
1060 add(8);
1061 for value in removed {
1062 add(4);
1063 add(value.len());
1064 }
1065 }
1066 let mut payload = Vec::new();
1067 if canonical {
1068 payload
1070 .try_reserve_exact(length)
1071 .map_err(|_| TelemetryError::Io("memory pressure"))?;
1072 } else {
1073 owned.reverse();
1076 }
1077 if removed.is_some() {
1078 payload.extend_from_slice(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes());
1079 }
1080 payload.extend_from_slice(&(board_count as u32).to_le_bytes());
1081 for original in boards.iter().take(board_count) {
1082 let owned_board = owned.pop();
1083 let board = owned_board.as_ref().unwrap_or(original);
1084 encode_string(&mut payload, &board.sender_id)?;
1085
1086 payload.extend_from_slice(&(board.reachable_endpoints.len() as u32).to_le_bytes());
1087 for ep in &board.reachable_endpoints {
1088 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1089 }
1090
1091 payload.extend_from_slice(&(board.reachable_timesync_sources.len() as u32).to_le_bytes());
1092 for source in &board.reachable_timesync_sources {
1093 encode_string(&mut payload, source)?;
1094 }
1095
1096 payload.extend_from_slice(&(board.connections.len() as u32).to_le_bytes());
1097 for peer in &board.connections {
1098 encode_string(&mut payload, peer)?;
1099 }
1100 }
1101
1102 if let Some(removed) = removed {
1103 payload.extend_from_slice(&(removed.len() as u32).to_le_bytes());
1104 for sender_id in removed {
1105 encode_string(&mut payload, sender_id)?;
1106 }
1107 }
1108 debug_assert_eq!(payload.len(), length);
1109 Ok(payload)
1110}
1111
1112pub fn build_discovery_topology(
1114 sender: &str,
1115 timestamp_ms: u64,
1116 boards: &[TopologyBoardNode],
1117) -> TelemetryResult<Packet> {
1118 let payload = topology_payload(boards, None)?;
1119
1120 Packet::new(
1121 DataType::DiscoveryTopology,
1122 &[DataEndpoint::Discovery],
1123 sender,
1124 timestamp_ms,
1125 payload.into(),
1126 )
1127}
1128
1129#[derive(Debug, Clone, PartialEq, Eq)]
1132pub struct DiscoveryTopologyUpdate {
1133 pub incremental: bool,
1134 pub boards: Vec<TopologyBoardNode>,
1135 pub removed: Vec<String>,
1136}
1137
1138pub fn apply_topology_update(
1141 retained: &mut Vec<TopologyBoardNode>,
1142 update: DiscoveryTopologyUpdate,
1143) -> bool {
1144 if !update.incremental {
1145 if *retained == update.boards {
1146 return false;
1147 }
1148 *retained = update.boards;
1149 return true;
1150 }
1151 let old_len = retained.len();
1152 retained.retain(|board| {
1153 !update.removed.iter().any(|id| id == &board.sender_id)
1154 || update
1155 .boards
1156 .iter()
1157 .any(|replacement| replacement.sender_id == board.sender_id)
1158 });
1159 let mut changed = old_len != retained.len();
1160 for board in update.boards {
1161 if let Some(old) = retained
1162 .iter_mut()
1163 .find(|old| old.sender_id == board.sender_id)
1164 {
1165 if *old != board {
1166 *old = board;
1167 changed = true;
1168 }
1169 } else {
1170 retained.push(board);
1171 changed = true;
1172 }
1173 }
1174 if changed {
1175 normalize_topology_boards(retained);
1176 }
1177 changed
1178}
1179
1180pub fn topology_delta(
1182 previous: &[TopologyBoardNode],
1183 current: &[TopologyBoardNode],
1184) -> (Vec<TopologyBoardNode>, Vec<String>) {
1185 let boards = current
1186 .iter()
1187 .filter(|node| previous.iter().find(|old| old.sender_id == node.sender_id) != Some(*node))
1188 .cloned()
1189 .collect();
1190 let removed = previous
1191 .iter()
1192 .filter(|old| !current.iter().any(|node| node.sender_id == old.sender_id))
1193 .map(|old| old.sender_id.clone())
1194 .collect();
1195 (boards, removed)
1196}
1197
1198pub fn build_discovery_topology_delta(
1200 sender: &str,
1201 timestamp_ms: u64,
1202 boards: &[TopologyBoardNode],
1203 removed: &[String],
1204) -> TelemetryResult<Packet> {
1205 let payload = topology_payload(boards, Some(removed))?;
1206 Packet::new(
1207 DataType::DiscoveryTopology,
1208 &[DataEndpoint::Discovery],
1209 sender,
1210 timestamp_ms,
1211 payload.into(),
1212 )
1213}
1214
1215fn decode_string(
1216 payload: &[u8],
1217 cursor: &mut usize,
1218 label: &'static str,
1219) -> TelemetryResult<String> {
1220 if payload.len().saturating_sub(*cursor) < 4 {
1221 return Err(TelemetryError::Unpack(label));
1222 }
1223 let len = u32::from_le_bytes(
1224 payload[*cursor..*cursor + 4]
1225 .try_into()
1226 .expect("4-byte len"),
1227 ) as usize;
1228 *cursor += 4;
1229 if payload.len().saturating_sub(*cursor) < len {
1230 return Err(TelemetryError::Unpack(label));
1231 }
1232 let raw = &payload[*cursor..*cursor + len];
1233 *cursor += len;
1234 core::str::from_utf8(raw)
1235 .map(|s| s.to_string())
1236 .map_err(|_| TelemetryError::Unpack(label))
1237}
1238
1239pub fn decode_discovery_topology(pkt: &Packet) -> TelemetryResult<Vec<TopologyBoardNode>> {
1241 if pkt.data_type() != DataType::DiscoveryTopology {
1242 return Err(TelemetryError::InvalidType);
1243 }
1244 Ok(decode_discovery_topology_update(pkt)?.boards)
1245}
1246
1247pub(crate) fn topology_payload_matches_retained(
1252 payload: &[u8],
1253 retained: &[TopologyBoardNode],
1254) -> Option<bool> {
1255 fn word(input: &mut &[u8]) -> Option<u32> {
1256 let (head, tail) = input.split_at_checked(4)?;
1257 *input = tail;
1258 Some(u32::from_le_bytes(head.try_into().ok()?))
1259 }
1260 fn text<'a>(input: &mut &'a [u8]) -> Option<&'a str> {
1261 let count = word(input)? as usize;
1262 let (head, tail) = input.split_at_checked(count)?;
1263 *input = tail;
1264 core::str::from_utf8(head).ok()
1265 }
1266 let mut input = payload;
1267 let first = word(&mut input)?;
1268 let full = first != DISCOVERY_TOPOLOGY_DELTA_MARKER;
1269 let count = if full { first } else { word(&mut input)? } as usize;
1270 if (full && count != retained.len()) || count > input.len() / 16 {
1271 return None;
1272 }
1273 let mut previous = None;
1274 for _ in 0..count {
1275 let name = text(&mut input)?;
1276 if previous.is_some_and(|previous| previous >= name) {
1278 return None;
1279 }
1280 previous = Some(name);
1281 let board = retained.iter().find(|board| board.sender_id == name)?;
1282 if word(&mut input)? as usize != board.reachable_endpoints.len() {
1283 return None;
1284 }
1285 for endpoint in &board.reachable_endpoints {
1286 if word(&mut input)? != endpoint.as_u32() {
1287 return None;
1288 }
1289 }
1290 for values in [&board.reachable_timesync_sources, &board.connections] {
1291 if word(&mut input)? as usize != values.len() {
1292 return None;
1293 }
1294 for value in values {
1295 if text(&mut input)? != value {
1296 return None;
1297 }
1298 }
1299 }
1300 }
1301 if !full {
1302 let removed = word(&mut input)? as usize;
1303 if removed > input.len() / 4 {
1304 return None;
1305 }
1306 for _ in 0..removed {
1307 let name = text(&mut input)?;
1308 if retained.iter().any(|board| board.sender_id == name) {
1309 return None;
1310 }
1311 }
1312 }
1313 input.is_empty().then_some(full)
1314}
1315
1316pub fn decode_discovery_topology_update(pkt: &Packet) -> TelemetryResult<DiscoveryTopologyUpdate> {
1318 if pkt.data_type() != DataType::DiscoveryTopology {
1319 return Err(TelemetryError::InvalidType);
1320 }
1321 let payload = pkt.payload();
1322 if payload.len() < 4 {
1323 return Err(TelemetryError::Unpack("discovery topology board count"));
1324 }
1325 let first = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count"));
1326 if first != DISCOVERY_TOPOLOGY_DELTA_MARKER {
1327 return Ok(DiscoveryTopologyUpdate {
1328 incremental: false,
1329 boards: decode_discovery_topology_payload(payload)?,
1330 removed: Vec::new(),
1331 });
1332 }
1333 if payload.len() < 8 {
1334 return Err(TelemetryError::Unpack("discovery topology delta count"));
1335 }
1336 let count = u32::from_le_bytes(payload[4..8].try_into().expect("4-byte count")) as usize;
1337 let mut cursor = 8usize;
1338 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1339 if payload.len().saturating_sub(cursor) < 4 {
1340 return Err(TelemetryError::Unpack("discovery topology removal count"));
1341 }
1342 let removed_count = u32::from_le_bytes(
1343 payload[cursor..cursor + 4]
1344 .try_into()
1345 .expect("4-byte count"),
1346 ) as usize;
1347 cursor += 4;
1348 let mut removed = bounded_wire_vec(
1349 payload,
1350 cursor,
1351 removed_count,
1352 4,
1353 "discovery topology removal count",
1354 )?;
1355 for _ in 0..removed_count {
1356 removed.push(decode_string(
1357 payload,
1358 &mut cursor,
1359 "discovery topology removed sender id",
1360 )?);
1361 }
1362 if cursor != payload.len() {
1363 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1364 }
1365 normalize_topology_boards(&mut boards);
1366 removed.sort_unstable();
1367 removed.dedup();
1368 Ok(DiscoveryTopologyUpdate {
1369 incremental: true,
1370 boards,
1371 removed,
1372 })
1373}
1374
1375pub fn decode_discovery_topology_payload(
1377 payload: &[u8],
1378) -> TelemetryResult<Vec<TopologyBoardNode>> {
1379 if payload.len() < 4 {
1380 return Err(TelemetryError::Unpack("discovery topology board count"));
1381 }
1382
1383 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
1384 let mut cursor = 4usize;
1385 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1386
1387 if cursor != payload.len() {
1388 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1389 }
1390
1391 normalize_topology_boards(&mut boards);
1392 Ok(boards)
1393}
1394
1395fn decode_topology_boards(
1396 payload: &[u8],
1397 cursor: &mut usize,
1398 count: usize,
1399) -> TelemetryResult<Vec<TopologyBoardNode>> {
1400 let mut boards = bounded_wire_vec(
1402 payload,
1403 *cursor,
1404 count,
1405 16,
1406 "discovery topology board count",
1407 )?;
1408
1409 for _ in 0..count {
1410 let sender_id = decode_string(payload, cursor, "discovery topology sender id")?;
1411
1412 if payload.len().saturating_sub(*cursor) < 4 {
1413 return Err(TelemetryError::Unpack("discovery topology endpoint count"));
1414 }
1415 let endpoint_count = u32::from_le_bytes(
1416 payload[*cursor..*cursor + 4]
1417 .try_into()
1418 .expect("4-byte count"),
1419 ) as usize;
1420 *cursor += 4;
1421 let mut reachable_endpoints = bounded_wire_vec(
1422 payload,
1423 *cursor,
1424 endpoint_count,
1425 4,
1426 "discovery topology endpoint count",
1427 )?;
1428 for _ in 0..endpoint_count {
1429 if payload.len().saturating_sub(*cursor) < 4 {
1430 return Err(TelemetryError::Unpack("discovery topology endpoint"));
1431 }
1432 let raw =
1433 u32::from_le_bytes(payload[*cursor..*cursor + 4].try_into().expect("4-byte ep"));
1434 *cursor += 4;
1435 let ep = DataEndpoint(raw);
1436 if !is_discovery_endpoint(ep) {
1437 reachable_endpoints.push(ep);
1438 }
1439 }
1440
1441 if payload.len().saturating_sub(*cursor) < 4 {
1442 return Err(TelemetryError::Unpack(
1443 "discovery topology timesync source count",
1444 ));
1445 }
1446 let source_count = u32::from_le_bytes(
1447 payload[*cursor..*cursor + 4]
1448 .try_into()
1449 .expect("4-byte count"),
1450 ) as usize;
1451 *cursor += 4;
1452 let mut reachable_timesync_sources = bounded_wire_vec(
1453 payload,
1454 *cursor,
1455 source_count,
1456 4,
1457 "discovery topology timesync source count",
1458 )?;
1459 for _ in 0..source_count {
1460 let source = decode_string(payload, cursor, "discovery topology timesync source")?;
1461 if !source.is_empty() {
1462 reachable_timesync_sources.push(source);
1463 }
1464 }
1465
1466 if payload.len().saturating_sub(*cursor) < 4 {
1467 return Err(TelemetryError::Unpack(
1468 "discovery topology connection count",
1469 ));
1470 }
1471 let connection_count = u32::from_le_bytes(
1472 payload[*cursor..*cursor + 4]
1473 .try_into()
1474 .expect("4-byte count"),
1475 ) as usize;
1476 *cursor += 4;
1477 let mut connections = bounded_wire_vec(
1478 payload,
1479 *cursor,
1480 connection_count,
1481 4,
1482 "discovery topology connection count",
1483 )?;
1484 for _ in 0..connection_count {
1485 let peer = decode_string(payload, cursor, "discovery topology connection")?;
1486 if !peer.is_empty() {
1487 connections.push(peer);
1488 }
1489 }
1490
1491 boards.push(TopologyBoardNode {
1492 sender_id,
1493 reachable_endpoints,
1494 reachable_timesync_sources,
1495 connections,
1496 });
1497 }
1498
1499 Ok(boards)
1500}
1501
1502fn encode_string(payload: &mut Vec<u8>, value: &str) -> TelemetryResult<()> {
1503 let len = u32::try_from(value.len())
1504 .map_err(|_| TelemetryError::Pack("discovery schema string too long"))?;
1505 payload.extend_from_slice(&len.to_le_bytes());
1506 payload.extend_from_slice(value.as_bytes());
1507 Ok(())
1508}
1509
1510fn read_u8(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u8> {
1511 if payload.len().saturating_sub(*cursor) < 1 {
1512 return Err(TelemetryError::Unpack(label));
1513 }
1514 let out = payload[*cursor];
1515 *cursor += 1;
1516 Ok(out)
1517}
1518
1519fn read_u32(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u32> {
1520 if payload.len().saturating_sub(*cursor) < 4 {
1521 return Err(TelemetryError::Unpack(label));
1522 }
1523 let out = u32::from_le_bytes(
1524 payload[*cursor..*cursor + 4]
1525 .try_into()
1526 .expect("4-byte u32"),
1527 );
1528 *cursor += 4;
1529 Ok(out)
1530}
1531
1532#[cfg(any(not(feature = "std"), test))]
1533fn skip_string(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<()> {
1534 let len = read_u32(payload, cursor, label)? as usize;
1535 if payload.len().saturating_sub(*cursor) < len {
1536 return Err(TelemetryError::Unpack(label));
1537 }
1538 *cursor += len;
1539 Ok(())
1540}
1541
1542#[cfg(any(not(feature = "std"), test))]
1543fn borrowed_schema_string<'a>(
1544 payload: &'a [u8],
1545 cursor: &mut usize,
1546 label: &'static str,
1547) -> TelemetryResult<&'a str> {
1548 let len = read_u32(payload, cursor, label)? as usize;
1549 let end = cursor
1550 .checked_add(len)
1551 .ok_or(TelemetryError::Unpack(label))?;
1552 let bytes = payload
1553 .get(*cursor..end)
1554 .ok_or(TelemetryError::Unpack(label))?;
1555 *cursor = end;
1556 core::str::from_utf8(bytes).map_err(|_| TelemetryError::Unpack(label))
1557}
1558
1559#[cfg(any(not(feature = "std"), test))]
1564pub(crate) fn discovery_schema_payload_is_fully_known(payload: &[u8]) -> TelemetryResult<bool> {
1565 let mut cursor = 0usize;
1566 if read_u32(payload, &mut cursor, "discovery schema version")? != 3 {
1567 return Ok(false);
1568 }
1569
1570 let endpoint_count =
1571 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1572 if endpoint_count > payload.len().saturating_sub(cursor) / 13 {
1573 return Err(TelemetryError::Unpack("discovery schema endpoint count"));
1574 }
1575 let mut all_known = true;
1576 for _ in 0..endpoint_count {
1577 let id = DataEndpoint(read_u32(
1578 payload,
1579 &mut cursor,
1580 "discovery schema endpoint id",
1581 )?);
1582 let known = crate::config::endpoint_exists(id);
1583 let meta = crate::config::get_endpoint_meta(id);
1584 let link_local_only =
1585 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1586 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema endpoint name")?;
1587 all_known &= known
1588 && AsRef::<str>::as_ref(&meta.name) == name
1589 && (!link_local_only || meta.link_local_only);
1590 skip_string(
1591 payload,
1592 &mut cursor,
1593 "discovery schema endpoint description",
1594 )?;
1595 }
1596
1597 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1598 if type_count > payload.len().saturating_sub(cursor) / 19 {
1599 return Err(TelemetryError::Unpack("discovery schema type count"));
1600 }
1601 for _ in 0..type_count {
1602 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1603 let meta = crate::config::try_get_message_meta(id);
1604 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema type name")?;
1605 skip_string(payload, &mut cursor, "discovery schema type description")?;
1606 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1607 if element_kind > 1 {
1608 return Err(TelemetryError::Unpack("discovery schema element kind"));
1609 }
1610 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1611 let data_type = message_data_type_from_code(read_u8(
1612 payload,
1613 &mut cursor,
1614 "discovery schema data type",
1615 )?)
1616 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1617 let class =
1618 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1619 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1620 let reliable =
1621 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1622 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1623 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1624 let encryption = e2e_encryption_policy_from_code(read_u8(
1625 payload,
1626 &mut cursor,
1627 "discovery schema e2e cryptography",
1628 )?)
1629 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1630 let endpoint_count =
1631 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1632 if endpoint_count > payload.len().saturating_sub(cursor) / 4 {
1633 return Err(TelemetryError::Unpack(
1634 "discovery schema type endpoint count",
1635 ));
1636 }
1637 let element = if element_kind == 0 {
1638 MessageElement::Static(count, data_type, class)
1639 } else {
1640 MessageElement::Dynamic(data_type, class)
1641 };
1642 all_known &= meta.as_ref().is_some_and(|meta| {
1643 AsRef::<str>::as_ref(&meta.name) == name
1644 && meta.element == element
1645 && meta.reliable == reliable
1646 && meta.priority >= priority
1647 && meta.e2e_encryption == encryption
1648 });
1649 for _ in 0..endpoint_count {
1650 let ep = DataEndpoint(read_u32(
1651 payload,
1652 &mut cursor,
1653 "discovery schema type endpoint",
1654 )?);
1655 all_known &= meta
1656 .as_ref()
1657 .is_some_and(|meta| meta.endpoints.contains(&ep));
1658 }
1659 }
1660 if cursor != payload.len() {
1661 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1662 }
1663 Ok(all_known)
1664}
1665
1666pub fn build_discovery_schema(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
1668 #[cfg(feature = "std")]
1669 return build_discovery_schema_from_owned_snapshot(sender, timestamp_ms, export_schema());
1670 #[cfg(not(feature = "std"))]
1671 return Packet::new(
1672 DataType::DiscoverySchema,
1673 &[DataEndpoint::Discovery],
1674 sender,
1675 timestamp_ms,
1676 crate::config::encode_embedded_schema_payload()?,
1677 );
1678}
1679
1680pub fn elect_discovery_master(local_sender: &str, boards: &[TopologyBoardNode]) -> String {
1688 let mut nodes: BTreeMap<String, Vec<String>> = BTreeMap::new();
1689 for board in boards {
1690 nodes.entry(board.sender_id.clone()).or_default();
1691 for peer in board.connections.iter() {
1692 nodes
1693 .entry(peer.clone())
1694 .or_default()
1695 .push(board.sender_id.clone());
1696 nodes
1697 .entry(board.sender_id.clone())
1698 .or_default()
1699 .push(peer.clone());
1700 }
1701 }
1702 nodes.entry(local_sender.to_string()).or_default();
1703 for peers in nodes.values_mut() {
1704 peers.sort_unstable();
1705 peers.dedup();
1706 }
1707
1708 let mut best_sender = local_sender.to_string();
1709 let mut best_unreachable = usize::MAX;
1710 let mut best_max_hops = usize::MAX;
1711 let mut best_total_hops = usize::MAX;
1712
1713 for sender in nodes.keys() {
1714 let mut frontier = vec![sender.clone()];
1715 let mut seen: BTreeMap<String, usize> = BTreeMap::new();
1716 seen.insert(sender.clone(), 0);
1717 let mut idx = 0;
1718 while idx < frontier.len() {
1719 let cur = frontier[idx].clone();
1720 idx += 1;
1721 let cur_dist = seen[&cur];
1722 if let Some(peers) = nodes.get(&cur) {
1723 for peer in peers {
1724 if !seen.contains_key(peer) {
1725 seen.insert(peer.clone(), cur_dist + 1);
1726 frontier.push(peer.clone());
1727 }
1728 }
1729 }
1730 }
1731
1732 let unreachable = nodes.len().saturating_sub(seen.len());
1733 let max_hops = seen.values().copied().max().unwrap_or(0);
1734 let total_hops = seen.values().copied().sum();
1735 let better = unreachable < best_unreachable
1736 || (unreachable == best_unreachable && max_hops < best_max_hops)
1737 || (unreachable == best_unreachable
1738 && max_hops == best_max_hops
1739 && total_hops < best_total_hops)
1740 || (unreachable == best_unreachable
1741 && max_hops == best_max_hops
1742 && total_hops == best_total_hops
1743 && sender < &best_sender);
1744 if better {
1745 best_sender = sender.clone();
1746 best_unreachable = unreachable;
1747 best_max_hops = max_hops;
1748 best_total_hops = total_hops;
1749 }
1750 }
1751
1752 best_sender
1753}
1754
1755pub fn build_discovery_schema_from_snapshot(
1757 sender: &str,
1758 timestamp_ms: u64,
1759 schema: RuntimeSchemaSnapshot,
1760) -> TelemetryResult<Packet> {
1761 let mut payload = Vec::new();
1762 payload.extend_from_slice(&3u32.to_le_bytes());
1763
1764 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1765 for ep in schema.endpoints {
1766 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1767 payload.push(ep.link_local_only as u8);
1768 encode_string(&mut payload, ep.name)?;
1769 #[cfg(feature = "std")]
1770 encode_string(&mut payload, ep.description)?;
1771 #[cfg(not(feature = "std"))]
1772 encode_string(&mut payload, "")?;
1773 }
1774
1775 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1776 for ty in schema.types {
1777 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1778 encode_string(&mut payload, ty.name)?;
1779 #[cfg(feature = "std")]
1780 encode_string(&mut payload, ty.description)?;
1781 #[cfg(not(feature = "std"))]
1782 encode_string(&mut payload, "")?;
1783 match ty.element {
1784 MessageElement::Static(count, data_type, class) => {
1785 payload.push(0);
1786 payload.extend_from_slice(&(count as u32).to_le_bytes());
1787 payload.push(message_data_type_code(data_type));
1788 payload.push(message_class_code(class));
1789 }
1790 MessageElement::Dynamic(data_type, class) => {
1791 payload.push(1);
1792 payload.extend_from_slice(&0u32.to_le_bytes());
1793 payload.push(message_data_type_code(data_type));
1794 payload.push(message_class_code(class));
1795 }
1796 }
1797 payload.push(reliable_code(ty.reliable));
1798 payload.push(ty.priority);
1799 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1800 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1801 for ep in ty.endpoints {
1802 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1803 }
1804 }
1805
1806 Packet::new(
1807 DataType::DiscoverySchema,
1808 &[DataEndpoint::Discovery],
1809 sender,
1810 timestamp_ms,
1811 payload.into(),
1812 )
1813}
1814
1815pub fn build_discovery_schema_from_owned_snapshot(
1817 sender: &str,
1818 timestamp_ms: u64,
1819 schema: OwnedRuntimeSchemaSnapshot,
1820) -> TelemetryResult<Packet> {
1821 let mut payload = Vec::new();
1822 payload.extend_from_slice(&3u32.to_le_bytes());
1823
1824 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1825 for ep in schema.endpoints {
1826 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1827 payload.push(ep.link_local_only as u8);
1828 encode_string(&mut payload, &ep.name)?;
1829 encode_string(&mut payload, "")?;
1833 }
1834
1835 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1836 for ty in schema.types {
1837 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1838 encode_string(&mut payload, &ty.name)?;
1839 encode_string(&mut payload, "")?;
1840 match ty.element {
1841 MessageElement::Static(count, data_type, class) => {
1842 payload.push(0);
1843 payload.extend_from_slice(&(count as u32).to_le_bytes());
1844 payload.push(message_data_type_code(data_type));
1845 payload.push(message_class_code(class));
1846 }
1847 MessageElement::Dynamic(data_type, class) => {
1848 payload.push(1);
1849 payload.extend_from_slice(&0u32.to_le_bytes());
1850 payload.push(message_data_type_code(data_type));
1851 payload.push(message_class_code(class));
1852 }
1853 }
1854 payload.push(reliable_code(ty.reliable));
1855 payload.push(ty.priority);
1856 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1857 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1858 for ep in ty.endpoints {
1859 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1860 }
1861 }
1862
1863 Packet::new(
1864 DataType::DiscoverySchema,
1865 &[DataEndpoint::Discovery],
1866 sender,
1867 timestamp_ms,
1868 payload.into(),
1869 )
1870}
1871
1872pub fn decode_discovery_schema(pkt: &Packet) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1874 if pkt.data_type() != DataType::DiscoverySchema {
1875 return Err(TelemetryError::InvalidType);
1876 }
1877 decode_discovery_schema_payload(pkt.payload())
1878}
1879
1880pub fn decode_discovery_schema_payload(
1882 payload: &[u8],
1883) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1884 let mut cursor = 0usize;
1885 let version = read_u32(payload, &mut cursor, "discovery schema version")?;
1886 #[cfg(feature = "std")]
1887 if version != 1 && version != 2 && version != 3 {
1888 return Err(TelemetryError::Unpack("discovery schema version"));
1889 }
1890 #[cfg(not(feature = "std"))]
1891 if version != 3 {
1892 return Err(TelemetryError::Unpack("discovery schema version"));
1893 }
1894
1895 let endpoint_count =
1896 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1897 let mut endpoints = bounded_wire_vec(
1898 payload,
1899 cursor,
1900 endpoint_count,
1901 13,
1902 "discovery schema endpoint count",
1903 )?;
1904 for _ in 0..endpoint_count {
1905 let id = DataEndpoint(read_u32(
1906 payload,
1907 &mut cursor,
1908 "discovery schema endpoint id",
1909 )?);
1910 let link_local_only =
1911 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1912 let name = decode_string(payload, &mut cursor, "discovery schema endpoint name")?;
1913 #[cfg(feature = "std")]
1914 let description = if version >= 2 {
1915 decode_string(
1916 payload,
1917 &mut cursor,
1918 "discovery schema endpoint description",
1919 )?
1920 } else {
1921 String::new()
1922 };
1923 #[cfg(not(feature = "std"))]
1924 let description = {
1925 skip_string(
1926 payload,
1927 &mut cursor,
1928 "discovery schema endpoint description",
1929 )?;
1930 String::new()
1931 };
1932 endpoints.push(OwnedEndpointDefinition {
1933 id,
1934 name,
1935 description,
1936 link_local_only,
1937 });
1938 }
1939
1940 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1941 let mut types = bounded_wire_vec(
1942 payload,
1943 cursor,
1944 type_count,
1945 19,
1946 "discovery schema type count",
1947 )?;
1948 for _ in 0..type_count {
1949 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1950 let name = decode_string(payload, &mut cursor, "discovery schema type name")?;
1951 #[cfg(feature = "std")]
1952 let description = if version >= 2 {
1953 decode_string(payload, &mut cursor, "discovery schema type description")?
1954 } else {
1955 String::new()
1956 };
1957 #[cfg(not(feature = "std"))]
1958 let description = {
1959 skip_string(payload, &mut cursor, "discovery schema type description")?;
1960 String::new()
1961 };
1962 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1963 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1964 let data_type = message_data_type_from_code(read_u8(
1965 payload,
1966 &mut cursor,
1967 "discovery schema data type",
1968 )?)
1969 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1970 let class =
1971 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1972 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1973 let element = match element_kind {
1974 0 => MessageElement::Static(count, data_type, class),
1975 1 => MessageElement::Dynamic(data_type, class),
1976 _ => return Err(TelemetryError::Unpack("discovery schema element kind")),
1977 };
1978 let reliable =
1979 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1980 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1981 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1982 #[cfg(feature = "std")]
1983 let e2e_encryption = if version >= 3 {
1984 e2e_encryption_policy_from_code(read_u8(
1985 payload,
1986 &mut cursor,
1987 "discovery schema e2e cryptography",
1988 )?)
1989 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?
1990 } else {
1991 E2eEncryptionPolicy::PreferOff
1992 };
1993 #[cfg(not(feature = "std"))]
1994 let e2e_encryption = e2e_encryption_policy_from_code(read_u8(
1995 payload,
1996 &mut cursor,
1997 "discovery schema e2e cryptography",
1998 )?)
1999 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
2000 let endpoint_count =
2001 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
2002 let mut type_endpoints = bounded_wire_vec(
2003 payload,
2004 cursor,
2005 endpoint_count,
2006 4,
2007 "discovery schema type endpoint count",
2008 )?;
2009 for _ in 0..endpoint_count {
2010 type_endpoints.push(DataEndpoint(read_u32(
2011 payload,
2012 &mut cursor,
2013 "discovery schema type endpoint",
2014 )?));
2015 }
2016 types.push(OwnedDataTypeDefinition {
2017 id,
2018 name,
2019 description,
2020 element,
2021 endpoints: type_endpoints,
2022 reliable,
2023 priority,
2024 e2e_encryption,
2025 });
2026 }
2027
2028 if cursor != payload.len() {
2029 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
2030 }
2031 Ok(OwnedRuntimeSchemaSnapshot { endpoints, types })
2032}
2033
2034pub(crate) fn same_schema_snapshot(existing: &Packet, incoming: &Packet) -> bool {
2038 existing.data_type() == DataType::DiscoverySchema
2039 && incoming.data_type() == DataType::DiscoverySchema
2040 && existing.sender() == incoming.sender()
2041 && existing.endpoints() == incoming.endpoints()
2042 && existing.wire_target_senders() == incoming.wire_target_senders()
2043 && existing.payload() == incoming.payload()
2044}
2045
2046#[cfg(test)]
2047mod bounded_decode_tests {
2048 use super::{
2049 ADDRESS_MODE_STATIC, ADDRESS_STATE_APPROVED, AddressAdvertisement, LinkCapabilities,
2050 build_discovery_address, build_managed_variable_request, decode_discovery_address,
2051 decode_discovery_schema_payload, decode_discovery_timesync_sources_payload,
2052 decode_discovery_topology_payload, decode_managed_variable_request,
2053 discovery_schema_payload_is_fully_known,
2054 };
2055 use crate::{DataEndpoint, DataType};
2056
2057 #[test]
2058 fn equivalent_schema_definitions_are_detected_without_owned_decode() {
2059 fn one_endpoint(id: DataEndpoint) -> Vec<u8> {
2060 let mut payload = Vec::new();
2061 payload.extend_from_slice(&3u32.to_le_bytes());
2062 payload.extend_from_slice(&1u32.to_le_bytes());
2063 payload.extend_from_slice(&id.0.to_le_bytes());
2064 payload.push(0);
2065 let meta = crate::config::get_endpoint_meta(id);
2066 payload.extend_from_slice(&(meta.name.len() as u32).to_le_bytes());
2067 payload.extend_from_slice(meta.name.as_bytes());
2068 payload.extend_from_slice(&0u32.to_le_bytes());
2069 payload.extend_from_slice(&0u32.to_le_bytes());
2070 payload
2071 }
2072
2073 assert!(
2074 discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint::Discovery,))
2075 .unwrap()
2076 );
2077 assert!(
2078 !discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint(u32::MAX - 7),))
2079 .unwrap()
2080 );
2081 }
2082
2083 #[test]
2084 fn reused_ids_with_changed_schema_are_not_treated_as_known() {
2085 let schema = crate::config::export_schema();
2086 let original =
2087 super::build_discovery_schema_from_owned_snapshot("test", 1, schema.clone()).unwrap();
2088 assert!(discovery_schema_payload_is_fully_known(original.payload()).unwrap());
2089 for change in 0..4 {
2090 let mut updated = schema.clone();
2091 match change {
2092 0 => updated.endpoints[0].name.push_str("_CHANGED"),
2093 1 => updated.endpoints[0].link_local_only = !updated.endpoints[0].link_local_only,
2094 2 => updated.types[0].name.push_str("_CHANGED"),
2095 _ => updated.types[0].endpoints.push(DataEndpoint(u32::MAX - 7)),
2096 }
2097 let packet =
2098 super::build_discovery_schema_from_owned_snapshot("test", 2, updated).unwrap();
2099 assert!(
2100 !discovery_schema_payload_is_fully_known(packet.payload()).unwrap(),
2101 "change {change} was silently ignored"
2102 );
2103 }
2104 }
2105
2106 #[test]
2107 fn unknown_managed_variable_ids_survive_discovery_before_schema_merge() {
2108 let unknown = DataType(4090);
2109 let unknown_endpoint = DataEndpoint(4090);
2110 let advertisement = AddressAdvertisement {
2111 hostname: "flight".into(),
2112 address: 42,
2113 requested_address: 42,
2114 mode: ADDRESS_MODE_STATIC,
2115 state: ADDRESS_STATE_APPROVED,
2116 birth_ms: 10,
2117 owner_hash: 20,
2118 reachable_endpoints: vec![unknown_endpoint],
2119 reachable_network_variables: vec![unknown],
2120 reachable_timesync_sources: Vec::new(),
2121 link_capabilities: LinkCapabilities {
2122 version: 1,
2123 flags: 0,
2124 profile: 0,
2125 max_frame_bytes: 128,
2126 compact_header_target_bytes: 40,
2127 max_side_transport_templates: 4,
2128 },
2129 };
2130 let packet = build_discovery_address("FC", 1, &advertisement).unwrap();
2131 let decoded = decode_discovery_address(&packet).unwrap();
2132 assert_eq!(decoded.reachable_endpoints, vec![unknown_endpoint]);
2133 assert_eq!(decoded.reachable_network_variables, vec![unknown]);
2134
2135 let request = build_managed_variable_request("GS", 2, unknown).unwrap();
2136 assert_eq!(decode_managed_variable_request(&request).unwrap(), unknown);
2137 }
2138
2139 #[test]
2140 fn corrupt_timesync_count_is_rejected_without_reserving() {
2141 assert!(decode_discovery_timesync_sources_payload(&u32::MAX.to_le_bytes()).is_err());
2142 }
2143
2144 #[test]
2145 fn corrupt_topology_count_is_rejected_without_reserving() {
2146 assert!(decode_discovery_topology_payload(&u32::MAX.to_le_bytes()).is_err());
2147 }
2148
2149 #[test]
2150 fn corrupt_schema_counts_are_rejected_without_reserving() {
2151 let mut endpoints = 3u32.to_le_bytes().to_vec();
2152 endpoints.extend_from_slice(&u32::MAX.to_le_bytes());
2153 assert!(decode_discovery_schema_payload(&endpoints).is_err());
2154
2155 let mut types = 3u32.to_le_bytes().to_vec();
2156 types.extend_from_slice(&0u32.to_le_bytes());
2157 types.extend_from_slice(&u32::MAX.to_le_bytes());
2158 assert!(decode_discovery_schema_payload(&types).is_err());
2159 }
2160 #[test]
2161 fn borrowed_topology_comparison_requires_exact_unchanged_semantics() {
2162 use super::{
2163 TopologyBoardNode, apply_topology_update, build_discovery_topology,
2164 build_discovery_topology_delta, decode_discovery_topology_update,
2165 topology_payload_matches_retained,
2166 };
2167 use crate::packet::Packet;
2168 let nodes = vec![
2169 TopologyBoardNode {
2170 sender_id: "AB".into(),
2171 reachable_endpoints: vec![DataEndpoint(400)],
2172 reachable_timesync_sources: vec!["UTC".into()],
2173 connections: vec!["GS".into()],
2174 },
2175 TopologyBoardNode {
2176 sender_id: "VB".into(),
2177 reachable_endpoints: vec![DataEndpoint(401)],
2178 reachable_timesync_sources: vec![],
2179 connections: vec!["AB".into(), "GS".into()],
2180 },
2181 ];
2182 let full = build_discovery_topology("GS", 0, &nodes).unwrap();
2183 let delta =
2184 build_discovery_topology_delta("GS", 0, &nodes[1..], &["ABSENT".into()]).unwrap();
2185 assert_eq!(
2186 topology_payload_matches_retained(full.payload(), &nodes),
2187 Some(true)
2188 );
2189 assert_eq!(
2190 topology_payload_matches_retained(delta.payload(), &nodes),
2191 Some(false)
2192 );
2193 let removal = build_discovery_topology_delta("GS", 0, &[], &["VB".into()]).unwrap();
2194 assert_eq!(
2195 topology_payload_matches_retained(removal.payload(), &nodes),
2196 None
2197 );
2198 assert_eq!(
2199 topology_payload_matches_retained(full.payload(), &nodes[..1]),
2200 None
2201 );
2202 for packet in [&full, &delta] {
2204 for end in 0..packet.payload().len() {
2205 assert_eq!(
2206 topology_payload_matches_retained(&packet.payload()[..end], &nodes),
2207 None
2208 );
2209 }
2210 let mut trailing = packet.payload().to_vec();
2211 trailing.push(0);
2212 assert_eq!(topology_payload_matches_retained(&trailing, &nodes), None);
2213 for index in 0..packet.payload().len() {
2216 for bit in 0..8 {
2217 let mut payload = packet.payload().to_vec();
2218 payload[index] ^= 1 << bit;
2219 if let Some(is_full) = topology_payload_matches_retained(&payload, &nodes) {
2220 let mutated = Packet::new(
2221 DataType::DiscoveryTopology,
2222 &[DataEndpoint::Discovery],
2223 "GS",
2224 0,
2225 payload.into(),
2226 )
2227 .unwrap();
2228 let update = decode_discovery_topology_update(&mutated).unwrap();
2229 assert_eq!(is_full, !update.incremental);
2230 let mut retained = nodes.clone();
2231 assert!(!apply_topology_update(&mut retained, update));
2232 assert_eq!(retained, nodes);
2233 }
2234 }
2235 }
2236 }
2237 }
2238}