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