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;
175pub const LINK_CAPABILITY_PAYLOAD_COMPRESSION: u32 = 0x0000_0040;
177
178pub const LINK_PROFILE_CANONICAL: u8 = 0;
179pub const LINK_PROFILE_TEMPLATE: u8 = 1;
180pub const LINK_PROFILE_IPV6_LIKE: u8 = 2;
181pub const LINK_PROFILE_IPV4_LIKE: u8 = 3;
182
183pub const ADDRESS_MODE_DYNAMIC: u8 = 0;
184pub const ADDRESS_MODE_REQUESTED: u8 = 1;
185pub const ADDRESS_MODE_STATIC: u8 = 2;
186pub const ADDRESS_STATE_REQUEST: u8 = 0;
187pub const ADDRESS_STATE_APPROVED: u8 = 1;
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct LinkCapabilities {
191 pub version: u8,
192 pub flags: u32,
193 pub profile: u8,
194 pub max_frame_bytes: u32,
195 pub compact_header_target_bytes: u32,
196 pub max_side_transport_templates: u32,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct AddressAdvertisement {
201 pub hostname: String,
202 pub address: u32,
203 pub requested_address: u32,
204 pub mode: u8,
205 pub state: u8,
206 pub birth_ms: u64,
207 pub owner_hash: u64,
208 pub reachable_endpoints: Vec<DataEndpoint>,
209 pub reachable_network_variables: Vec<DataType>,
213 pub reachable_timesync_sources: Vec<String>,
214 pub link_capabilities: LinkCapabilities,
215}
216
217#[inline]
218pub const fn is_router_control_endpoint(ep: DataEndpoint) -> bool {
219 matches!(
220 ep,
221 DataEndpoint::TelemetryError | DataEndpoint::TimeSync | DataEndpoint::Discovery
222 )
223}
224
225#[inline]
227pub const fn is_discovery_endpoint(ep: DataEndpoint) -> bool {
228 matches!(ep, DataEndpoint::Discovery)
229}
230
231#[inline]
233pub const fn is_discovery_type(ty: DataType) -> bool {
234 matches!(
235 ty,
236 DataType::DiscoveryAnnounce
237 | DataType::DiscoveryTimeSyncSources
238 | DataType::DiscoveryTopology
239 | DataType::DiscoverySchema
240 | DataType::DiscoveryTopologyRequest
241 | DataType::DiscoverySchemaRequest
242 | DataType::ManagedVariableRequest
243 | DataType::ManagedVariableValue
244 | DataType::DiscoveryLeave
245 | DataType::DiscoveryLinkCapabilities
246 | DataType::DiscoveryAddress
247 )
248}
249
250#[inline]
251pub const fn is_discovery_request_type(ty: DataType) -> bool {
252 matches!(
253 ty,
254 DataType::DiscoveryTopologyRequest
255 | DataType::DiscoverySchemaRequest
256 | DataType::ManagedVariableRequest
257 | DataType::DiscoveryLeave
258 | DataType::DiscoveryAddress
259 )
260}
261
262fn sort_dedup_strings(items: &mut Vec<String>) {
263 items.sort_unstable();
264 items.dedup();
265}
266
267pub fn normalize_topology_boards(boards: &mut Vec<TopologyBoardNode>) {
269 for board in boards.iter_mut() {
270 board
271 .reachable_endpoints
272 .retain(|ep| !is_router_control_endpoint(*ep));
273 crate::small_sort::sort(&mut board.reachable_endpoints);
274 board.reachable_endpoints.dedup();
275 sort_dedup_strings(&mut board.reachable_timesync_sources);
276 board.connections.retain(|peer| peer != &board.sender_id);
277 sort_dedup_strings(&mut board.connections);
278 }
279 boards.sort_unstable_by(|a, b| a.sender_id.cmp(&b.sender_id));
280 boards.dedup_by(|a, b| a.sender_id == b.sender_id);
281}
282
283pub fn topology_links_from_boards(boards: &[TopologyBoardNode]) -> Vec<TopologyLink> {
284 let mut links = Vec::new();
285 for board in boards {
286 for peer in &board.connections {
287 if peer == &board.sender_id {
288 continue;
289 }
290 let (source, target) = if board.sender_id <= *peer {
291 (board.sender_id.clone(), peer.clone())
292 } else {
293 (peer.clone(), board.sender_id.clone())
294 };
295 links.push(TopologyLink { source, target });
296 }
297 }
298 links.sort_unstable_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target)));
299 links.dedup_by(|a, b| a.source == b.source && a.target == b.target);
300 links
301}
302
303pub fn merge_topology_boards(dst: &mut Vec<TopologyBoardNode>, src: &[TopologyBoardNode]) {
305 let mut index = 0;
310 while index < dst.len() {
311 if dst[index + 1..]
312 .iter()
313 .any(|node| node.sender_id == dst[index].sender_id)
314 {
315 dst.remove(index);
316 } else {
317 index += 1;
318 }
319 }
320 for board in src {
321 if let Some(entry) = dst
322 .iter_mut()
323 .find(|node| node.sender_id == board.sender_id)
324 {
325 for endpoint in &board.reachable_endpoints {
326 if !entry.reachable_endpoints.contains(endpoint) {
327 entry.reachable_endpoints.push(*endpoint);
328 }
329 }
330 for source in &board.reachable_timesync_sources {
331 if !entry.reachable_timesync_sources.contains(source) {
332 entry.reachable_timesync_sources.push(source.clone());
333 }
334 }
335 for connection in &board.connections {
336 if !entry.connections.contains(connection) {
337 entry.connections.push(connection.clone());
338 }
339 }
340 } else {
341 dst.push(board.clone());
342 }
343 }
344 normalize_topology_boards(dst);
345}
346
347pub fn summarize_topology_boards(boards: &[TopologyBoardNode]) -> (Vec<DataEndpoint>, Vec<String>) {
349 let mut reachable_endpoints = Vec::new();
350 let mut reachable_timesync_sources = Vec::new();
351 for board in boards {
352 for endpoint in &board.reachable_endpoints {
353 if !is_router_control_endpoint(*endpoint) && !reachable_endpoints.contains(endpoint) {
354 reachable_endpoints.push(*endpoint);
355 }
356 }
357 for source in &board.reachable_timesync_sources {
358 if !reachable_timesync_sources.contains(source) {
359 reachable_timesync_sources.push(source.clone());
360 }
361 }
362 }
363 crate::small_sort::sort(&mut reachable_endpoints);
364 reachable_endpoints.dedup();
365 reachable_endpoints.retain(|ep| !is_router_control_endpoint(*ep));
366 sort_dedup_strings(&mut reachable_timesync_sources);
367 (reachable_endpoints, reachable_timesync_sources)
368}
369
370pub fn build_discovery_announce(
372 sender: &str,
373 timestamp_ms: u64,
374 endpoints: &[DataEndpoint],
375) -> TelemetryResult<Packet> {
376 let payload_words: Vec<u32> = endpoints.iter().copied().map(|ep| ep.as_u32()).collect();
377 Packet::new(
378 DataType::DiscoveryAnnounce,
379 &[DataEndpoint::Discovery],
380 sender,
381 timestamp_ms,
382 encode_slice_le(payload_words.as_slice()),
383 )
384}
385
386pub fn decode_discovery_announce(pkt: &Packet) -> TelemetryResult<Vec<DataEndpoint>> {
388 if pkt.data_type() != DataType::DiscoveryAnnounce {
389 return Err(TelemetryError::InvalidType);
390 }
391 decode_discovery_payload(pkt.payload())
392}
393
394pub fn decode_discovery_payload(payload: &[u8]) -> TelemetryResult<Vec<DataEndpoint>> {
396 if !payload.len().is_multiple_of(4) {
397 return Err(TelemetryError::Unpack("discovery payload width"));
398 }
399
400 let mut endpoints = Vec::with_capacity(payload.len() / 4);
401 for chunk in payload.as_chunks::<4>().0 {
402 let raw = u32::from_le_bytes(*chunk);
403 let ep = DataEndpoint(raw);
404 if is_discovery_endpoint(ep) {
405 continue;
406 }
407 endpoints.push(ep);
408 }
409 crate::small_sort::sort(&mut endpoints);
410 endpoints.dedup();
411 Ok(endpoints)
412}
413
414pub fn build_discovery_timesync_sources<S: AsRef<str>>(
416 sender: &str,
417 timestamp_ms: u64,
418 sources: &[S],
419) -> TelemetryResult<Packet> {
420 let mut payload = Vec::new();
421 let mut deduped: Vec<&str> = sources.iter().map(|s| s.as_ref()).collect();
422 crate::small_sort::sort(&mut deduped);
423 deduped.dedup();
424
425 payload.extend_from_slice(&(deduped.len() as u32).to_le_bytes());
426 for source in deduped {
427 let bytes = source.as_bytes();
428 let len = u32::try_from(bytes.len())
429 .map_err(|_| TelemetryError::Pack("discovery source id too long"))?;
430 payload.extend_from_slice(&len.to_le_bytes());
431 payload.extend_from_slice(bytes);
432 }
433
434 Packet::new(
435 DataType::DiscoveryTimeSyncSources,
436 &[DataEndpoint::Discovery],
437 sender,
438 timestamp_ms,
439 payload.into(),
440 )
441}
442
443pub fn build_discovery_topology_request(
444 sender: &str,
445 timestamp_ms: u64,
446) -> TelemetryResult<Packet> {
447 Packet::new(
448 DataType::DiscoveryTopologyRequest,
449 &[DataEndpoint::Discovery],
450 sender,
451 timestamp_ms,
452 Vec::<u8>::new().into(),
453 )
454}
455
456pub fn build_discovery_schema_request(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
457 Packet::new(
458 DataType::DiscoverySchemaRequest,
459 &[DataEndpoint::Discovery],
460 sender,
461 timestamp_ms,
462 Vec::<u8>::new().into(),
463 )
464}
465
466pub fn build_discovery_leave(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
467 Packet::new(
468 DataType::DiscoveryLeave,
469 &[DataEndpoint::Discovery],
470 sender,
471 timestamp_ms,
472 Vec::<u8>::new().into(),
473 )
474}
475
476pub fn build_discovery_link_capabilities(
477 sender: &str,
478 timestamp_ms: u64,
479 capabilities: LinkCapabilities,
480) -> TelemetryResult<Packet> {
481 let mut payload = Vec::with_capacity(18);
482 payload.push(capabilities.version);
483 payload.extend_from_slice(&capabilities.flags.to_le_bytes());
484 payload.push(capabilities.profile);
485 payload.extend_from_slice(&capabilities.max_frame_bytes.to_le_bytes());
486 payload.extend_from_slice(&capabilities.compact_header_target_bytes.to_le_bytes());
487 payload.extend_from_slice(&capabilities.max_side_transport_templates.to_le_bytes());
488 Packet::new(
489 DataType::DiscoveryLinkCapabilities,
490 &[DataEndpoint::Discovery],
491 sender,
492 timestamp_ms,
493 payload.into(),
494 )
495}
496
497pub fn decode_discovery_link_capabilities(pkt: &Packet) -> TelemetryResult<LinkCapabilities> {
498 if pkt.data_type() != DataType::DiscoveryLinkCapabilities {
499 return Err(TelemetryError::InvalidType);
500 }
501 decode_link_capabilities_payload(pkt.payload())
502}
503
504fn decode_link_capabilities_payload(payload: &[u8]) -> TelemetryResult<LinkCapabilities> {
505 if payload.len() != 18 {
506 return Err(TelemetryError::Unpack("discovery link capabilities width"));
507 }
508 Ok(LinkCapabilities {
509 version: payload[0],
510 flags: u32::from_le_bytes(payload[1..5].try_into().expect("4-byte flags")),
511 profile: payload[5],
512 max_frame_bytes: u32::from_le_bytes(payload[6..10].try_into().expect("4-byte max frame")),
513 compact_header_target_bytes: u32::from_le_bytes(
514 payload[10..14].try_into().expect("4-byte target"),
515 ),
516 max_side_transport_templates: u32::from_le_bytes(
517 payload[14..18].try_into().expect("4-byte templates"),
518 ),
519 })
520}
521
522pub fn build_discovery_address(
523 sender: &str,
524 timestamp_ms: u64,
525 ad: &AddressAdvertisement,
526) -> TelemetryResult<Packet> {
527 let mut payload = Vec::new();
528 payload.push(2);
529 payload.push(ad.mode);
530 payload.push(ad.state);
531 payload.extend_from_slice(&ad.address.to_le_bytes());
532 payload.extend_from_slice(&ad.requested_address.to_le_bytes());
533 payload.extend_from_slice(&ad.birth_ms.to_le_bytes());
534 payload.extend_from_slice(&ad.owner_hash.to_le_bytes());
535 encode_string(&mut payload, &ad.hostname)?;
536 let mut endpoints = ad.reachable_endpoints.clone();
537 endpoints.retain(|ep| !is_discovery_endpoint(*ep));
538 crate::small_sort::sort(&mut endpoints);
539 endpoints.dedup();
540 let endpoint_count = u32::try_from(endpoints.len())
541 .map_err(|_| TelemetryError::Pack("discovery address endpoint count"))?;
542 payload.extend_from_slice(&endpoint_count.to_le_bytes());
543 for ep in endpoints {
544 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
545 }
546 let mut network_variables = ad.reachable_network_variables.clone();
547 crate::small_sort::sort(&mut network_variables);
548 network_variables.dedup();
549 let network_variable_count = u32::try_from(network_variables.len())
550 .map_err(|_| TelemetryError::Pack("discovery address network variable count"))?;
551 payload.extend_from_slice(&network_variable_count.to_le_bytes());
552 for ty in network_variables {
553 payload.extend_from_slice(&ty.as_u32().to_le_bytes());
554 }
555 let mut sources = ad.reachable_timesync_sources.clone();
556 sort_dedup_strings(&mut sources);
557 let source_count = u32::try_from(sources.len())
558 .map_err(|_| TelemetryError::Pack("discovery address source count"))?;
559 payload.extend_from_slice(&source_count.to_le_bytes());
560 for source in sources {
561 encode_string(&mut payload, &source)?;
562 }
563 payload.push(ad.link_capabilities.version);
564 payload.extend_from_slice(&ad.link_capabilities.flags.to_le_bytes());
565 payload.push(ad.link_capabilities.profile);
566 payload.extend_from_slice(&ad.link_capabilities.max_frame_bytes.to_le_bytes());
567 payload.extend_from_slice(
568 &ad.link_capabilities
569 .compact_header_target_bytes
570 .to_le_bytes(),
571 );
572 payload.extend_from_slice(
573 &ad.link_capabilities
574 .max_side_transport_templates
575 .to_le_bytes(),
576 );
577 Packet::new(
578 DataType::DiscoveryAddress,
579 &[DataEndpoint::Discovery],
580 sender,
581 timestamp_ms,
582 payload.into(),
583 )
584}
585
586pub(crate) struct AddressView<'a> {
588 pub hostname: &'a str,
589 pub address: u32,
590 pub requested_address: u32,
591 pub mode: u8,
592 pub state: u8,
593 pub birth_ms: u64,
594 pub owner_hash: u64,
595 reachable_endpoints: &'a [u8],
596 reachable_network_variables: &'a [u8],
597 reachable_timesync_sources: &'a [u8],
598 pub link_capabilities: LinkCapabilities,
599}
600impl AddressView<'_> {
601 pub fn endpoints(&self) -> impl Iterator<Item = DataEndpoint> + '_ {
602 self.reachable_endpoints
603 .as_chunks::<4>()
604 .0
605 .iter()
606 .map(|b| DataEndpoint(u32::from_le_bytes(*b)))
607 .filter(|ep| !is_discovery_endpoint(*ep))
608 }
609 pub fn variables(&self) -> impl Iterator<Item = DataType> + '_ {
610 self.reachable_network_variables
611 .as_chunks::<4>()
612 .0
613 .iter()
614 .map(|b| DataType(u32::from_le_bytes(*b)))
615 .filter(|ty| !is_discovery_type(*ty))
616 }
617 pub fn sources(&self) -> impl Iterator<Item = &str> + '_ {
618 let mut cursor = 0usize;
619 core::iter::from_fn(move || {
620 if cursor == self.reachable_timesync_sources.len() {
621 return None;
622 }
623 borrowed_schema_string(
624 self.reachable_timesync_sources,
625 &mut cursor,
626 "discovery address source",
627 )
628 .ok()
629 })
630 .filter(|s| !s.is_empty())
631 }
632}
633fn borrowed_u32s<'a>(
634 payload: &'a [u8],
635 cursor: &mut usize,
636 count: usize,
637 label: &'static str,
638) -> TelemetryResult<&'a [u8]> {
639 if count > payload.len().saturating_sub(*cursor) / 4 {
640 return Err(TelemetryError::Unpack(label));
641 }
642 let start = *cursor;
643 *cursor += count * 4;
644 Ok(&payload[start..*cursor])
645}
646pub(crate) fn decode_address_view(payload: &[u8]) -> TelemetryResult<AddressView<'_>> {
647 let mut cursor = 0usize;
648 let version = read_u8(payload, &mut cursor, "discovery address version")?;
649 if version != 1 && version != 2 {
650 return Err(TelemetryError::Unpack("discovery address version"));
651 }
652 let mode = read_u8(payload, &mut cursor, "discovery address mode")?;
653 let state = read_u8(payload, &mut cursor, "discovery address state")?;
654 let address = read_u32(payload, &mut cursor, "discovery address current")?;
655 let requested_address = read_u32(payload, &mut cursor, "discovery address requested")?;
656 if payload.len().saturating_sub(cursor) < 8 {
657 return Err(TelemetryError::Unpack("discovery address birth"));
658 }
659 let birth_ms = u64::from_le_bytes(
660 payload[cursor..cursor + 8]
661 .try_into()
662 .expect("8-byte birth"),
663 );
664 cursor += 8;
665 if payload.len().saturating_sub(cursor) < 8 {
666 return Err(TelemetryError::Unpack("discovery address owner"));
667 }
668 let owner_hash = u64::from_le_bytes(
669 payload[cursor..cursor + 8]
670 .try_into()
671 .expect("8-byte owner"),
672 );
673 cursor += 8;
674 let hostname = borrowed_schema_string(payload, &mut cursor, "discovery address hostname")?;
675 let endpoint_count =
676 read_u32(payload, &mut cursor, "discovery address endpoint count")? as usize;
677 let reachable_endpoints = borrowed_u32s(
678 payload,
679 &mut cursor,
680 endpoint_count,
681 "discovery address endpoint count",
682 )?;
683 let reachable_network_variables = if version >= 2 {
684 let count = read_u32(
685 payload,
686 &mut cursor,
687 "discovery address network variable count",
688 )? as usize;
689 borrowed_u32s(
690 payload,
691 &mut cursor,
692 count,
693 "discovery address network variable count",
694 )?
695 } else {
696 &[]
697 };
698 let count = read_u32(payload, &mut cursor, "discovery address source count")? as usize;
699 if count > payload.len().saturating_sub(cursor) / 4 {
700 return Err(TelemetryError::Unpack("discovery address source count"));
701 }
702 let source_start = cursor;
703 for _ in 0..count {
704 borrowed_schema_string(payload, &mut cursor, "discovery address source")?;
705 }
706 let reachable_timesync_sources = &payload[source_start..cursor];
707 let link_capabilities = decode_link_capabilities_payload(&payload[cursor..])?;
710 if hostname.is_empty() || address == 0 {
711 return Err(TelemetryError::Unpack("bad discovery address"));
712 }
713 Ok(AddressView {
714 hostname,
715 address,
716 requested_address,
717 mode,
718 state,
719 birth_ms,
720 owner_hash,
721 reachable_endpoints,
722 reachable_network_variables,
723 reachable_timesync_sources,
724 link_capabilities,
725 })
726}
727
728pub fn decode_discovery_address(pkt: &Packet) -> TelemetryResult<AddressAdvertisement> {
729 if pkt.data_type() != DataType::DiscoveryAddress {
730 return Err(TelemetryError::InvalidType);
731 }
732 let ad = decode_address_view(pkt.payload())?;
733 let mut reachable_endpoints = bounded_wire_vec(
734 ad.reachable_endpoints,
735 0,
736 ad.reachable_endpoints.len() / 4,
737 4,
738 "discovery address endpoint count",
739 )?;
740 reachable_endpoints.extend(ad.endpoints());
741 crate::small_sort::sort(&mut reachable_endpoints);
742 reachable_endpoints.dedup();
743 let mut reachable_network_variables = bounded_wire_vec(
744 ad.reachable_network_variables,
745 0,
746 ad.reachable_network_variables.len() / 4,
747 4,
748 "discovery address network variable count",
749 )?;
750 reachable_network_variables.extend(ad.variables());
751 crate::small_sort::sort(&mut reachable_network_variables);
752 reachable_network_variables.dedup();
753 let mut reachable_timesync_sources = bounded_wire_vec(
754 ad.reachable_timesync_sources,
755 0,
756 ad.sources().count(),
757 4,
758 "discovery address source count",
759 )?;
760 reachable_timesync_sources.extend(ad.sources().map(ToString::to_string));
761 sort_dedup_strings(&mut reachable_timesync_sources);
762 Ok(AddressAdvertisement {
763 hostname: ad.hostname.into(),
764 address: ad.address,
765 requested_address: ad.requested_address,
766 mode: ad.mode,
767 state: ad.state,
768 birth_ms: ad.birth_ms,
769 owner_hash: ad.owner_hash,
770 reachable_endpoints,
771 reachable_network_variables,
772 reachable_timesync_sources,
773 link_capabilities: ad.link_capabilities,
774 })
775}
776
777pub fn build_managed_variable_request(
778 sender: &str,
779 timestamp_ms: u64,
780 ty: DataType,
781) -> TelemetryResult<Packet> {
782 Packet::new(
783 DataType::ManagedVariableRequest,
784 &[DataEndpoint::Discovery],
785 sender,
786 timestamp_ms,
787 encode_slice_le(&[ty.as_u32()]),
788 )
789}
790
791pub fn decode_managed_variable_request(pkt: &Packet) -> TelemetryResult<DataType> {
792 if pkt.data_type() != DataType::ManagedVariableRequest {
793 return Err(TelemetryError::InvalidType);
794 }
795 let payload = pkt.payload();
796 if payload.len() != 4 {
797 return Err(TelemetryError::Unpack("managed variable request width"));
798 }
799 let raw = u32::from_le_bytes(payload.try_into().expect("4-byte payload"));
800 Ok(DataType(raw))
803}
804
805pub fn decode_discovery_timesync_sources(pkt: &Packet) -> TelemetryResult<Vec<String>> {
807 if pkt.data_type() != DataType::DiscoveryTimeSyncSources {
808 return Err(TelemetryError::InvalidType);
809 }
810 decode_discovery_timesync_sources_payload(pkt.payload())
811}
812
813pub fn decode_discovery_timesync_sources_payload(payload: &[u8]) -> TelemetryResult<Vec<String>> {
815 if payload.len() < 4 {
816 return Err(TelemetryError::Unpack("discovery timesync source count"));
817 }
818
819 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
820 let mut cursor = 4usize;
821 let mut out = bounded_wire_vec(payload, cursor, count, 4, "discovery timesync source count")?;
822
823 for _ in 0..count {
824 if payload.len().saturating_sub(cursor) < 4 {
825 return Err(TelemetryError::Unpack("discovery timesync source len"));
826 }
827 let len = u32::from_le_bytes(payload[cursor..cursor + 4].try_into().expect("4-byte len"))
828 as usize;
829 cursor += 4;
830 if payload.len().saturating_sub(cursor) < len {
831 return Err(TelemetryError::Unpack("discovery timesync source bytes"));
832 }
833 let raw = &payload[cursor..cursor + len];
834 cursor += len;
835 let source = core::str::from_utf8(raw)
836 .map_err(|_| TelemetryError::Unpack("discovery timesync source utf8"))?;
837 if !source.is_empty() {
838 out.push(source.to_string());
839 }
840 }
841
842 if cursor != payload.len() {
843 return Err(TelemetryError::Unpack("discovery timesync trailing bytes"));
844 }
845
846 out.sort_unstable();
847 out.dedup();
848 Ok(out)
849}
850
851pub(crate) fn topology_payload_is_incremental(payload: &[u8]) -> bool {
852 payload.starts_with(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes())
853}
854
855pub(crate) fn apply_shared_topology_update(
856 retained: &mut Arc<Vec<TopologyBoardNode>>,
857 update: DiscoveryTopologyUpdate,
858) -> bool {
859 if !update.incremental {
860 if retained.as_slice() == update.boards {
861 return false;
862 }
863 *retained = Arc::new(update.boards);
864 true
865 } else {
866 apply_topology_update(Arc::make_mut(retained), update)
867 }
868}
869
870#[derive(Default)]
874pub(crate) struct CloneScratch {
875 pub bytes: usize,
876 pub largest: usize,
877}
878impl CloneScratch {
879 pub fn buffer(&mut self, bytes: usize) {
880 if bytes != 0 {
881 let owned = bytes.saturating_add(32);
882 self.bytes = self.bytes.saturating_add(owned);
883 self.largest = self.largest.max(owned);
884 }
885 }
886 pub fn strings(&mut self, strings: &[String]) {
887 self.buffer(strings.len().saturating_mul(core::mem::size_of::<String>()));
888 for value in strings {
889 self.buffer(value.len());
890 }
891 }
892 pub fn graph(&mut self, boards: &[TopologyBoardNode], new_nodes: usize) {
893 let node_size = core::mem::size_of::<TopologyBoardNode>();
894 self.buffer(boards.len().saturating_mul(node_size));
896 self.buffer(
897 boards
898 .len()
899 .saturating_add(new_nodes)
900 .saturating_mul(node_size)
901 .saturating_mul(2),
902 );
903 for board in boards {
904 self.buffer(board.sender_id.len());
905 self.buffer(board.reachable_endpoints.len().saturating_mul(4));
906 self.strings(&board.reachable_timesync_sources);
907 self.strings(&board.connections);
908 }
909 }
910}
911
912type TopologyScratchPeer<'a> = (
913 &'a str,
914 &'a [DataEndpoint],
915 &'a [String],
916 &'a [TopologyBoardNode],
917);
918
919pub(crate) fn topology_snapshot_scratch<'a>(
922 sender: &str,
923 local_endpoints: usize,
924 variables: usize,
925 graph_copies: usize,
926 include_wire: bool,
927 peers: impl Iterator<Item = TopologyScratchPeer<'a>> + Clone,
928) -> (usize, usize) {
929 topology_snapshot_scratch_impl(
930 sender,
931 local_endpoints,
932 variables,
933 graph_copies,
934 include_wire,
935 &|index| peers.clone().nth(index),
936 )
937}
938
939#[inline(never)]
940fn topology_snapshot_scratch_impl<'a>(
941 sender: &str,
942 local_endpoints: usize,
943 variables: usize,
944 graph_copies: usize,
945 include_wire: bool,
946 peer_at: &dyn Fn(usize) -> Option<TopologyScratchPeer<'a>>,
947) -> (usize, usize) {
948 let peers = core::iter::from_fn({
951 let mut index = 0usize;
952 move || {
953 let peer = peer_at(index);
954 index = index.saturating_add(1);
955 peer
956 }
957 });
958 #[derive(Clone, Copy)]
959 struct View<'a> {
960 name: &'a str,
961 endpoints: &'a [DataEndpoint],
962 sources: &'a [String],
963 connections: &'a [String],
964 backlink: bool,
965 }
966 let node_size = core::mem::size_of::<TopologyBoardNode>();
967 let string_size = core::mem::size_of::<String>();
968 let mut nodes = 1usize;
969 let mut graph = node_size
970 .saturating_add(sender.len().saturating_mul(2))
971 .saturating_add(local_endpoints.saturating_add(1).saturating_mul(4))
972 .saturating_add(string_size);
973 let mut wire = 24usize
974 .saturating_add(sender.len().saturating_mul(2))
975 .saturating_add(local_endpoints.saturating_add(1).saturating_mul(4));
976 for (index, (name, _, _, _)) in peers.clone().enumerate() {
977 if name != sender && !peers.clone().take(index).any(|(old, _, _, _)| old == name) {
978 graph = graph.saturating_add(string_size).saturating_add(name.len());
979 wire = wire.saturating_add(4).saturating_add(name.len());
980 }
981 }
982 let views = peers.flat_map(|(name, endpoints, sources, boards)| {
985 core::iter::once(View {
986 name,
987 endpoints,
988 sources,
989 connections: &[],
990 backlink: true,
991 })
992 .filter(move |_| boards.is_empty())
993 .chain(boards.iter().map(move |board| View {
994 name: &board.sender_id,
995 endpoints: &board.reachable_endpoints,
996 sources: &board.reachable_timesync_sources,
997 connections: &board.connections,
998 backlink: board.sender_id == name,
999 }))
1000 });
1001 for (index, node) in views.clone().enumerate() {
1002 let prior = || {
1003 views
1004 .clone()
1005 .take(index)
1006 .filter(|old| old.name == node.name)
1007 };
1008 if node.name != sender && prior().next().is_none() {
1009 nodes = nodes.saturating_add(1);
1010 graph = graph
1011 .saturating_add(node_size)
1012 .saturating_add(node.name.len());
1013 wire = wire.saturating_add(20).saturating_add(node.name.len());
1014 }
1015 for endpoint in node.endpoints {
1016 if !prior().any(|old| old.endpoints.contains(endpoint)) {
1017 graph = graph.saturating_add(4);
1018 wire = wire.saturating_add(4);
1019 }
1020 }
1021 for source in node.sources {
1022 if !prior().any(|old| old.sources.contains(source)) {
1023 graph = graph
1024 .saturating_add(string_size)
1025 .saturating_add(source.len());
1026 wire = wire.saturating_add(4).saturating_add(source.len());
1027 }
1028 }
1029 for connection in node.connections {
1030 if connection != node.name
1031 && !prior().any(|old| {
1032 old.connections.contains(connection) || (old.backlink && connection == sender)
1033 })
1034 {
1035 graph = graph
1036 .saturating_add(string_size)
1037 .saturating_add(connection.len());
1038 wire = wire.saturating_add(4).saturating_add(connection.len());
1039 }
1040 }
1041 if node.backlink
1042 && node.name != sender
1043 && !node.connections.iter().any(|value| value == sender)
1044 && !prior()
1045 .any(|old| old.backlink || old.connections.iter().any(|value| value == sender))
1046 {
1047 graph = graph
1048 .saturating_add(string_size)
1049 .saturating_add(sender.len());
1050 wire = wire.saturating_add(4).saturating_add(sender.len());
1051 }
1052 }
1053 let grow = |bytes: usize| bytes.checked_next_power_of_two().unwrap_or(usize::MAX);
1054 let largest = 1024usize
1055 .max(grow(nodes).saturating_mul(node_size))
1056 .max(if include_wire { grow(wire) } else { 0 });
1057 let additional = 2048usize
1061 .saturating_add(graph.saturating_mul(graph_copies))
1062 .saturating_add(variables.saturating_mul(32));
1063 (additional, largest)
1064}
1065
1066#[inline(never)]
1070fn topology_payload(
1071 boards: &[TopologyBoardNode],
1072 removed: Option<&[String]>,
1073) -> TelemetryResult<Vec<u8>> {
1074 let canonical = boards
1075 .windows(2)
1076 .all(|pair| pair[0].sender_id < pair[1].sender_id)
1077 && boards.iter().all(|board| {
1078 board
1079 .reachable_endpoints
1080 .iter()
1081 .all(|ep| !is_router_control_endpoint(*ep))
1082 && board
1083 .reachable_endpoints
1084 .windows(2)
1085 .all(|pair| pair[0] < pair[1])
1086 && board
1087 .reachable_timesync_sources
1088 .windows(2)
1089 .all(|pair| pair[0] < pair[1])
1090 && board
1091 .connections
1092 .iter()
1093 .all(|peer| peer != &board.sender_id)
1094 && board.connections.windows(2).all(|pair| pair[0] < pair[1])
1095 });
1096 let mut owned = Vec::new();
1097 let normalized = if canonical {
1098 boards
1099 } else {
1100 owned.extend_from_slice(boards);
1101 normalize_topology_boards(&mut owned);
1102 &owned
1103 };
1104 let board_count = normalized.len();
1105 let mut length = 4usize;
1106 let mut add = |bytes: usize| {
1107 length = length.saturating_add(bytes);
1108 };
1109 for board in normalized {
1110 add(16);
1111 add(board.sender_id.len());
1112 add(board.reachable_endpoints.len().saturating_mul(4));
1113 for value in board
1114 .reachable_timesync_sources
1115 .iter()
1116 .chain(&board.connections)
1117 {
1118 add(4);
1119 add(value.len());
1120 }
1121 }
1122 if let Some(removed) = removed {
1123 add(8);
1124 for value in removed {
1125 add(4);
1126 add(value.len());
1127 }
1128 }
1129 let mut payload = Vec::new();
1130 if canonical {
1131 payload
1133 .try_reserve_exact(length)
1134 .map_err(|_| TelemetryError::Io("memory pressure"))?;
1135 } else {
1136 owned.reverse();
1139 }
1140 if removed.is_some() {
1141 payload.extend_from_slice(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes());
1142 }
1143 payload.extend_from_slice(&(board_count as u32).to_le_bytes());
1144 for original in boards.iter().take(board_count) {
1145 let owned_board = owned.pop();
1146 let board = owned_board.as_ref().unwrap_or(original);
1147 encode_string(&mut payload, &board.sender_id)?;
1148
1149 payload.extend_from_slice(&(board.reachable_endpoints.len() as u32).to_le_bytes());
1150 for ep in &board.reachable_endpoints {
1151 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1152 }
1153
1154 payload.extend_from_slice(&(board.reachable_timesync_sources.len() as u32).to_le_bytes());
1155 for source in &board.reachable_timesync_sources {
1156 encode_string(&mut payload, source)?;
1157 }
1158
1159 payload.extend_from_slice(&(board.connections.len() as u32).to_le_bytes());
1160 for peer in &board.connections {
1161 encode_string(&mut payload, peer)?;
1162 }
1163 }
1164
1165 if let Some(removed) = removed {
1166 payload.extend_from_slice(&(removed.len() as u32).to_le_bytes());
1167 for sender_id in removed {
1168 encode_string(&mut payload, sender_id)?;
1169 }
1170 }
1171 debug_assert_eq!(payload.len(), length);
1172 Ok(payload)
1173}
1174
1175pub fn build_discovery_topology(
1177 sender: &str,
1178 timestamp_ms: u64,
1179 boards: &[TopologyBoardNode],
1180) -> TelemetryResult<Packet> {
1181 let payload = topology_payload(boards, None)?;
1182
1183 Packet::new(
1184 DataType::DiscoveryTopology,
1185 &[DataEndpoint::Discovery],
1186 sender,
1187 timestamp_ms,
1188 payload.into(),
1189 )
1190}
1191
1192#[derive(Debug, Clone, PartialEq, Eq)]
1195pub struct DiscoveryTopologyUpdate {
1196 pub incremental: bool,
1197 pub boards: Vec<TopologyBoardNode>,
1198 pub removed: Vec<String>,
1199}
1200
1201pub fn apply_topology_update(
1204 retained: &mut Vec<TopologyBoardNode>,
1205 update: DiscoveryTopologyUpdate,
1206) -> bool {
1207 if !update.incremental {
1208 if *retained == update.boards {
1209 return false;
1210 }
1211 *retained = update.boards;
1212 return true;
1213 }
1214 let old_len = retained.len();
1215 retained.retain(|board| {
1216 !update.removed.iter().any(|id| id == &board.sender_id)
1217 || update
1218 .boards
1219 .iter()
1220 .any(|replacement| replacement.sender_id == board.sender_id)
1221 });
1222 let mut changed = old_len != retained.len();
1223 for board in update.boards {
1224 if let Some(old) = retained
1225 .iter_mut()
1226 .find(|old| old.sender_id == board.sender_id)
1227 {
1228 if *old != board {
1229 *old = board;
1230 changed = true;
1231 }
1232 } else {
1233 retained.push(board);
1234 changed = true;
1235 }
1236 }
1237 if changed {
1238 normalize_topology_boards(retained);
1239 }
1240 changed
1241}
1242
1243pub fn topology_delta(
1245 previous: &[TopologyBoardNode],
1246 current: &[TopologyBoardNode],
1247) -> (Vec<TopologyBoardNode>, Vec<String>) {
1248 let boards = current
1249 .iter()
1250 .filter(|node| previous.iter().find(|old| old.sender_id == node.sender_id) != Some(*node))
1251 .cloned()
1252 .collect();
1253 let removed = previous
1254 .iter()
1255 .filter(|old| !current.iter().any(|node| node.sender_id == old.sender_id))
1256 .map(|old| old.sender_id.clone())
1257 .collect();
1258 (boards, removed)
1259}
1260
1261pub fn build_discovery_topology_delta(
1263 sender: &str,
1264 timestamp_ms: u64,
1265 boards: &[TopologyBoardNode],
1266 removed: &[String],
1267) -> TelemetryResult<Packet> {
1268 let payload = topology_payload(boards, Some(removed))?;
1269 Packet::new(
1270 DataType::DiscoveryTopology,
1271 &[DataEndpoint::Discovery],
1272 sender,
1273 timestamp_ms,
1274 payload.into(),
1275 )
1276}
1277
1278fn decode_string(
1279 payload: &[u8],
1280 cursor: &mut usize,
1281 label: &'static str,
1282) -> TelemetryResult<String> {
1283 if payload.len().saturating_sub(*cursor) < 4 {
1284 return Err(TelemetryError::Unpack(label));
1285 }
1286 let len = u32::from_le_bytes(
1287 payload[*cursor..*cursor + 4]
1288 .try_into()
1289 .expect("4-byte len"),
1290 ) as usize;
1291 *cursor += 4;
1292 if payload.len().saturating_sub(*cursor) < len {
1293 return Err(TelemetryError::Unpack(label));
1294 }
1295 let raw = &payload[*cursor..*cursor + len];
1296 *cursor += len;
1297 core::str::from_utf8(raw)
1298 .map(|s| s.to_string())
1299 .map_err(|_| TelemetryError::Unpack(label))
1300}
1301
1302pub fn decode_discovery_topology(pkt: &Packet) -> TelemetryResult<Vec<TopologyBoardNode>> {
1304 if pkt.data_type() != DataType::DiscoveryTopology {
1305 return Err(TelemetryError::InvalidType);
1306 }
1307 Ok(decode_discovery_topology_update(pkt)?.boards)
1308}
1309
1310pub(crate) fn topology_payload_matches_retained(
1315 payload: &[u8],
1316 retained: &[TopologyBoardNode],
1317) -> Option<bool> {
1318 fn word(input: &mut &[u8]) -> Option<u32> {
1319 let (head, tail) = input.split_at_checked(4)?;
1320 *input = tail;
1321 Some(u32::from_le_bytes(head.try_into().ok()?))
1322 }
1323 fn text<'a>(input: &mut &'a [u8]) -> Option<&'a str> {
1324 let count = word(input)? as usize;
1325 let (head, tail) = input.split_at_checked(count)?;
1326 *input = tail;
1327 core::str::from_utf8(head).ok()
1328 }
1329 let mut input = payload;
1330 let first = word(&mut input)?;
1331 let full = first != DISCOVERY_TOPOLOGY_DELTA_MARKER;
1332 let count = if full { first } else { word(&mut input)? } as usize;
1333 if (full && count != retained.len()) || count > input.len() / 16 {
1334 return None;
1335 }
1336 let mut previous = None;
1337 for _ in 0..count {
1338 let name = text(&mut input)?;
1339 if previous.is_some_and(|previous| previous >= name) {
1341 return None;
1342 }
1343 previous = Some(name);
1344 let board = retained.iter().find(|board| board.sender_id == name)?;
1345 if word(&mut input)? as usize != board.reachable_endpoints.len() {
1346 return None;
1347 }
1348 for endpoint in &board.reachable_endpoints {
1349 if word(&mut input)? != endpoint.as_u32() {
1350 return None;
1351 }
1352 }
1353 for values in [&board.reachable_timesync_sources, &board.connections] {
1354 if word(&mut input)? as usize != values.len() {
1355 return None;
1356 }
1357 for value in values {
1358 if text(&mut input)? != value {
1359 return None;
1360 }
1361 }
1362 }
1363 }
1364 if !full {
1365 let removed = word(&mut input)? as usize;
1366 if removed > input.len() / 4 {
1367 return None;
1368 }
1369 for _ in 0..removed {
1370 let name = text(&mut input)?;
1371 if retained.iter().any(|board| board.sender_id == name) {
1372 return None;
1373 }
1374 }
1375 }
1376 input.is_empty().then_some(full)
1377}
1378
1379pub fn decode_discovery_topology_update(pkt: &Packet) -> TelemetryResult<DiscoveryTopologyUpdate> {
1381 if pkt.data_type() != DataType::DiscoveryTopology {
1382 return Err(TelemetryError::InvalidType);
1383 }
1384 let payload = pkt.payload();
1385 if payload.len() < 4 {
1386 return Err(TelemetryError::Unpack("discovery topology board count"));
1387 }
1388 let first = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count"));
1389 if first != DISCOVERY_TOPOLOGY_DELTA_MARKER {
1390 return Ok(DiscoveryTopologyUpdate {
1391 incremental: false,
1392 boards: decode_discovery_topology_payload(payload)?,
1393 removed: Vec::new(),
1394 });
1395 }
1396 if payload.len() < 8 {
1397 return Err(TelemetryError::Unpack("discovery topology delta count"));
1398 }
1399 let count = u32::from_le_bytes(payload[4..8].try_into().expect("4-byte count")) as usize;
1400 let mut cursor = 8usize;
1401 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1402 if payload.len().saturating_sub(cursor) < 4 {
1403 return Err(TelemetryError::Unpack("discovery topology removal count"));
1404 }
1405 let removed_count = u32::from_le_bytes(
1406 payload[cursor..cursor + 4]
1407 .try_into()
1408 .expect("4-byte count"),
1409 ) as usize;
1410 cursor += 4;
1411 let mut removed = bounded_wire_vec(
1412 payload,
1413 cursor,
1414 removed_count,
1415 4,
1416 "discovery topology removal count",
1417 )?;
1418 for _ in 0..removed_count {
1419 removed.push(decode_string(
1420 payload,
1421 &mut cursor,
1422 "discovery topology removed sender id",
1423 )?);
1424 }
1425 if cursor != payload.len() {
1426 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1427 }
1428 normalize_topology_boards(&mut boards);
1429 removed.sort_unstable();
1430 removed.dedup();
1431 Ok(DiscoveryTopologyUpdate {
1432 incremental: true,
1433 boards,
1434 removed,
1435 })
1436}
1437
1438pub fn decode_discovery_topology_payload(
1440 payload: &[u8],
1441) -> TelemetryResult<Vec<TopologyBoardNode>> {
1442 if payload.len() < 4 {
1443 return Err(TelemetryError::Unpack("discovery topology board count"));
1444 }
1445
1446 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
1447 let mut cursor = 4usize;
1448 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1449
1450 if cursor != payload.len() {
1451 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1452 }
1453
1454 normalize_topology_boards(&mut boards);
1455 Ok(boards)
1456}
1457
1458fn decode_topology_boards(
1459 payload: &[u8],
1460 cursor: &mut usize,
1461 count: usize,
1462) -> TelemetryResult<Vec<TopologyBoardNode>> {
1463 let mut boards = bounded_wire_vec(
1465 payload,
1466 *cursor,
1467 count,
1468 16,
1469 "discovery topology board count",
1470 )?;
1471
1472 for _ in 0..count {
1473 let sender_id = decode_string(payload, cursor, "discovery topology sender id")?;
1474
1475 if payload.len().saturating_sub(*cursor) < 4 {
1476 return Err(TelemetryError::Unpack("discovery topology endpoint count"));
1477 }
1478 let endpoint_count = u32::from_le_bytes(
1479 payload[*cursor..*cursor + 4]
1480 .try_into()
1481 .expect("4-byte count"),
1482 ) as usize;
1483 *cursor += 4;
1484 let mut reachable_endpoints = bounded_wire_vec(
1485 payload,
1486 *cursor,
1487 endpoint_count,
1488 4,
1489 "discovery topology endpoint count",
1490 )?;
1491 for _ in 0..endpoint_count {
1492 if payload.len().saturating_sub(*cursor) < 4 {
1493 return Err(TelemetryError::Unpack("discovery topology endpoint"));
1494 }
1495 let raw =
1496 u32::from_le_bytes(payload[*cursor..*cursor + 4].try_into().expect("4-byte ep"));
1497 *cursor += 4;
1498 let ep = DataEndpoint(raw);
1499 if !is_discovery_endpoint(ep) {
1500 reachable_endpoints.push(ep);
1501 }
1502 }
1503
1504 if payload.len().saturating_sub(*cursor) < 4 {
1505 return Err(TelemetryError::Unpack(
1506 "discovery topology timesync source count",
1507 ));
1508 }
1509 let source_count = u32::from_le_bytes(
1510 payload[*cursor..*cursor + 4]
1511 .try_into()
1512 .expect("4-byte count"),
1513 ) as usize;
1514 *cursor += 4;
1515 let mut reachable_timesync_sources = bounded_wire_vec(
1516 payload,
1517 *cursor,
1518 source_count,
1519 4,
1520 "discovery topology timesync source count",
1521 )?;
1522 for _ in 0..source_count {
1523 let source = decode_string(payload, cursor, "discovery topology timesync source")?;
1524 if !source.is_empty() {
1525 reachable_timesync_sources.push(source);
1526 }
1527 }
1528
1529 if payload.len().saturating_sub(*cursor) < 4 {
1530 return Err(TelemetryError::Unpack(
1531 "discovery topology connection count",
1532 ));
1533 }
1534 let connection_count = u32::from_le_bytes(
1535 payload[*cursor..*cursor + 4]
1536 .try_into()
1537 .expect("4-byte count"),
1538 ) as usize;
1539 *cursor += 4;
1540 let mut connections = bounded_wire_vec(
1541 payload,
1542 *cursor,
1543 connection_count,
1544 4,
1545 "discovery topology connection count",
1546 )?;
1547 for _ in 0..connection_count {
1548 let peer = decode_string(payload, cursor, "discovery topology connection")?;
1549 if !peer.is_empty() {
1550 connections.push(peer);
1551 }
1552 }
1553
1554 boards.push(TopologyBoardNode {
1555 sender_id,
1556 reachable_endpoints,
1557 reachable_timesync_sources,
1558 connections,
1559 });
1560 }
1561
1562 Ok(boards)
1563}
1564
1565fn encode_string(payload: &mut Vec<u8>, value: &str) -> TelemetryResult<()> {
1566 let len = u32::try_from(value.len())
1567 .map_err(|_| TelemetryError::Pack("discovery schema string too long"))?;
1568 payload.extend_from_slice(&len.to_le_bytes());
1569 payload.extend_from_slice(value.as_bytes());
1570 Ok(())
1571}
1572
1573fn read_u8(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u8> {
1574 if payload.len().saturating_sub(*cursor) < 1 {
1575 return Err(TelemetryError::Unpack(label));
1576 }
1577 let out = payload[*cursor];
1578 *cursor += 1;
1579 Ok(out)
1580}
1581
1582fn read_u32(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u32> {
1583 if payload.len().saturating_sub(*cursor) < 4 {
1584 return Err(TelemetryError::Unpack(label));
1585 }
1586 let out = u32::from_le_bytes(
1587 payload[*cursor..*cursor + 4]
1588 .try_into()
1589 .expect("4-byte u32"),
1590 );
1591 *cursor += 4;
1592 Ok(out)
1593}
1594
1595#[cfg(any(not(feature = "std"), test))]
1596fn skip_string(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<()> {
1597 let len = read_u32(payload, cursor, label)? as usize;
1598 if payload.len().saturating_sub(*cursor) < len {
1599 return Err(TelemetryError::Unpack(label));
1600 }
1601 *cursor += len;
1602 Ok(())
1603}
1604
1605fn borrowed_schema_string<'a>(
1606 payload: &'a [u8],
1607 cursor: &mut usize,
1608 label: &'static str,
1609) -> TelemetryResult<&'a str> {
1610 let len = read_u32(payload, cursor, label)? as usize;
1611 let end = cursor
1612 .checked_add(len)
1613 .ok_or(TelemetryError::Unpack(label))?;
1614 let bytes = payload
1615 .get(*cursor..end)
1616 .ok_or(TelemetryError::Unpack(label))?;
1617 *cursor = end;
1618 core::str::from_utf8(bytes).map_err(|_| TelemetryError::Unpack(label))
1619}
1620
1621#[cfg(any(not(feature = "std"), test))]
1626fn scan_schema_changes(
1627 payload: &[u8],
1628 retain: bool,
1629) -> TelemetryResult<(bool, OwnedRuntimeSchemaSnapshot)> {
1630 let mut changed = OwnedRuntimeSchemaSnapshot {
1631 endpoints: Vec::new(),
1632 types: Vec::new(),
1633 };
1634 let mut cursor = 0usize;
1635 if read_u32(payload, &mut cursor, "discovery schema version")? != 3 {
1636 return Err(TelemetryError::Unpack("discovery schema version"));
1637 }
1638
1639 let endpoint_count =
1640 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1641 if endpoint_count > payload.len().saturating_sub(cursor) / 13 {
1642 return Err(TelemetryError::Unpack("discovery schema endpoint count"));
1643 }
1644 let mut all_known = true;
1645 for _ in 0..endpoint_count {
1646 let id = DataEndpoint(read_u32(
1647 payload,
1648 &mut cursor,
1649 "discovery schema endpoint id",
1650 )?);
1651 let known = crate::config::endpoint_exists(id);
1652 let meta = crate::config::get_endpoint_meta(id);
1653 let link_local_only =
1654 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1655 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema endpoint name")?;
1656 let equivalent = known
1657 && AsRef::<str>::as_ref(&meta.name) == name
1658 && (!link_local_only || meta.link_local_only);
1659 skip_string(
1660 payload,
1661 &mut cursor,
1662 "discovery schema endpoint description",
1663 )?;
1664 all_known &= equivalent;
1665 if retain && !equivalent {
1666 let bytes = name
1667 .len()
1668 .saturating_add(core::mem::size_of::<OwnedEndpointDefinition>());
1669 crate::memory_admission::check(
1670 bytes.saturating_mul(3).saturating_add(1024),
1671 bytes.saturating_add(128),
1672 )?;
1673 changed
1674 .endpoints
1675 .try_reserve_exact(1)
1676 .map_err(|_| TelemetryError::Io("memory pressure"))?;
1677 changed.endpoints.push(OwnedEndpointDefinition {
1678 id,
1679 name: name.into(),
1680 description: String::new(),
1681 link_local_only,
1682 });
1683 }
1684 }
1685
1686 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1687 if type_count > payload.len().saturating_sub(cursor) / 19 {
1688 return Err(TelemetryError::Unpack("discovery schema type count"));
1689 }
1690 for _ in 0..type_count {
1691 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1692 let meta = crate::config::try_get_message_meta(id);
1693 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema type name")?;
1694 skip_string(payload, &mut cursor, "discovery schema type description")?;
1695 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1696 if element_kind > 1 {
1697 return Err(TelemetryError::Unpack("discovery schema element kind"));
1698 }
1699 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1700 let data_type = message_data_type_from_code(read_u8(
1701 payload,
1702 &mut cursor,
1703 "discovery schema data type",
1704 )?)
1705 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1706 let class =
1707 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1708 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1709 let reliable =
1710 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1711 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1712 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1713 let encryption = e2e_encryption_policy_from_code(read_u8(
1714 payload,
1715 &mut cursor,
1716 "discovery schema e2e cryptography",
1717 )?)
1718 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1719 let endpoint_count =
1720 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1721 if endpoint_count > payload.len().saturating_sub(cursor) / 4 {
1722 return Err(TelemetryError::Unpack(
1723 "discovery schema type endpoint count",
1724 ));
1725 }
1726 let element = if element_kind == 0 {
1727 MessageElement::Static(count, data_type, class)
1728 } else {
1729 MessageElement::Dynamic(data_type, class)
1730 };
1731 let mut equivalent = meta.as_ref().is_some_and(|meta| {
1732 AsRef::<str>::as_ref(&meta.name) == name
1733 && meta.element == element
1734 && meta.reliable == reliable
1735 && meta.priority >= priority
1736 && meta.e2e_encryption == encryption
1737 });
1738 let endpoints_start = cursor;
1739 for _ in 0..endpoint_count {
1740 let ep = DataEndpoint(read_u32(
1741 payload,
1742 &mut cursor,
1743 "discovery schema type endpoint",
1744 )?);
1745 equivalent &= meta
1746 .as_ref()
1747 .is_some_and(|meta| meta.endpoints.contains(&ep));
1748 }
1749 all_known &= equivalent;
1750 if retain && !equivalent {
1751 let bytes = name
1752 .len()
1753 .saturating_add(endpoint_count.saturating_mul(4))
1754 .saturating_add(core::mem::size_of::<OwnedDataTypeDefinition>());
1755 crate::memory_admission::check(
1758 bytes.saturating_mul(3).saturating_add(1024),
1759 bytes.saturating_add(128),
1760 )?;
1761 changed
1762 .types
1763 .try_reserve_exact(1)
1764 .map_err(|_| TelemetryError::Io("memory pressure"))?;
1765 let mut endpoints = bounded_wire_vec(
1766 payload,
1767 endpoints_start,
1768 endpoint_count,
1769 4,
1770 "discovery schema type endpoint count",
1771 )?;
1772 for bytes in payload[endpoints_start..cursor].as_chunks::<4>().0 {
1773 endpoints.push(DataEndpoint(u32::from_le_bytes(*bytes)));
1774 }
1775 changed.types.push(OwnedDataTypeDefinition {
1776 id,
1777 name: name.into(),
1778 description: String::new(),
1779 element,
1780 endpoints,
1781 reliable,
1782 priority,
1783 e2e_encryption: encryption,
1784 });
1785 }
1786 }
1787 if cursor != payload.len() {
1788 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1789 }
1790 Ok((all_known, changed))
1791}
1792
1793#[cfg(test)]
1794pub(crate) fn discovery_schema_payload_is_fully_known(payload: &[u8]) -> TelemetryResult<bool> {
1795 scan_schema_changes(payload, false).map(|(known, _)| known)
1796}
1797
1798#[cfg(any(not(feature = "std"), test))]
1801pub(crate) fn decode_schema_changes(payload: &[u8]) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1802 scan_schema_changes(payload, true).map(|(_, changed)| changed)
1803}
1804
1805pub fn build_discovery_schema(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
1807 #[cfg(feature = "std")]
1808 return build_discovery_schema_from_owned_snapshot(sender, timestamp_ms, export_schema());
1809 #[cfg(not(feature = "std"))]
1810 return Packet::new(
1811 DataType::DiscoverySchema,
1812 &[DataEndpoint::Discovery],
1813 sender,
1814 timestamp_ms,
1815 crate::config::encode_embedded_schema_payload()?,
1816 );
1817}
1818
1819pub fn elect_discovery_master(local_sender: &str, boards: &[TopologyBoardNode]) -> String {
1827 let mut nodes: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
1828 for board in boards {
1829 nodes.entry(board.sender_id.as_str()).or_default();
1830 for peer in board.connections.iter() {
1831 nodes
1832 .entry(peer.as_str())
1833 .or_default()
1834 .push(board.sender_id.as_str());
1835 nodes
1836 .entry(board.sender_id.as_str())
1837 .or_default()
1838 .push(peer.as_str());
1839 }
1840 }
1841 nodes.entry(local_sender).or_default();
1842 for peers in nodes.values_mut() {
1843 peers.sort_unstable();
1844 peers.dedup();
1845 }
1846
1847 let mut best_sender = local_sender;
1848 let mut best_unreachable = usize::MAX;
1849 let mut best_max_hops = usize::MAX;
1850 let mut best_total_hops = usize::MAX;
1851
1852 let names: Vec<&str> = nodes.keys().copied().collect();
1856 let mut distances = vec![usize::MAX; names.len()];
1857 let mut frontier = Vec::with_capacity(names.len());
1858 for (source_index, &sender) in names.iter().enumerate() {
1859 distances.fill(usize::MAX);
1860 frontier.clear();
1861 frontier.push(source_index);
1862 distances[source_index] = 0;
1863 let mut idx = 0;
1864 while idx < frontier.len() {
1865 let current = frontier[idx];
1866 idx += 1;
1867 let current_distance = distances[current];
1868 if let Some(peers) = nodes.get(names[current]) {
1869 for &peer in peers {
1870 let Ok(peer_index) = names.binary_search(&peer) else {
1872 continue;
1873 };
1874 if distances[peer_index] == usize::MAX {
1875 distances[peer_index] = current_distance + 1;
1876 frontier.push(peer_index);
1877 }
1878 }
1879 }
1880 }
1881
1882 let reachable = distances.iter().copied().filter(|&n| n != usize::MAX);
1883 let unreachable = names.len().saturating_sub(frontier.len());
1884 let max_hops = reachable.clone().max().unwrap_or(0);
1885 let total_hops = reachable.sum();
1886 let better = unreachable < best_unreachable
1887 || (unreachable == best_unreachable && max_hops < best_max_hops)
1888 || (unreachable == best_unreachable
1889 && max_hops == best_max_hops
1890 && total_hops < best_total_hops)
1891 || (unreachable == best_unreachable
1892 && max_hops == best_max_hops
1893 && total_hops == best_total_hops
1894 && sender < best_sender);
1895 if better {
1896 best_sender = sender;
1897 best_unreachable = unreachable;
1898 best_max_hops = max_hops;
1899 best_total_hops = total_hops;
1900 }
1901 }
1902
1903 best_sender.to_string()
1904}
1905
1906pub fn build_discovery_schema_from_snapshot(
1908 sender: &str,
1909 timestamp_ms: u64,
1910 schema: RuntimeSchemaSnapshot,
1911) -> TelemetryResult<Packet> {
1912 let mut payload = Vec::new();
1913 payload.extend_from_slice(&3u32.to_le_bytes());
1914
1915 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1916 for ep in schema.endpoints {
1917 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1918 payload.push(ep.link_local_only as u8);
1919 encode_string(&mut payload, ep.name)?;
1920 #[cfg(feature = "std")]
1921 encode_string(&mut payload, ep.description)?;
1922 #[cfg(not(feature = "std"))]
1923 encode_string(&mut payload, "")?;
1924 }
1925
1926 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1927 for ty in schema.types {
1928 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1929 encode_string(&mut payload, ty.name)?;
1930 #[cfg(feature = "std")]
1931 encode_string(&mut payload, ty.description)?;
1932 #[cfg(not(feature = "std"))]
1933 encode_string(&mut payload, "")?;
1934 match ty.element {
1935 MessageElement::Static(count, data_type, class) => {
1936 payload.push(0);
1937 payload.extend_from_slice(&(count as u32).to_le_bytes());
1938 payload.push(message_data_type_code(data_type));
1939 payload.push(message_class_code(class));
1940 }
1941 MessageElement::Dynamic(data_type, class) => {
1942 payload.push(1);
1943 payload.extend_from_slice(&0u32.to_le_bytes());
1944 payload.push(message_data_type_code(data_type));
1945 payload.push(message_class_code(class));
1946 }
1947 }
1948 payload.push(reliable_code(ty.reliable));
1949 payload.push(ty.priority);
1950 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1951 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1952 for ep in ty.endpoints {
1953 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1954 }
1955 }
1956
1957 Packet::new(
1958 DataType::DiscoverySchema,
1959 &[DataEndpoint::Discovery],
1960 sender,
1961 timestamp_ms,
1962 payload.into(),
1963 )
1964}
1965
1966pub fn build_discovery_schema_from_owned_snapshot(
1968 sender: &str,
1969 timestamp_ms: u64,
1970 schema: OwnedRuntimeSchemaSnapshot,
1971) -> TelemetryResult<Packet> {
1972 let mut payload = Vec::new();
1973 payload.extend_from_slice(&3u32.to_le_bytes());
1974
1975 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1976 for ep in schema.endpoints {
1977 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1978 payload.push(ep.link_local_only as u8);
1979 encode_string(&mut payload, &ep.name)?;
1980 encode_string(&mut payload, "")?;
1984 }
1985
1986 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1987 for ty in schema.types {
1988 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1989 encode_string(&mut payload, &ty.name)?;
1990 encode_string(&mut payload, "")?;
1991 match ty.element {
1992 MessageElement::Static(count, data_type, class) => {
1993 payload.push(0);
1994 payload.extend_from_slice(&(count as u32).to_le_bytes());
1995 payload.push(message_data_type_code(data_type));
1996 payload.push(message_class_code(class));
1997 }
1998 MessageElement::Dynamic(data_type, class) => {
1999 payload.push(1);
2000 payload.extend_from_slice(&0u32.to_le_bytes());
2001 payload.push(message_data_type_code(data_type));
2002 payload.push(message_class_code(class));
2003 }
2004 }
2005 payload.push(reliable_code(ty.reliable));
2006 payload.push(ty.priority);
2007 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
2008 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
2009 for ep in ty.endpoints {
2010 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
2011 }
2012 }
2013
2014 Packet::new(
2015 DataType::DiscoverySchema,
2016 &[DataEndpoint::Discovery],
2017 sender,
2018 timestamp_ms,
2019 payload.into(),
2020 )
2021}
2022
2023pub fn decode_discovery_schema(pkt: &Packet) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
2025 if pkt.data_type() != DataType::DiscoverySchema {
2026 return Err(TelemetryError::InvalidType);
2027 }
2028 decode_discovery_schema_payload(pkt.payload())
2029}
2030
2031pub fn decode_discovery_schema_payload(
2033 payload: &[u8],
2034) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
2035 let mut cursor = 0usize;
2036 let version = read_u32(payload, &mut cursor, "discovery schema version")?;
2037 #[cfg(feature = "std")]
2038 if version != 1 && version != 2 && version != 3 {
2039 return Err(TelemetryError::Unpack("discovery schema version"));
2040 }
2041 #[cfg(not(feature = "std"))]
2042 if version != 3 {
2043 return Err(TelemetryError::Unpack("discovery schema version"));
2044 }
2045
2046 let endpoint_count =
2047 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
2048 let mut endpoints = bounded_wire_vec(
2049 payload,
2050 cursor,
2051 endpoint_count,
2052 13,
2053 "discovery schema endpoint count",
2054 )?;
2055 for _ in 0..endpoint_count {
2056 let id = DataEndpoint(read_u32(
2057 payload,
2058 &mut cursor,
2059 "discovery schema endpoint id",
2060 )?);
2061 let link_local_only =
2062 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
2063 let name = decode_string(payload, &mut cursor, "discovery schema endpoint name")?;
2064 #[cfg(feature = "std")]
2065 let description = if version >= 2 {
2066 decode_string(
2067 payload,
2068 &mut cursor,
2069 "discovery schema endpoint description",
2070 )?
2071 } else {
2072 String::new()
2073 };
2074 #[cfg(not(feature = "std"))]
2075 let description = {
2076 skip_string(
2077 payload,
2078 &mut cursor,
2079 "discovery schema endpoint description",
2080 )?;
2081 String::new()
2082 };
2083 endpoints.push(OwnedEndpointDefinition {
2084 id,
2085 name,
2086 description,
2087 link_local_only,
2088 });
2089 }
2090
2091 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
2092 let mut types = bounded_wire_vec(
2093 payload,
2094 cursor,
2095 type_count,
2096 19,
2097 "discovery schema type count",
2098 )?;
2099 for _ in 0..type_count {
2100 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
2101 let name = decode_string(payload, &mut cursor, "discovery schema type name")?;
2102 #[cfg(feature = "std")]
2103 let description = if version >= 2 {
2104 decode_string(payload, &mut cursor, "discovery schema type description")?
2105 } else {
2106 String::new()
2107 };
2108 #[cfg(not(feature = "std"))]
2109 let description = {
2110 skip_string(payload, &mut cursor, "discovery schema type description")?;
2111 String::new()
2112 };
2113 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
2114 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
2115 let data_type = message_data_type_from_code(read_u8(
2116 payload,
2117 &mut cursor,
2118 "discovery schema data type",
2119 )?)
2120 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
2121 let class =
2122 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
2123 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
2124 let element = match element_kind {
2125 0 => MessageElement::Static(count, data_type, class),
2126 1 => MessageElement::Dynamic(data_type, class),
2127 _ => return Err(TelemetryError::Unpack("discovery schema element kind")),
2128 };
2129 let reliable =
2130 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
2131 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
2132 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
2133 #[cfg(feature = "std")]
2134 let e2e_encryption = if version >= 3 {
2135 e2e_encryption_policy_from_code(read_u8(
2136 payload,
2137 &mut cursor,
2138 "discovery schema e2e cryptography",
2139 )?)
2140 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?
2141 } else {
2142 E2eEncryptionPolicy::PreferOff
2143 };
2144 #[cfg(not(feature = "std"))]
2145 let e2e_encryption = e2e_encryption_policy_from_code(read_u8(
2146 payload,
2147 &mut cursor,
2148 "discovery schema e2e cryptography",
2149 )?)
2150 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
2151 let endpoint_count =
2152 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
2153 let mut type_endpoints = bounded_wire_vec(
2154 payload,
2155 cursor,
2156 endpoint_count,
2157 4,
2158 "discovery schema type endpoint count",
2159 )?;
2160 for _ in 0..endpoint_count {
2161 type_endpoints.push(DataEndpoint(read_u32(
2162 payload,
2163 &mut cursor,
2164 "discovery schema type endpoint",
2165 )?));
2166 }
2167 types.push(OwnedDataTypeDefinition {
2168 id,
2169 name,
2170 description,
2171 element,
2172 endpoints: type_endpoints,
2173 reliable,
2174 priority,
2175 e2e_encryption,
2176 });
2177 }
2178
2179 if cursor != payload.len() {
2180 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
2181 }
2182 Ok(OwnedRuntimeSchemaSnapshot { endpoints, types })
2183}
2184
2185pub(crate) fn same_schema_snapshot(existing: &Packet, incoming: &Packet) -> bool {
2189 existing.data_type() == DataType::DiscoverySchema
2190 && incoming.data_type() == DataType::DiscoverySchema
2191 && existing.sender() == incoming.sender()
2192 && existing.endpoints() == incoming.endpoints()
2193 && existing.wire_target_senders() == incoming.wire_target_senders()
2194 && existing.payload() == incoming.payload()
2195}
2196
2197#[cfg(test)]
2198mod bounded_decode_tests {
2199 use super::{
2200 ADDRESS_MODE_STATIC, ADDRESS_STATE_APPROVED, AddressAdvertisement, LinkCapabilities,
2201 build_discovery_address, build_managed_variable_request, decode_discovery_address,
2202 decode_discovery_schema_payload, decode_discovery_timesync_sources_payload,
2203 decode_discovery_topology_payload, decode_managed_variable_request,
2204 discovery_schema_payload_is_fully_known,
2205 };
2206 use crate::{DataEndpoint, DataType};
2207
2208 #[test]
2209 fn schema_delta_owns_only_changed_definitions_and_validates_the_whole_payload() {
2210 let original = crate::config::export_schema();
2211 let mut updated = original.clone();
2212 let id = updated.types[0].id;
2213 updated.types[0].priority = 255;
2214 updated.types[0].endpoints.push(DataEndpoint(u32::MAX - 11));
2215 let packet = super::build_discovery_schema_from_owned_snapshot("rf", 1, updated).unwrap();
2216 let delta = super::decode_schema_changes(packet.payload()).unwrap();
2217 assert!(delta.endpoints.is_empty());
2218 assert_eq!(delta.types.len(), 1);
2219 assert_eq!(delta.types[0].id, id);
2220 assert_eq!(delta.types[0].priority, 255);
2221 assert!(
2222 delta.types[0]
2223 .endpoints
2224 .contains(&DataEndpoint(u32::MAX - 11))
2225 );
2226 let mut malformed = packet.payload().to_vec();
2227 malformed.push(0);
2228 assert!(super::decode_schema_changes(&malformed).is_err());
2229 let unchanged =
2230 super::build_discovery_schema_from_owned_snapshot("rf", 1, original).unwrap();
2231 let delta = super::decode_schema_changes(unchanged.payload()).unwrap();
2232 assert!(delta.types.is_empty() && delta.endpoints.is_empty());
2233 }
2234
2235 #[test]
2236 fn schema_delta_retains_unknown_endpoints_and_runtime_types() {
2237 let mut updated = crate::config::export_schema();
2238 let mut endpoint = updated.endpoints[0].clone();
2239 endpoint.id = DataEndpoint(u32::MAX - 13);
2240 endpoint.name = "runtime_delta_endpoint".into();
2241 updated.endpoints.push(endpoint.clone());
2242 let mut ty = updated.types[0].clone();
2243 ty.id = DataType(u32::MAX - 13);
2244 ty.name = "runtime_delta_type".into();
2245 ty.endpoints = vec![endpoint.id];
2246 updated.types.push(ty.clone());
2247 let packet = super::build_discovery_schema_from_owned_snapshot("rf", 1, updated).unwrap();
2248 let delta = super::decode_schema_changes(packet.payload()).unwrap();
2249 assert_eq!(delta.types.len(), 1);
2250 assert_eq!(delta.endpoints.len(), 1);
2251 assert_eq!(delta.types[0].id, ty.id);
2252 assert_eq!(delta.types[0].endpoints, ty.endpoints);
2253 assert_eq!(delta.endpoints[0].id, endpoint.id);
2254 }
2255
2256 #[test]
2257 fn borrowed_address_requires_complete_counts_strings_and_link_contract() {
2258 let advertisement = AddressAdvertisement {
2259 hostname: "borrowed-peer".into(),
2260 address: 42,
2261 requested_address: 42,
2262 mode: ADDRESS_MODE_STATIC,
2263 state: ADDRESS_STATE_APPROVED,
2264 birth_ms: 123,
2265 owner_hash: 456,
2266 reachable_endpoints: vec![DataEndpoint(4001)],
2267 reachable_network_variables: vec![DataType(4002)],
2268 reachable_timesync_sources: vec!["peer-clock".into()],
2269 link_capabilities: LinkCapabilities {
2270 version: 1,
2271 flags: 3,
2272 profile: 2,
2273 max_frame_bytes: 64,
2274 compact_header_target_bytes: 40,
2275 max_side_transport_templates: 8,
2276 },
2277 };
2278 let pkt = build_discovery_address("borrowed-peer", 1, &advertisement).unwrap();
2279 let view = super::decode_address_view(pkt.payload()).unwrap();
2280 assert_eq!(view.hostname, advertisement.hostname);
2281 assert_eq!(view.birth_ms, 123);
2282 assert_eq!(view.owner_hash, 456);
2283 assert_eq!(view.link_capabilities, advertisement.link_capabilities);
2284 assert_eq!(
2285 view.endpoints().collect::<Vec<_>>(),
2286 advertisement.reachable_endpoints
2287 );
2288 assert_eq!(
2289 view.variables().collect::<Vec<_>>(),
2290 advertisement.reachable_network_variables
2291 );
2292 assert_eq!(view.sources().collect::<Vec<_>>(), vec!["peer-clock"]);
2293 assert_eq!(decode_discovery_address(&pkt).unwrap(), advertisement);
2294 for end in 0..pkt.payload().len() {
2295 assert!(
2296 super::decode_address_view(&pkt.payload()[..end]).is_err(),
2297 "truncation {end}"
2298 );
2299 }
2300 let mut trailing = pkt.payload().to_vec();
2301 trailing.push(0);
2302 assert!(super::decode_address_view(&trailing).is_err());
2303 let mut bad_count = pkt.payload().to_vec();
2304 let offset = 27 + 4 + advertisement.hostname.len();
2305 bad_count[offset..offset + 4].copy_from_slice(&u32::MAX.to_le_bytes());
2306 assert!(super::decode_address_view(&bad_count).is_err());
2307 let mut invalid_utf8 = pkt.payload().to_vec();
2308 invalid_utf8[31] = 255;
2309 assert!(super::decode_address_view(&invalid_utf8).is_err());
2310 }
2311
2312 #[test]
2313 fn equivalent_schema_definitions_are_detected_without_owned_decode() {
2314 fn one_endpoint(id: DataEndpoint) -> Vec<u8> {
2315 let mut payload = Vec::new();
2316 payload.extend_from_slice(&3u32.to_le_bytes());
2317 payload.extend_from_slice(&1u32.to_le_bytes());
2318 payload.extend_from_slice(&id.0.to_le_bytes());
2319 payload.push(0);
2320 let meta = crate::config::get_endpoint_meta(id);
2321 payload.extend_from_slice(&(meta.name.len() as u32).to_le_bytes());
2322 payload.extend_from_slice(meta.name.as_bytes());
2323 payload.extend_from_slice(&0u32.to_le_bytes());
2324 payload.extend_from_slice(&0u32.to_le_bytes());
2325 payload
2326 }
2327
2328 assert!(
2329 discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint::Discovery,))
2330 .unwrap()
2331 );
2332 assert!(
2333 !discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint(u32::MAX - 7),))
2334 .unwrap()
2335 );
2336 }
2337
2338 #[test]
2339 fn reused_ids_with_changed_schema_are_not_treated_as_known() {
2340 let schema = crate::config::export_schema();
2341 let original =
2342 super::build_discovery_schema_from_owned_snapshot("test", 1, schema.clone()).unwrap();
2343 assert!(discovery_schema_payload_is_fully_known(original.payload()).unwrap());
2344 for change in 0..4 {
2345 let mut updated = schema.clone();
2346 match change {
2347 0 => updated.endpoints[0].name.push_str("_CHANGED"),
2348 1 => updated.endpoints[0].link_local_only = !updated.endpoints[0].link_local_only,
2349 2 => updated.types[0].name.push_str("_CHANGED"),
2350 _ => updated.types[0].endpoints.push(DataEndpoint(u32::MAX - 7)),
2351 }
2352 let packet =
2353 super::build_discovery_schema_from_owned_snapshot("test", 2, updated).unwrap();
2354 assert!(
2355 !discovery_schema_payload_is_fully_known(packet.payload()).unwrap(),
2356 "change {change} was silently ignored"
2357 );
2358 }
2359 }
2360
2361 #[test]
2362 fn unknown_managed_variable_ids_survive_discovery_before_schema_merge() {
2363 let unknown = DataType(4090);
2364 let unknown_endpoint = DataEndpoint(4090);
2365 let advertisement = AddressAdvertisement {
2366 hostname: "flight".into(),
2367 address: 42,
2368 requested_address: 42,
2369 mode: ADDRESS_MODE_STATIC,
2370 state: ADDRESS_STATE_APPROVED,
2371 birth_ms: 10,
2372 owner_hash: 20,
2373 reachable_endpoints: vec![unknown_endpoint],
2374 reachable_network_variables: vec![unknown],
2375 reachable_timesync_sources: Vec::new(),
2376 link_capabilities: LinkCapabilities {
2377 version: 1,
2378 flags: 0,
2379 profile: 0,
2380 max_frame_bytes: 128,
2381 compact_header_target_bytes: 40,
2382 max_side_transport_templates: 4,
2383 },
2384 };
2385 let packet = build_discovery_address("FC", 1, &advertisement).unwrap();
2386 let decoded = decode_discovery_address(&packet).unwrap();
2387 assert_eq!(decoded.reachable_endpoints, vec![unknown_endpoint]);
2388 assert_eq!(decoded.reachable_network_variables, vec![unknown]);
2389
2390 let request = build_managed_variable_request("GS", 2, unknown).unwrap();
2391 assert_eq!(decode_managed_variable_request(&request).unwrap(), unknown);
2392 }
2393
2394 #[test]
2395 fn corrupt_timesync_count_is_rejected_without_reserving() {
2396 assert!(decode_discovery_timesync_sources_payload(&u32::MAX.to_le_bytes()).is_err());
2397 }
2398
2399 #[test]
2400 fn corrupt_topology_count_is_rejected_without_reserving() {
2401 assert!(decode_discovery_topology_payload(&u32::MAX.to_le_bytes()).is_err());
2402 }
2403
2404 #[test]
2405 fn corrupt_schema_counts_are_rejected_without_reserving() {
2406 let mut endpoints = 3u32.to_le_bytes().to_vec();
2407 endpoints.extend_from_slice(&u32::MAX.to_le_bytes());
2408 assert!(decode_discovery_schema_payload(&endpoints).is_err());
2409
2410 let mut types = 3u32.to_le_bytes().to_vec();
2411 types.extend_from_slice(&0u32.to_le_bytes());
2412 types.extend_from_slice(&u32::MAX.to_le_bytes());
2413 assert!(decode_discovery_schema_payload(&types).is_err());
2414 }
2415 #[test]
2416 fn borrowed_topology_comparison_requires_exact_unchanged_semantics() {
2417 use super::{
2418 TopologyBoardNode, apply_topology_update, build_discovery_topology,
2419 build_discovery_topology_delta, decode_discovery_topology_update,
2420 topology_payload_matches_retained,
2421 };
2422 use crate::packet::Packet;
2423 let nodes = vec![
2424 TopologyBoardNode {
2425 sender_id: "AB".into(),
2426 reachable_endpoints: vec![DataEndpoint(400)],
2427 reachable_timesync_sources: vec!["UTC".into()],
2428 connections: vec!["GS".into()],
2429 },
2430 TopologyBoardNode {
2431 sender_id: "VB".into(),
2432 reachable_endpoints: vec![DataEndpoint(401)],
2433 reachable_timesync_sources: vec![],
2434 connections: vec!["AB".into(), "GS".into()],
2435 },
2436 ];
2437 let full = build_discovery_topology("GS", 0, &nodes).unwrap();
2438 let delta =
2439 build_discovery_topology_delta("GS", 0, &nodes[1..], &["ABSENT".into()]).unwrap();
2440 assert_eq!(
2441 topology_payload_matches_retained(full.payload(), &nodes),
2442 Some(true)
2443 );
2444 assert_eq!(
2445 topology_payload_matches_retained(delta.payload(), &nodes),
2446 Some(false)
2447 );
2448 let removal = build_discovery_topology_delta("GS", 0, &[], &["VB".into()]).unwrap();
2449 assert_eq!(
2450 topology_payload_matches_retained(removal.payload(), &nodes),
2451 None
2452 );
2453 assert_eq!(
2454 topology_payload_matches_retained(full.payload(), &nodes[..1]),
2455 None
2456 );
2457 for packet in [&full, &delta] {
2459 for end in 0..packet.payload().len() {
2460 assert_eq!(
2461 topology_payload_matches_retained(&packet.payload()[..end], &nodes),
2462 None
2463 );
2464 }
2465 let mut trailing = packet.payload().to_vec();
2466 trailing.push(0);
2467 assert_eq!(topology_payload_matches_retained(&trailing, &nodes), None);
2468 for index in 0..packet.payload().len() {
2471 for bit in 0..8 {
2472 let mut payload = packet.payload().to_vec();
2473 payload[index] ^= 1 << bit;
2474 if let Some(is_full) = topology_payload_matches_retained(&payload, &nodes) {
2475 let mutated = Packet::new(
2476 DataType::DiscoveryTopology,
2477 &[DataEndpoint::Discovery],
2478 "GS",
2479 0,
2480 payload.into(),
2481 )
2482 .unwrap();
2483 let update = decode_discovery_topology_update(&mutated).unwrap();
2484 assert_eq!(is_full, !update.incremental);
2485 let mut retained = nodes.clone();
2486 assert!(!apply_topology_update(&mut retained, update));
2487 assert_eq!(retained, nodes);
2488 }
2489 }
2490 }
2491 }
2492 }
2493}