1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use alloc::vec;
4use alloc::vec::Vec;
5
6#[cfg(feature = "std")]
7use crate::E2eEncryptionPolicy;
8#[cfg(feature = "std")]
9use crate::config::export_schema;
10use crate::router::encode_slice_le;
11use crate::{
12 DataEndpoint, DataType, MessageElement, TelemetryError, TelemetryResult,
13 config::{
14 OwnedDataTypeDefinition, OwnedEndpointDefinition, OwnedRuntimeSchemaSnapshot,
15 RuntimeSchemaSnapshot, e2e_encryption_policy_code, e2e_encryption_policy_from_code,
16 message_class_code, message_class_from_code, message_data_type_code,
17 message_data_type_from_code, reliable_code, reliable_from_code,
18 },
19 packet::Packet,
20 try_enum_from_u32,
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_INCREMENTAL_RETRY_COUNT: u8 = 3;
34pub const TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS: u64 = 30_000;
35const DISCOVERY_TOPOLOGY_DELTA_MARKER: u32 = 0;
39
40fn bounded_wire_vec<T>(
47 payload: &[u8],
48 cursor: usize,
49 count: usize,
50 minimum_encoded_bytes: usize,
51 label: &'static str,
52) -> TelemetryResult<Vec<T>> {
53 let remaining = payload.len().saturating_sub(cursor);
54 if minimum_encoded_bytes == 0 || count > remaining / minimum_encoded_bytes {
55 return Err(TelemetryError::Unpack(label));
56 }
57 let mut values = Vec::new();
58 values
59 .try_reserve_exact(count)
60 .map_err(|_| TelemetryError::Unpack(label))?;
61 Ok(values)
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct DiscoveryCadenceState {
66 pub current_interval_ms: u64,
67 pub next_announce_ms: u64,
68}
69
70impl Default for DiscoveryCadenceState {
71 fn default() -> Self {
72 Self {
73 current_interval_ms: DISCOVERY_FAST_INTERVAL_MS,
74 next_announce_ms: 0,
75 }
76 }
77}
78
79impl DiscoveryCadenceState {
80 pub fn on_topology_change(&mut self, now_ms: u64) {
82 self.current_interval_ms = DISCOVERY_FAST_INTERVAL_MS;
83 self.next_announce_ms = now_ms;
84 }
85
86 pub fn on_announce_sent(&mut self, now_ms: u64) {
88 self.next_announce_ms = now_ms.saturating_add(self.current_interval_ms);
89 self.current_interval_ms = core::cmp::min(
90 self.current_interval_ms.saturating_mul(2),
91 DISCOVERY_SLOW_INTERVAL_MS,
92 );
93 }
94
95 pub fn due(&self, now_ms: u64) -> bool {
97 now_ms >= self.next_announce_ms
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct TopologyBoardNode {
103 pub sender_id: String,
104 pub reachable_endpoints: Vec<DataEndpoint>,
105 pub reachable_timesync_sources: Vec<String>,
106 pub connections: Vec<String>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct TopologyLink {
111 pub source: String,
112 pub target: String,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct TopologyAnnouncerRoute {
117 pub sender_id: String,
118 pub reachable_endpoints: Vec<DataEndpoint>,
119 pub reachable_network_variables: Vec<DataType>,
120 pub reachable_timesync_sources: Vec<String>,
121 pub routers: Vec<TopologyBoardNode>,
122 pub last_seen_ms: u64,
123 pub age_ms: u64,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct TopologySideRoute {
128 pub side_id: usize,
129 pub side_name: String,
130 pub reachable_endpoints: Vec<DataEndpoint>,
131 pub reachable_network_variables: Vec<DataType>,
132 pub reachable_timesync_sources: Vec<String>,
133 pub announcers: Vec<TopologyAnnouncerRoute>,
134 pub last_seen_ms: u64,
135 pub age_ms: u64,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct TopologySnapshot {
140 pub advertised_endpoints: Vec<DataEndpoint>,
141 pub advertised_timesync_sources: Vec<String>,
142 pub routers: Vec<TopologyBoardNode>,
143 pub links: Vec<TopologyLink>,
144 pub routes: Vec<TopologySideRoute>,
145 pub current_announce_interval_ms: u64,
146 pub next_announce_ms: u64,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct ClientStatsSnapshot {
151 pub sender_id: String,
152 pub connected: bool,
153 pub side_ids: Vec<usize>,
154 pub side_names: Vec<String>,
155 pub last_seen_ms: Option<u64>,
156 pub age_ms: Option<u64>,
157 pub reachable_endpoints: Vec<DataEndpoint>,
158 pub reachable_timesync_sources: Vec<String>,
159 pub packets_sent: u64,
160 pub packets_received: u64,
161 pub bytes_sent: u64,
162 pub bytes_received: u64,
163}
164
165pub const LINK_CAPABILITY_HEADER_TEMPLATES: u32 = 0x0000_0001;
166pub const LINK_CAPABILITY_CHUNKING: u32 = 0x0000_0002;
167pub const LINK_CAPABILITY_RELIABILITY: u32 = 0x0000_0004;
168pub const LINK_CAPABILITY_CRYPTO: u32 = 0x0000_0008;
169pub const LINK_CAPABILITY_END_TO_END_RELIABILITY: u32 = 0x0000_0010;
170pub const LINK_CAPABILITY_OMIT_UNCHANGED_TIMESTAMPS: u32 = 0x0000_0020;
171
172pub const LINK_PROFILE_CANONICAL: u8 = 0;
173pub const LINK_PROFILE_TEMPLATE: u8 = 1;
174pub const LINK_PROFILE_IPV6_LIKE: u8 = 2;
175pub const LINK_PROFILE_IPV4_LIKE: u8 = 3;
176
177pub const ADDRESS_MODE_DYNAMIC: u8 = 0;
178pub const ADDRESS_MODE_REQUESTED: u8 = 1;
179pub const ADDRESS_MODE_STATIC: u8 = 2;
180pub const ADDRESS_STATE_REQUEST: u8 = 0;
181pub const ADDRESS_STATE_APPROVED: u8 = 1;
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct LinkCapabilities {
185 pub version: u8,
186 pub flags: u32,
187 pub profile: u8,
188 pub max_frame_bytes: u32,
189 pub compact_header_target_bytes: u32,
190 pub max_side_transport_templates: u32,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct AddressAdvertisement {
195 pub hostname: String,
196 pub address: u32,
197 pub requested_address: u32,
198 pub mode: u8,
199 pub state: u8,
200 pub birth_ms: u64,
201 pub owner_hash: u64,
202 pub reachable_endpoints: Vec<DataEndpoint>,
203 pub reachable_network_variables: Vec<DataType>,
207 pub reachable_timesync_sources: Vec<String>,
208 pub link_capabilities: LinkCapabilities,
209}
210
211#[inline]
212pub const fn is_router_control_endpoint(ep: DataEndpoint) -> bool {
213 matches!(
214 ep,
215 DataEndpoint::TelemetryError | DataEndpoint::TimeSync | DataEndpoint::Discovery
216 )
217}
218
219#[inline]
221pub const fn is_discovery_endpoint(ep: DataEndpoint) -> bool {
222 matches!(ep, DataEndpoint::Discovery)
223}
224
225#[inline]
227pub const fn is_discovery_type(ty: DataType) -> bool {
228 matches!(
229 ty,
230 DataType::DiscoveryAnnounce
231 | DataType::DiscoveryTimeSyncSources
232 | DataType::DiscoveryTopology
233 | DataType::DiscoverySchema
234 | DataType::DiscoveryTopologyRequest
235 | DataType::DiscoverySchemaRequest
236 | DataType::ManagedVariableRequest
237 | DataType::ManagedVariableValue
238 | DataType::DiscoveryLeave
239 | DataType::DiscoveryLinkCapabilities
240 | DataType::DiscoveryAddress
241 )
242}
243
244#[inline]
245pub const fn is_discovery_request_type(ty: DataType) -> bool {
246 matches!(
247 ty,
248 DataType::DiscoveryTopologyRequest
249 | DataType::DiscoverySchemaRequest
250 | DataType::ManagedVariableRequest
251 | DataType::DiscoveryLeave
252 | DataType::DiscoveryAddress
253 )
254}
255
256fn sort_dedup_strings(items: &mut Vec<String>) {
257 items.sort_unstable();
258 items.dedup();
259}
260
261pub fn normalize_topology_boards(boards: &mut Vec<TopologyBoardNode>) {
263 for board in boards.iter_mut() {
264 board
265 .reachable_endpoints
266 .retain(|ep| !is_router_control_endpoint(*ep));
267 board.reachable_endpoints.sort_unstable();
268 board.reachable_endpoints.dedup();
269 sort_dedup_strings(&mut board.reachable_timesync_sources);
270 board.connections.retain(|peer| peer != &board.sender_id);
271 sort_dedup_strings(&mut board.connections);
272 }
273 boards.sort_unstable_by(|a, b| a.sender_id.cmp(&b.sender_id));
274 boards.dedup_by(|a, b| a.sender_id == b.sender_id);
275}
276
277pub fn topology_links_from_boards(boards: &[TopologyBoardNode]) -> Vec<TopologyLink> {
278 let mut links = Vec::new();
279 for board in boards {
280 for peer in &board.connections {
281 if peer == &board.sender_id {
282 continue;
283 }
284 let (source, target) = if board.sender_id <= *peer {
285 (board.sender_id.clone(), peer.clone())
286 } else {
287 (peer.clone(), board.sender_id.clone())
288 };
289 links.push(TopologyLink { source, target });
290 }
291 }
292 links.sort_unstable_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target)));
293 links.dedup_by(|a, b| a.source == b.source && a.target == b.target);
294 links
295}
296
297pub fn merge_topology_boards(dst: &mut Vec<TopologyBoardNode>, src: &[TopologyBoardNode]) {
299 let mut merged: BTreeMap<String, TopologyBoardNode> = dst
300 .iter()
301 .cloned()
302 .map(|board| (board.sender_id.clone(), board))
303 .collect();
304 for board in src {
305 let entry = merged
306 .entry(board.sender_id.clone())
307 .or_insert_with(|| TopologyBoardNode {
308 sender_id: board.sender_id.clone(),
309 reachable_endpoints: Vec::new(),
310 reachable_timesync_sources: Vec::new(),
311 connections: Vec::new(),
312 });
313 entry
314 .reachable_endpoints
315 .extend(board.reachable_endpoints.iter().copied());
316 entry
317 .reachable_timesync_sources
318 .extend(board.reachable_timesync_sources.iter().cloned());
319 entry.connections.extend(board.connections.iter().cloned());
320 }
321 let mut out: Vec<TopologyBoardNode> = merged.into_values().collect();
322 normalize_topology_boards(&mut out);
323 *dst = out;
324}
325
326pub fn summarize_topology_boards(boards: &[TopologyBoardNode]) -> (Vec<DataEndpoint>, Vec<String>) {
328 let mut reachable_endpoints = Vec::new();
329 let mut reachable_timesync_sources = Vec::new();
330 for board in boards {
331 reachable_endpoints.extend(board.reachable_endpoints.iter().copied());
332 reachable_timesync_sources.extend(board.reachable_timesync_sources.iter().cloned());
333 }
334 reachable_endpoints.sort_unstable();
335 reachable_endpoints.dedup();
336 reachable_endpoints.retain(|ep| !is_router_control_endpoint(*ep));
337 sort_dedup_strings(&mut reachable_timesync_sources);
338 (reachable_endpoints, reachable_timesync_sources)
339}
340
341pub fn build_discovery_announce(
343 sender: &str,
344 timestamp_ms: u64,
345 endpoints: &[DataEndpoint],
346) -> TelemetryResult<Packet> {
347 let payload_words: Vec<u32> = endpoints.iter().copied().map(|ep| ep.as_u32()).collect();
348 Packet::new(
349 DataType::DiscoveryAnnounce,
350 &[DataEndpoint::Discovery],
351 sender,
352 timestamp_ms,
353 encode_slice_le(payload_words.as_slice()),
354 )
355}
356
357pub fn decode_discovery_announce(pkt: &Packet) -> TelemetryResult<Vec<DataEndpoint>> {
359 if pkt.data_type() != DataType::DiscoveryAnnounce {
360 return Err(TelemetryError::InvalidType);
361 }
362 decode_discovery_payload(pkt.payload())
363}
364
365pub fn decode_discovery_payload(payload: &[u8]) -> TelemetryResult<Vec<DataEndpoint>> {
367 if !payload.len().is_multiple_of(4) {
368 return Err(TelemetryError::Unpack("discovery payload width"));
369 }
370
371 let mut endpoints = Vec::with_capacity(payload.len() / 4);
372 for chunk in payload.as_chunks::<4>().0 {
373 let raw = u32::from_le_bytes(*chunk);
374 let ep = try_enum_from_u32(raw).ok_or(TelemetryError::Unpack("bad discovery endpoint"))?;
375 if is_discovery_endpoint(ep) {
376 continue;
377 }
378 endpoints.push(ep);
379 }
380 endpoints.sort_unstable();
381 endpoints.dedup();
382 Ok(endpoints)
383}
384
385pub fn build_discovery_timesync_sources<S: AsRef<str>>(
387 sender: &str,
388 timestamp_ms: u64,
389 sources: &[S],
390) -> TelemetryResult<Packet> {
391 let mut payload = Vec::new();
392 let mut deduped: Vec<&str> = sources.iter().map(|s| s.as_ref()).collect();
393 deduped.sort_unstable();
394 deduped.dedup();
395
396 payload.extend_from_slice(&(deduped.len() as u32).to_le_bytes());
397 for source in deduped {
398 let bytes = source.as_bytes();
399 let len = u32::try_from(bytes.len())
400 .map_err(|_| TelemetryError::Pack("discovery source id too long"))?;
401 payload.extend_from_slice(&len.to_le_bytes());
402 payload.extend_from_slice(bytes);
403 }
404
405 Packet::new(
406 DataType::DiscoveryTimeSyncSources,
407 &[DataEndpoint::Discovery],
408 sender,
409 timestamp_ms,
410 payload.into(),
411 )
412}
413
414pub fn build_discovery_topology_request(
415 sender: &str,
416 timestamp_ms: u64,
417) -> TelemetryResult<Packet> {
418 Packet::new(
419 DataType::DiscoveryTopologyRequest,
420 &[DataEndpoint::Discovery],
421 sender,
422 timestamp_ms,
423 Vec::<u8>::new().into(),
424 )
425}
426
427pub fn build_discovery_schema_request(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
428 Packet::new(
429 DataType::DiscoverySchemaRequest,
430 &[DataEndpoint::Discovery],
431 sender,
432 timestamp_ms,
433 Vec::<u8>::new().into(),
434 )
435}
436
437pub fn build_discovery_leave(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
438 Packet::new(
439 DataType::DiscoveryLeave,
440 &[DataEndpoint::Discovery],
441 sender,
442 timestamp_ms,
443 Vec::<u8>::new().into(),
444 )
445}
446
447pub fn build_discovery_link_capabilities(
448 sender: &str,
449 timestamp_ms: u64,
450 capabilities: LinkCapabilities,
451) -> TelemetryResult<Packet> {
452 let mut payload = Vec::with_capacity(18);
453 payload.push(capabilities.version);
454 payload.extend_from_slice(&capabilities.flags.to_le_bytes());
455 payload.push(capabilities.profile);
456 payload.extend_from_slice(&capabilities.max_frame_bytes.to_le_bytes());
457 payload.extend_from_slice(&capabilities.compact_header_target_bytes.to_le_bytes());
458 payload.extend_from_slice(&capabilities.max_side_transport_templates.to_le_bytes());
459 Packet::new(
460 DataType::DiscoveryLinkCapabilities,
461 &[DataEndpoint::Discovery],
462 sender,
463 timestamp_ms,
464 payload.into(),
465 )
466}
467
468pub fn decode_discovery_link_capabilities(pkt: &Packet) -> TelemetryResult<LinkCapabilities> {
469 if pkt.data_type() != DataType::DiscoveryLinkCapabilities {
470 return Err(TelemetryError::InvalidType);
471 }
472 let payload = pkt.payload();
473 if payload.len() != 18 {
474 return Err(TelemetryError::Unpack("discovery link capabilities width"));
475 }
476 Ok(LinkCapabilities {
477 version: payload[0],
478 flags: u32::from_le_bytes(payload[1..5].try_into().expect("4-byte flags")),
479 profile: payload[5],
480 max_frame_bytes: u32::from_le_bytes(payload[6..10].try_into().expect("4-byte max frame")),
481 compact_header_target_bytes: u32::from_le_bytes(
482 payload[10..14].try_into().expect("4-byte target"),
483 ),
484 max_side_transport_templates: u32::from_le_bytes(
485 payload[14..18].try_into().expect("4-byte templates"),
486 ),
487 })
488}
489
490pub fn build_discovery_address(
491 sender: &str,
492 timestamp_ms: u64,
493 ad: &AddressAdvertisement,
494) -> TelemetryResult<Packet> {
495 let mut payload = Vec::new();
496 payload.push(2);
497 payload.push(ad.mode);
498 payload.push(ad.state);
499 payload.extend_from_slice(&ad.address.to_le_bytes());
500 payload.extend_from_slice(&ad.requested_address.to_le_bytes());
501 payload.extend_from_slice(&ad.birth_ms.to_le_bytes());
502 payload.extend_from_slice(&ad.owner_hash.to_le_bytes());
503 encode_string(&mut payload, &ad.hostname)?;
504 let mut endpoints = ad.reachable_endpoints.clone();
505 endpoints.retain(|ep| !is_discovery_endpoint(*ep));
506 endpoints.sort_unstable();
507 endpoints.dedup();
508 let endpoint_count = u32::try_from(endpoints.len())
509 .map_err(|_| TelemetryError::Pack("discovery address endpoint count"))?;
510 payload.extend_from_slice(&endpoint_count.to_le_bytes());
511 for ep in endpoints {
512 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
513 }
514 let mut network_variables = ad.reachable_network_variables.clone();
515 network_variables.sort_unstable();
516 network_variables.dedup();
517 let network_variable_count = u32::try_from(network_variables.len())
518 .map_err(|_| TelemetryError::Pack("discovery address network variable count"))?;
519 payload.extend_from_slice(&network_variable_count.to_le_bytes());
520 for ty in network_variables {
521 payload.extend_from_slice(&ty.as_u32().to_le_bytes());
522 }
523 let mut sources = ad.reachable_timesync_sources.clone();
524 sort_dedup_strings(&mut sources);
525 let source_count = u32::try_from(sources.len())
526 .map_err(|_| TelemetryError::Pack("discovery address source count"))?;
527 payload.extend_from_slice(&source_count.to_le_bytes());
528 for source in sources {
529 encode_string(&mut payload, &source)?;
530 }
531 payload.push(ad.link_capabilities.version);
532 payload.extend_from_slice(&ad.link_capabilities.flags.to_le_bytes());
533 payload.push(ad.link_capabilities.profile);
534 payload.extend_from_slice(&ad.link_capabilities.max_frame_bytes.to_le_bytes());
535 payload.extend_from_slice(
536 &ad.link_capabilities
537 .compact_header_target_bytes
538 .to_le_bytes(),
539 );
540 payload.extend_from_slice(
541 &ad.link_capabilities
542 .max_side_transport_templates
543 .to_le_bytes(),
544 );
545 Packet::new(
546 DataType::DiscoveryAddress,
547 &[DataEndpoint::Discovery],
548 sender,
549 timestamp_ms,
550 payload.into(),
551 )
552}
553
554pub fn decode_discovery_address(pkt: &Packet) -> TelemetryResult<AddressAdvertisement> {
555 if pkt.data_type() != DataType::DiscoveryAddress {
556 return Err(TelemetryError::InvalidType);
557 }
558 let payload = pkt.payload();
559 let mut cursor = 0usize;
560 let version = read_u8(payload, &mut cursor, "discovery address version")?;
561 if version != 1 && version != 2 {
562 return Err(TelemetryError::Unpack("discovery address version"));
563 }
564 let mode = read_u8(payload, &mut cursor, "discovery address mode")?;
565 let state = read_u8(payload, &mut cursor, "discovery address state")?;
566 let address = read_u32(payload, &mut cursor, "discovery address current")?;
567 let requested_address = read_u32(payload, &mut cursor, "discovery address requested")?;
568 if payload.len().saturating_sub(cursor) < 8 {
569 return Err(TelemetryError::Unpack("discovery address birth"));
570 }
571 let birth_ms = u64::from_le_bytes(
572 payload[cursor..cursor + 8]
573 .try_into()
574 .expect("8-byte birth"),
575 );
576 cursor += 8;
577 if payload.len().saturating_sub(cursor) < 8 {
578 return Err(TelemetryError::Unpack("discovery address owner"));
579 }
580 let owner_hash = u64::from_le_bytes(
581 payload[cursor..cursor + 8]
582 .try_into()
583 .expect("8-byte owner"),
584 );
585 cursor += 8;
586 let hostname = decode_string(payload, &mut cursor, "discovery address hostname")?;
587 let endpoint_count =
588 read_u32(payload, &mut cursor, "discovery address endpoint count")? as usize;
589 let mut reachable_endpoints = bounded_wire_vec(
590 payload, cursor, endpoint_count, 4, "discovery address endpoint count",
591 )?;
592 for _ in 0..endpoint_count {
593 let raw = read_u32(payload, &mut cursor, "discovery address endpoint")?;
594 let ep = try_enum_from_u32(raw).ok_or(TelemetryError::Unpack("bad discovery endpoint"))?;
595 if !is_discovery_endpoint(ep) {
596 reachable_endpoints.push(ep);
597 }
598 }
599 reachable_endpoints.sort_unstable();
600 reachable_endpoints.dedup();
601 let mut reachable_network_variables = Vec::new();
602 if version >= 2 {
603 let count = read_u32(
604 payload,
605 &mut cursor,
606 "discovery address network variable count",
607 )? as usize;
608 reachable_network_variables = bounded_wire_vec(
609 payload, cursor, count, 4, "discovery address network variable count",
610 )?;
611 for _ in 0..count {
612 let raw = read_u32(payload, &mut cursor, "discovery address network variable")?;
613 let ty = DataType::try_from_u32(raw)
614 .ok_or(TelemetryError::Unpack("bad discovery network variable"))?;
615 if !is_discovery_type(ty) {
616 reachable_network_variables.push(ty);
617 }
618 }
619 reachable_network_variables.sort_unstable();
620 reachable_network_variables.dedup();
621 }
622 let source_count = read_u32(payload, &mut cursor, "discovery address source count")? as usize;
623 let mut reachable_timesync_sources = bounded_wire_vec(
624 payload, cursor, source_count, 4, "discovery address source count",
625 )?;
626 for _ in 0..source_count {
627 let source = decode_string(payload, &mut cursor, "discovery address source")?;
628 if !source.is_empty() {
629 reachable_timesync_sources.push(source);
630 }
631 }
632 sort_dedup_strings(&mut reachable_timesync_sources);
633 let version = read_u8(payload, &mut cursor, "discovery address link version")?;
634 let flags = read_u32(payload, &mut cursor, "discovery address link flags")?;
635 let profile = read_u8(payload, &mut cursor, "discovery address link profile")?;
636 let max_frame_bytes = read_u32(payload, &mut cursor, "discovery address link max frame")?;
637 let compact_header_target_bytes = read_u32(
638 payload,
639 &mut cursor,
640 "discovery address link compact target",
641 )?;
642 let max_side_transport_templates =
643 read_u32(payload, &mut cursor, "discovery address link templates")?;
644 if cursor != payload.len() {
645 return Err(TelemetryError::Unpack("discovery address trailing bytes"));
646 }
647 if hostname.is_empty() || address == 0 {
648 return Err(TelemetryError::Unpack("bad discovery address"));
649 }
650 Ok(AddressAdvertisement {
651 hostname,
652 address,
653 requested_address,
654 mode,
655 state,
656 birth_ms,
657 owner_hash,
658 reachable_endpoints,
659 reachable_network_variables,
660 reachable_timesync_sources,
661 link_capabilities: LinkCapabilities {
662 version,
663 flags,
664 profile,
665 max_frame_bytes,
666 compact_header_target_bytes,
667 max_side_transport_templates,
668 },
669 })
670}
671
672pub fn build_managed_variable_request(
673 sender: &str,
674 timestamp_ms: u64,
675 ty: DataType,
676) -> TelemetryResult<Packet> {
677 Packet::new(
678 DataType::ManagedVariableRequest,
679 &[DataEndpoint::Discovery],
680 sender,
681 timestamp_ms,
682 encode_slice_le(&[ty.as_u32()]),
683 )
684}
685
686pub fn decode_managed_variable_request(pkt: &Packet) -> TelemetryResult<DataType> {
687 if pkt.data_type() != DataType::ManagedVariableRequest {
688 return Err(TelemetryError::InvalidType);
689 }
690 let payload = pkt.payload();
691 if payload.len() != 4 {
692 return Err(TelemetryError::Unpack("managed variable request width"));
693 }
694 let raw = u32::from_le_bytes(payload.try_into().expect("4-byte payload"));
695 try_enum_from_u32(raw).ok_or(TelemetryError::Unpack("bad managed variable data type"))
696}
697
698pub fn decode_discovery_timesync_sources(pkt: &Packet) -> TelemetryResult<Vec<String>> {
700 if pkt.data_type() != DataType::DiscoveryTimeSyncSources {
701 return Err(TelemetryError::InvalidType);
702 }
703 decode_discovery_timesync_sources_payload(pkt.payload())
704}
705
706pub fn decode_discovery_timesync_sources_payload(payload: &[u8]) -> TelemetryResult<Vec<String>> {
708 if payload.len() < 4 {
709 return Err(TelemetryError::Unpack("discovery timesync source count"));
710 }
711
712 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
713 let mut cursor = 4usize;
714 let mut out = bounded_wire_vec(
715 payload, cursor, count, 4, "discovery timesync source count",
716 )?;
717
718 for _ in 0..count {
719 if payload.len().saturating_sub(cursor) < 4 {
720 return Err(TelemetryError::Unpack("discovery timesync source len"));
721 }
722 let len = u32::from_le_bytes(payload[cursor..cursor + 4].try_into().expect("4-byte len"))
723 as usize;
724 cursor += 4;
725 if payload.len().saturating_sub(cursor) < len {
726 return Err(TelemetryError::Unpack("discovery timesync source bytes"));
727 }
728 let raw = &payload[cursor..cursor + len];
729 cursor += len;
730 let source = core::str::from_utf8(raw)
731 .map_err(|_| TelemetryError::Unpack("discovery timesync source utf8"))?;
732 if !source.is_empty() {
733 out.push(source.to_string());
734 }
735 }
736
737 if cursor != payload.len() {
738 return Err(TelemetryError::Unpack("discovery timesync trailing bytes"));
739 }
740
741 out.sort_unstable();
742 out.dedup();
743 Ok(out)
744}
745
746pub fn build_discovery_topology(
748 sender: &str,
749 timestamp_ms: u64,
750 boards: &[TopologyBoardNode],
751) -> TelemetryResult<Packet> {
752 let mut payload = Vec::new();
753 let mut normalized = boards.to_vec();
754 normalize_topology_boards(&mut normalized);
755
756 payload.extend_from_slice(&(normalized.len() as u32).to_le_bytes());
757 for board in normalized {
758 let sender_bytes = board.sender_id.as_bytes();
759 let sender_len = u32::try_from(sender_bytes.len())
760 .map_err(|_| TelemetryError::Pack("discovery topology sender id too long"))?;
761 payload.extend_from_slice(&sender_len.to_le_bytes());
762 payload.extend_from_slice(sender_bytes);
763
764 payload.extend_from_slice(&(board.reachable_endpoints.len() as u32).to_le_bytes());
765 for ep in board.reachable_endpoints {
766 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
767 }
768
769 payload.extend_from_slice(&(board.reachable_timesync_sources.len() as u32).to_le_bytes());
770 for source in board.reachable_timesync_sources {
771 let bytes = source.as_bytes();
772 let len = u32::try_from(bytes.len())
773 .map_err(|_| TelemetryError::Pack("discovery topology source id too long"))?;
774 payload.extend_from_slice(&len.to_le_bytes());
775 payload.extend_from_slice(bytes);
776 }
777
778 payload.extend_from_slice(&(board.connections.len() as u32).to_le_bytes());
779 for peer in board.connections {
780 let bytes = peer.as_bytes();
781 let len = u32::try_from(bytes.len())
782 .map_err(|_| TelemetryError::Pack("discovery topology connection id too long"))?;
783 payload.extend_from_slice(&len.to_le_bytes());
784 payload.extend_from_slice(bytes);
785 }
786 }
787
788 Packet::new(
789 DataType::DiscoveryTopology,
790 &[DataEndpoint::Discovery],
791 sender,
792 timestamp_ms,
793 payload.into(),
794 )
795}
796
797#[derive(Debug, Clone, PartialEq, Eq)]
800pub struct DiscoveryTopologyUpdate {
801 pub incremental: bool,
802 pub boards: Vec<TopologyBoardNode>,
803 pub removed: Vec<String>,
804}
805
806pub fn topology_delta(
808 previous: &[TopologyBoardNode],
809 current: &[TopologyBoardNode],
810) -> (Vec<TopologyBoardNode>, Vec<String>) {
811 let boards = current
812 .iter()
813 .filter(|node| previous.iter().find(|old| old.sender_id == node.sender_id) != Some(*node))
814 .cloned()
815 .collect();
816 let removed = previous
817 .iter()
818 .filter(|old| !current.iter().any(|node| node.sender_id == old.sender_id))
819 .map(|old| old.sender_id.clone())
820 .collect();
821 (boards, removed)
822}
823
824pub fn build_discovery_topology_delta(
826 sender: &str,
827 timestamp_ms: u64,
828 boards: &[TopologyBoardNode],
829 removed: &[String],
830) -> TelemetryResult<Packet> {
831 let full = build_discovery_topology(sender, timestamp_ms, boards)?;
832 let mut payload = Vec::with_capacity(full.payload().len() + 8 + removed.len() * 8);
833 payload.extend_from_slice(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes());
834 payload.extend_from_slice(full.payload());
835 payload.extend_from_slice(&(removed.len() as u32).to_le_bytes());
836 for sender_id in removed {
837 encode_string(&mut payload, sender_id)?;
838 }
839 Packet::new(
840 DataType::DiscoveryTopology,
841 &[DataEndpoint::Discovery],
842 sender,
843 timestamp_ms,
844 payload.into(),
845 )
846}
847
848fn decode_string(
849 payload: &[u8],
850 cursor: &mut usize,
851 label: &'static str,
852) -> TelemetryResult<String> {
853 if payload.len().saturating_sub(*cursor) < 4 {
854 return Err(TelemetryError::Unpack(label));
855 }
856 let len = u32::from_le_bytes(
857 payload[*cursor..*cursor + 4]
858 .try_into()
859 .expect("4-byte len"),
860 ) as usize;
861 *cursor += 4;
862 if payload.len().saturating_sub(*cursor) < len {
863 return Err(TelemetryError::Unpack(label));
864 }
865 let raw = &payload[*cursor..*cursor + len];
866 *cursor += len;
867 core::str::from_utf8(raw)
868 .map(|s| s.to_string())
869 .map_err(|_| TelemetryError::Unpack(label))
870}
871
872pub fn decode_discovery_topology(pkt: &Packet) -> TelemetryResult<Vec<TopologyBoardNode>> {
874 if pkt.data_type() != DataType::DiscoveryTopology {
875 return Err(TelemetryError::InvalidType);
876 }
877 Ok(decode_discovery_topology_update(pkt)?.boards)
878}
879
880pub fn decode_discovery_topology_update(pkt: &Packet) -> TelemetryResult<DiscoveryTopologyUpdate> {
882 if pkt.data_type() != DataType::DiscoveryTopology {
883 return Err(TelemetryError::InvalidType);
884 }
885 let payload = pkt.payload();
886 if payload.len() < 4 {
887 return Err(TelemetryError::Unpack("discovery topology board count"));
888 }
889 let first = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count"));
890 if first != DISCOVERY_TOPOLOGY_DELTA_MARKER {
891 return Ok(DiscoveryTopologyUpdate {
892 incremental: false,
893 boards: decode_discovery_topology_payload(payload)?,
894 removed: Vec::new(),
895 });
896 }
897 if payload.len() < 8 {
898 return Err(TelemetryError::Unpack("discovery topology delta count"));
899 }
900 let count = u32::from_le_bytes(payload[4..8].try_into().expect("4-byte count")) as usize;
901 let mut cursor = 8usize;
902 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
903 if payload.len().saturating_sub(cursor) < 4 {
904 return Err(TelemetryError::Unpack("discovery topology removal count"));
905 }
906 let removed_count = u32::from_le_bytes(
907 payload[cursor..cursor + 4]
908 .try_into()
909 .expect("4-byte count"),
910 ) as usize;
911 cursor += 4;
912 let mut removed = bounded_wire_vec(
913 payload, cursor, removed_count, 4, "discovery topology removal count",
914 )?;
915 for _ in 0..removed_count {
916 removed.push(decode_string(
917 payload,
918 &mut cursor,
919 "discovery topology removed sender id",
920 )?);
921 }
922 if cursor != payload.len() {
923 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
924 }
925 normalize_topology_boards(&mut boards);
926 removed.sort_unstable();
927 removed.dedup();
928 Ok(DiscoveryTopologyUpdate {
929 incremental: true,
930 boards,
931 removed,
932 })
933}
934
935pub fn decode_discovery_topology_payload(
937 payload: &[u8],
938) -> TelemetryResult<Vec<TopologyBoardNode>> {
939 if payload.len() < 4 {
940 return Err(TelemetryError::Unpack("discovery topology board count"));
941 }
942
943 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
944 let mut cursor = 4usize;
945 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
946
947 if cursor != payload.len() {
948 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
949 }
950
951 normalize_topology_boards(&mut boards);
952 Ok(boards)
953}
954
955fn decode_topology_boards(
956 payload: &[u8],
957 cursor: &mut usize,
958 count: usize,
959) -> TelemetryResult<Vec<TopologyBoardNode>> {
960 let mut boards = bounded_wire_vec(
962 payload, *cursor, count, 16, "discovery topology board count",
963 )?;
964
965 for _ in 0..count {
966 let sender_id = decode_string(payload, cursor, "discovery topology sender id")?;
967
968 if payload.len().saturating_sub(*cursor) < 4 {
969 return Err(TelemetryError::Unpack("discovery topology endpoint count"));
970 }
971 let endpoint_count = u32::from_le_bytes(
972 payload[*cursor..*cursor + 4]
973 .try_into()
974 .expect("4-byte count"),
975 ) as usize;
976 *cursor += 4;
977 let mut reachable_endpoints = bounded_wire_vec(
978 payload, *cursor, endpoint_count, 4, "discovery topology endpoint count",
979 )?;
980 for _ in 0..endpoint_count {
981 if payload.len().saturating_sub(*cursor) < 4 {
982 return Err(TelemetryError::Unpack("discovery topology endpoint"));
983 }
984 let raw =
985 u32::from_le_bytes(payload[*cursor..*cursor + 4].try_into().expect("4-byte ep"));
986 *cursor += 4;
987 let ep =
988 try_enum_from_u32(raw).ok_or(TelemetryError::Unpack("bad discovery endpoint"))?;
989 if !is_discovery_endpoint(ep) {
990 reachable_endpoints.push(ep);
991 }
992 }
993
994 if payload.len().saturating_sub(*cursor) < 4 {
995 return Err(TelemetryError::Unpack(
996 "discovery topology timesync source count",
997 ));
998 }
999 let source_count = u32::from_le_bytes(
1000 payload[*cursor..*cursor + 4]
1001 .try_into()
1002 .expect("4-byte count"),
1003 ) as usize;
1004 *cursor += 4;
1005 let mut reachable_timesync_sources = bounded_wire_vec(
1006 payload, *cursor, source_count, 4,
1007 "discovery topology timesync source count",
1008 )?;
1009 for _ in 0..source_count {
1010 let source = decode_string(payload, cursor, "discovery topology timesync source")?;
1011 if !source.is_empty() {
1012 reachable_timesync_sources.push(source);
1013 }
1014 }
1015
1016 if payload.len().saturating_sub(*cursor) < 4 {
1017 return Err(TelemetryError::Unpack(
1018 "discovery topology connection count",
1019 ));
1020 }
1021 let connection_count = u32::from_le_bytes(
1022 payload[*cursor..*cursor + 4]
1023 .try_into()
1024 .expect("4-byte count"),
1025 ) as usize;
1026 *cursor += 4;
1027 let mut connections = bounded_wire_vec(
1028 payload, *cursor, connection_count, 4,
1029 "discovery topology connection count",
1030 )?;
1031 for _ in 0..connection_count {
1032 let peer = decode_string(payload, cursor, "discovery topology connection")?;
1033 if !peer.is_empty() {
1034 connections.push(peer);
1035 }
1036 }
1037
1038 boards.push(TopologyBoardNode {
1039 sender_id,
1040 reachable_endpoints,
1041 reachable_timesync_sources,
1042 connections,
1043 });
1044 }
1045
1046 Ok(boards)
1047}
1048
1049fn encode_string(payload: &mut Vec<u8>, value: &str) -> TelemetryResult<()> {
1050 let len = u32::try_from(value.len())
1051 .map_err(|_| TelemetryError::Pack("discovery schema string too long"))?;
1052 payload.extend_from_slice(&len.to_le_bytes());
1053 payload.extend_from_slice(value.as_bytes());
1054 Ok(())
1055}
1056
1057fn read_u8(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u8> {
1058 if payload.len().saturating_sub(*cursor) < 1 {
1059 return Err(TelemetryError::Unpack(label));
1060 }
1061 let out = payload[*cursor];
1062 *cursor += 1;
1063 Ok(out)
1064}
1065
1066fn read_u32(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u32> {
1067 if payload.len().saturating_sub(*cursor) < 4 {
1068 return Err(TelemetryError::Unpack(label));
1069 }
1070 let out = u32::from_le_bytes(
1071 payload[*cursor..*cursor + 4]
1072 .try_into()
1073 .expect("4-byte u32"),
1074 );
1075 *cursor += 4;
1076 Ok(out)
1077}
1078
1079#[cfg(not(feature = "std"))]
1080fn skip_string(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<()> {
1081 let len = read_u32(payload, cursor, label)? as usize;
1082 if payload.len().saturating_sub(*cursor) < len {
1083 return Err(TelemetryError::Unpack(label));
1084 }
1085 *cursor += len;
1086 Ok(())
1087}
1088
1089pub fn build_discovery_schema(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
1091 #[cfg(feature = "std")]
1092 return build_discovery_schema_from_owned_snapshot(sender, timestamp_ms, export_schema());
1093 #[cfg(not(feature = "std"))]
1094 return Packet::new(
1095 DataType::DiscoverySchema,
1096 &[DataEndpoint::Discovery],
1097 sender,
1098 timestamp_ms,
1099 crate::config::encode_embedded_schema_payload().into(),
1100 );
1101}
1102
1103pub fn elect_discovery_master(local_sender: &str, boards: &[TopologyBoardNode]) -> String {
1111 let mut nodes: BTreeMap<String, Vec<String>> = BTreeMap::new();
1112 for board in boards {
1113 nodes.entry(board.sender_id.clone()).or_default();
1114 for peer in board.connections.iter() {
1115 nodes
1116 .entry(peer.clone())
1117 .or_default()
1118 .push(board.sender_id.clone());
1119 nodes
1120 .entry(board.sender_id.clone())
1121 .or_default()
1122 .push(peer.clone());
1123 }
1124 }
1125 nodes.entry(local_sender.to_string()).or_default();
1126 for peers in nodes.values_mut() {
1127 peers.sort_unstable();
1128 peers.dedup();
1129 }
1130
1131 let mut best_sender = local_sender.to_string();
1132 let mut best_unreachable = usize::MAX;
1133 let mut best_max_hops = usize::MAX;
1134 let mut best_total_hops = usize::MAX;
1135
1136 for sender in nodes.keys() {
1137 let mut frontier = vec![sender.clone()];
1138 let mut seen: BTreeMap<String, usize> = BTreeMap::new();
1139 seen.insert(sender.clone(), 0);
1140 let mut idx = 0;
1141 while idx < frontier.len() {
1142 let cur = frontier[idx].clone();
1143 idx += 1;
1144 let cur_dist = seen[&cur];
1145 if let Some(peers) = nodes.get(&cur) {
1146 for peer in peers {
1147 if !seen.contains_key(peer) {
1148 seen.insert(peer.clone(), cur_dist + 1);
1149 frontier.push(peer.clone());
1150 }
1151 }
1152 }
1153 }
1154
1155 let unreachable = nodes.len().saturating_sub(seen.len());
1156 let max_hops = seen.values().copied().max().unwrap_or(0);
1157 let total_hops = seen.values().copied().sum();
1158 let better = unreachable < best_unreachable
1159 || (unreachable == best_unreachable && max_hops < best_max_hops)
1160 || (unreachable == best_unreachable
1161 && max_hops == best_max_hops
1162 && total_hops < best_total_hops)
1163 || (unreachable == best_unreachable
1164 && max_hops == best_max_hops
1165 && total_hops == best_total_hops
1166 && sender < &best_sender);
1167 if better {
1168 best_sender = sender.clone();
1169 best_unreachable = unreachable;
1170 best_max_hops = max_hops;
1171 best_total_hops = total_hops;
1172 }
1173 }
1174
1175 best_sender
1176}
1177
1178pub fn build_discovery_schema_from_snapshot(
1180 sender: &str,
1181 timestamp_ms: u64,
1182 schema: RuntimeSchemaSnapshot,
1183) -> TelemetryResult<Packet> {
1184 let mut payload = Vec::new();
1185 payload.extend_from_slice(&3u32.to_le_bytes());
1186
1187 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1188 for ep in schema.endpoints {
1189 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1190 payload.push(ep.link_local_only as u8);
1191 encode_string(&mut payload, ep.name)?;
1192 #[cfg(feature = "std")]
1193 encode_string(&mut payload, ep.description)?;
1194 #[cfg(not(feature = "std"))]
1195 encode_string(&mut payload, "")?;
1196 }
1197
1198 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1199 for ty in schema.types {
1200 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1201 encode_string(&mut payload, ty.name)?;
1202 #[cfg(feature = "std")]
1203 encode_string(&mut payload, ty.description)?;
1204 #[cfg(not(feature = "std"))]
1205 encode_string(&mut payload, "")?;
1206 match ty.element {
1207 MessageElement::Static(count, data_type, class) => {
1208 payload.push(0);
1209 payload.extend_from_slice(&(count as u32).to_le_bytes());
1210 payload.push(message_data_type_code(data_type));
1211 payload.push(message_class_code(class));
1212 }
1213 MessageElement::Dynamic(data_type, class) => {
1214 payload.push(1);
1215 payload.extend_from_slice(&0u32.to_le_bytes());
1216 payload.push(message_data_type_code(data_type));
1217 payload.push(message_class_code(class));
1218 }
1219 }
1220 payload.push(reliable_code(ty.reliable));
1221 payload.push(ty.priority);
1222 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1223 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1224 for ep in ty.endpoints {
1225 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1226 }
1227 }
1228
1229 Packet::new(
1230 DataType::DiscoverySchema,
1231 &[DataEndpoint::Discovery],
1232 sender,
1233 timestamp_ms,
1234 payload.into(),
1235 )
1236}
1237
1238pub fn build_discovery_schema_from_owned_snapshot(
1240 sender: &str,
1241 timestamp_ms: u64,
1242 schema: OwnedRuntimeSchemaSnapshot,
1243) -> TelemetryResult<Packet> {
1244 let mut payload = Vec::new();
1245 payload.extend_from_slice(&3u32.to_le_bytes());
1246
1247 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1248 for ep in schema.endpoints {
1249 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1250 payload.push(ep.link_local_only as u8);
1251 encode_string(&mut payload, &ep.name)?;
1252 encode_string(&mut payload, "")?;
1256 }
1257
1258 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1259 for ty in schema.types {
1260 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1261 encode_string(&mut payload, &ty.name)?;
1262 encode_string(&mut payload, "")?;
1263 match ty.element {
1264 MessageElement::Static(count, data_type, class) => {
1265 payload.push(0);
1266 payload.extend_from_slice(&(count as u32).to_le_bytes());
1267 payload.push(message_data_type_code(data_type));
1268 payload.push(message_class_code(class));
1269 }
1270 MessageElement::Dynamic(data_type, class) => {
1271 payload.push(1);
1272 payload.extend_from_slice(&0u32.to_le_bytes());
1273 payload.push(message_data_type_code(data_type));
1274 payload.push(message_class_code(class));
1275 }
1276 }
1277 payload.push(reliable_code(ty.reliable));
1278 payload.push(ty.priority);
1279 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1280 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1281 for ep in ty.endpoints {
1282 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1283 }
1284 }
1285
1286 Packet::new(
1287 DataType::DiscoverySchema,
1288 &[DataEndpoint::Discovery],
1289 sender,
1290 timestamp_ms,
1291 payload.into(),
1292 )
1293}
1294
1295pub fn decode_discovery_schema(pkt: &Packet) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1297 if pkt.data_type() != DataType::DiscoverySchema {
1298 return Err(TelemetryError::InvalidType);
1299 }
1300 decode_discovery_schema_payload(pkt.payload())
1301}
1302
1303pub fn decode_discovery_schema_payload(
1305 payload: &[u8],
1306) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1307 let mut cursor = 0usize;
1308 let version = read_u32(payload, &mut cursor, "discovery schema version")?;
1309 #[cfg(feature = "std")]
1310 if version != 1 && version != 2 && version != 3 {
1311 return Err(TelemetryError::Unpack("discovery schema version"));
1312 }
1313 #[cfg(not(feature = "std"))]
1314 if version != 3 {
1315 return Err(TelemetryError::Unpack("discovery schema version"));
1316 }
1317
1318 let endpoint_count =
1319 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1320 let mut endpoints = bounded_wire_vec(
1321 payload, cursor, endpoint_count, 13, "discovery schema endpoint count",
1322 )?;
1323 for _ in 0..endpoint_count {
1324 let id = DataEndpoint(read_u32(
1325 payload,
1326 &mut cursor,
1327 "discovery schema endpoint id",
1328 )?);
1329 let link_local_only =
1330 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1331 let name = decode_string(payload, &mut cursor, "discovery schema endpoint name")?;
1332 #[cfg(feature = "std")]
1333 let description = if version >= 2 {
1334 decode_string(
1335 payload,
1336 &mut cursor,
1337 "discovery schema endpoint description",
1338 )?
1339 } else {
1340 String::new()
1341 };
1342 #[cfg(not(feature = "std"))]
1343 let description = {
1344 skip_string(
1345 payload,
1346 &mut cursor,
1347 "discovery schema endpoint description",
1348 )?;
1349 String::new()
1350 };
1351 endpoints.push(OwnedEndpointDefinition {
1352 id,
1353 name,
1354 description,
1355 link_local_only,
1356 });
1357 }
1358
1359 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1360 let mut types = bounded_wire_vec(
1361 payload, cursor, type_count, 19, "discovery schema type count",
1362 )?;
1363 for _ in 0..type_count {
1364 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1365 let name = decode_string(payload, &mut cursor, "discovery schema type name")?;
1366 #[cfg(feature = "std")]
1367 let description = if version >= 2 {
1368 decode_string(payload, &mut cursor, "discovery schema type description")?
1369 } else {
1370 String::new()
1371 };
1372 #[cfg(not(feature = "std"))]
1373 let description = {
1374 skip_string(payload, &mut cursor, "discovery schema type description")?;
1375 String::new()
1376 };
1377 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1378 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1379 let data_type = message_data_type_from_code(read_u8(
1380 payload,
1381 &mut cursor,
1382 "discovery schema data type",
1383 )?)
1384 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1385 let class =
1386 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1387 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1388 let element = match element_kind {
1389 0 => MessageElement::Static(count, data_type, class),
1390 1 => MessageElement::Dynamic(data_type, class),
1391 _ => return Err(TelemetryError::Unpack("discovery schema element kind")),
1392 };
1393 let reliable =
1394 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1395 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1396 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1397 #[cfg(feature = "std")]
1398 let e2e_encryption = if version >= 3 {
1399 e2e_encryption_policy_from_code(read_u8(
1400 payload,
1401 &mut cursor,
1402 "discovery schema e2e cryptography",
1403 )?)
1404 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?
1405 } else {
1406 E2eEncryptionPolicy::PreferOff
1407 };
1408 #[cfg(not(feature = "std"))]
1409 let e2e_encryption = e2e_encryption_policy_from_code(read_u8(
1410 payload,
1411 &mut cursor,
1412 "discovery schema e2e cryptography",
1413 )?)
1414 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1415 let endpoint_count =
1416 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1417 let mut type_endpoints = bounded_wire_vec(
1418 payload, cursor, endpoint_count, 4,
1419 "discovery schema type endpoint count",
1420 )?;
1421 for _ in 0..endpoint_count {
1422 type_endpoints.push(DataEndpoint(read_u32(
1423 payload,
1424 &mut cursor,
1425 "discovery schema type endpoint",
1426 )?));
1427 }
1428 types.push(OwnedDataTypeDefinition {
1429 id,
1430 name,
1431 description,
1432 element,
1433 endpoints: type_endpoints,
1434 reliable,
1435 priority,
1436 e2e_encryption,
1437 });
1438 }
1439
1440 if cursor != payload.len() {
1441 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1442 }
1443 Ok(OwnedRuntimeSchemaSnapshot { endpoints, types })
1444}
1445
1446#[cfg(test)]
1447mod bounded_decode_tests {
1448 use super::{
1449 decode_discovery_schema_payload, decode_discovery_timesync_sources_payload,
1450 decode_discovery_topology_payload,
1451 };
1452
1453 #[test]
1454 fn corrupt_timesync_count_is_rejected_without_reserving() {
1455 assert!(decode_discovery_timesync_sources_payload(&u32::MAX.to_le_bytes()).is_err());
1456 }
1457
1458 #[test]
1459 fn corrupt_topology_count_is_rejected_without_reserving() {
1460 assert!(decode_discovery_topology_payload(&u32::MAX.to_le_bytes()).is_err());
1461 }
1462
1463 #[test]
1464 fn corrupt_schema_counts_are_rejected_without_reserving() {
1465 let mut endpoints = 3u32.to_le_bytes().to_vec();
1466 endpoints.extend_from_slice(&u32::MAX.to_le_bytes());
1467 assert!(decode_discovery_schema_payload(&endpoints).is_err());
1468
1469 let mut types = 3u32.to_le_bytes().to_vec();
1470 types.extend_from_slice(&0u32.to_le_bytes());
1471 types.extend_from_slice(&u32::MAX.to_le_bytes());
1472 assert!(decode_discovery_schema_payload(&types).is_err());
1473 }
1474}