Skip to main content

zerodds_dcps/
builtin_topics.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Builtin-Topic-Datentypen — DCPS-API-Sicht (DDS 1.4 §2.2.5).
4//!
5//! Die Spec definiert vier "Builtin-Topics":
6//!
7//! | Topic-Name        | Sample-Typ                       | Quelle |
8//! |-------------------|----------------------------------|--------|
9//! | `DCPSParticipant` | [`ParticipantBuiltinTopicData`]  | SPDP   |
10//! | `DCPSTopic`       | [`TopicBuiltinTopicData`]        | SEDP-Topic-Announce (RTPS 2.5 §9.6.2.2.4) |
11//! | `DCPSPublication` | [`PublicationBuiltinTopicData`]  | SEDP-Pub-Announce |
12//! | `DCPSSubscription`| [`SubscriptionBuiltinTopicData`] | SEDP-Sub-Announce |
13//!
14//! Diese DCPS-Strukturen sind **anwendungsfreundlich**: feste Felder,
15//! kein PL_CDR-Wire-Format. Die wire-Decoder fuer
16//! Pub/Sub/Participant aus dem Crate `zerodds-rtps` werden vom Runtime
17//! aufgerufen und das Ergebnis in die hier definierten DCPS-Typen
18//! konvertiert (siehe `From`-Impls weiter unten), bevor sie ueber den
19//! Builtin-Subscriber an User-Code ausgeliefert werden.
20//!
21//! # `DdsType`-Implementation
22//!
23//! Da die Builtin-Topics auch ueber `DataReader::take()` ausgegeben
24//! werden, brauchen sie eine `DdsType`-Impl. Wir verwenden ein
25//! minimales internes Encoding (ZeroDDS-internal-PL_CDR_LE), das
26//! Roundtrip-fest ist — die Builtin-Daten werden niemals "wire" auf
27//! ein anderes Vendor uebertragen, daher reicht das.
28//!
29//! Spec-Referenzen:
30//! - DDS-DCPS 1.4 §2.2.5 (Built-in Topics)
31//! - DDSI-RTPS 2.5 §8.5.4 (SEDP Built-in Endpoints)
32
33extern crate alloc;
34use alloc::string::{String, ToString};
35use alloc::vec::Vec;
36
37use crate::dds_type::{DdsType, DecodeError, EncodeError};
38
39use zerodds_rtps::participant_data as wire_part;
40use zerodds_rtps::publication_data as wire_pub;
41use zerodds_rtps::subscription_data as wire_sub;
42use zerodds_rtps::wire_types::Guid;
43
44// ---------------------------------------------------------------------
45// Topic-Namen (Spec §2.2.5).
46// ---------------------------------------------------------------------
47
48/// Topic-Name `"DCPSParticipant"` (Spec §2.2.5.1).
49pub const TOPIC_NAME_DCPS_PARTICIPANT: &str = "DCPSParticipant";
50/// Topic-Name `"DCPSTopic"` (Spec §2.2.5.2).
51pub const TOPIC_NAME_DCPS_TOPIC: &str = "DCPSTopic";
52/// Topic-Name `"DCPSPublication"` (Spec §2.2.5.3).
53pub const TOPIC_NAME_DCPS_PUBLICATION: &str = "DCPSPublication";
54/// Topic-Name `"DCPSSubscription"` (Spec §2.2.5.4).
55pub const TOPIC_NAME_DCPS_SUBSCRIPTION: &str = "DCPSSubscription";
56
57// ---------------------------------------------------------------------
58// 1) DCPSParticipant
59// ---------------------------------------------------------------------
60
61/// Sample-Typ des `DCPSParticipant`-Builtin-Topics (DDS 1.4 §2.2.5.1).
62///
63/// Repraesentiert einen entdeckten remote `DomainParticipant`.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ParticipantBuiltinTopicData {
66    /// Stabiler Identifier des Participants (16-Byte GUID; entspricht
67    /// `BuiltinTopicKey_t` der Spec — bei uns sind das die letzten
68    /// 16 Bytes der RTPS-GUID).
69    pub key: Guid,
70    /// USER_DATA-QoS-Bytes (Spec §2.2.5.1: `user_data`). Optional im
71    /// SPDP-Beacon — gefuellt aus `DomainParticipantQos::user_data`,
72    /// wire-encoded als PID_USER_DATA (DDSI-RTPS §9.6.3.2).
73    pub user_data: Vec<u8>,
74}
75
76impl ParticipantBuiltinTopicData {
77    /// Konstruiert aus dem Wire-Datentyp (`zerodds-rtps`).
78    #[must_use]
79    pub fn from_wire(w: &wire_part::ParticipantBuiltinTopicData) -> Self {
80        Self {
81            key: w.guid,
82            user_data: w.user_data.clone(),
83        }
84    }
85}
86
87impl DdsType for ParticipantBuiltinTopicData {
88    const TYPE_NAME: &'static str = "DDS::ParticipantBuiltinTopicData";
89    const HAS_KEY: bool = true;
90    const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(16);
91
92    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
93        out.extend_from_slice(&self.key.to_bytes());
94        encode_bytes_le(&self.user_data, out)
95    }
96
97    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
98        let mut r = Reader::new(bytes);
99        let key = r.read_guid()?;
100        let user_data = r.read_bytes()?;
101        Ok(Self { key, user_data })
102    }
103
104    fn encode_key_holder_be(&self, holder: &mut crate::dds_type::PlainCdr2BeKeyHolder) {
105        holder.write_bytes(&self.key.to_bytes());
106    }
107}
108
109// ---------------------------------------------------------------------
110// 2) DCPSTopic
111// ---------------------------------------------------------------------
112
113/// Sample-Typ des `DCPSTopic`-Builtin-Topics (DDS 1.4 §2.2.5.2).
114///
115/// Minimaler Subset (Topic-Name + Type-Name + Durability +
116/// Reliability), aus dem End-User-Tooling die discovered Topics
117/// rendern kann. Volle Topic-QoS-Tabelle (DDS 1.4 §2.2.5.2) bleibt
118/// optionale Erweiterung.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TopicBuiltinTopicData {
121    /// Stabile Identitaet des Topic-Eintrags (synthetisch aus Hash
122    /// von Topic-Name + Type-Name; siehe [`Self::synthesize_key`]).
123    pub key: Guid,
124    /// Topic-Name (z.B. `"ChatterTopic"`).
125    pub name: String,
126    /// IDL-Type-Name.
127    pub type_name: String,
128    /// Durability-QoS-Kind.
129    pub durability: zerodds_qos::DurabilityKind,
130    /// Reliability-QoS-Kind.
131    pub reliability: zerodds_qos::ReliabilityKind,
132}
133
134impl TopicBuiltinTopicData {
135    /// Erzeugt einen synthetischen GUID-Key aus Topic + Type-Name.
136    /// Stabil: derselbe Topic+Type ergibt denselben Key. Damit das
137    /// `DCPSTopic`-Builtin-Topic Idempotent-Updates statt Insert-pro-
138    /// Endpoint liefert (Spec §2.2.5.2).
139    #[must_use]
140    pub fn synthesize_key(topic: &str, type_name: &str) -> Guid {
141        // FNV-1a 64-bit, doppelt — deterministisch, no_std,
142        // ausreichend kollisionssicher fuer einige tausend Topics.
143        let h1 = fnv1a64(topic.as_bytes(), 0xcbf2_9ce4_8422_2325);
144        let h2 = fnv1a64(type_name.as_bytes(), h1);
145        let mut bytes = [0u8; 16];
146        bytes[0..8].copy_from_slice(&h1.to_le_bytes());
147        bytes[8..16].copy_from_slice(&h2.to_le_bytes());
148        Guid::from_bytes(bytes)
149    }
150
151    /// Konstruiert aus einem entdeckten Publication-Wire-Datentyp.
152    #[must_use]
153    pub fn from_publication(w: &wire_pub::PublicationBuiltinTopicData) -> Self {
154        Self {
155            key: Self::synthesize_key(&w.topic_name, &w.type_name),
156            name: w.topic_name.clone(),
157            type_name: w.type_name.clone(),
158            durability: w.durability,
159            reliability: w.reliability.kind,
160        }
161    }
162
163    /// Konstruiert aus einem entdeckten Subscription-Wire-Datentyp.
164    #[must_use]
165    pub fn from_subscription(w: &wire_sub::SubscriptionBuiltinTopicData) -> Self {
166        Self {
167            key: Self::synthesize_key(&w.topic_name, &w.type_name),
168            name: w.topic_name.clone(),
169            type_name: w.type_name.clone(),
170            durability: w.durability,
171            reliability: w.reliability.kind,
172        }
173    }
174}
175
176impl DdsType for TopicBuiltinTopicData {
177    const TYPE_NAME: &'static str = "DDS::TopicBuiltinTopicData";
178    const HAS_KEY: bool = true;
179    const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(16);
180
181    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
182        out.extend_from_slice(&self.key.to_bytes());
183        encode_string_le(&self.name, out)?;
184        encode_string_le(&self.type_name, out)?;
185        out.push(self.durability as u8);
186        out.push(self.reliability as u8);
187        Ok(())
188    }
189
190    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
191        let mut r = Reader::new(bytes);
192        let key = r.read_guid()?;
193        let name = r.read_string()?;
194        let type_name = r.read_string()?;
195        let durability = decode_durability(r.read_u8()?)?;
196        let reliability = decode_reliability(r.read_u8()?)?;
197        Ok(Self {
198            key,
199            name,
200            type_name,
201            durability,
202            reliability,
203        })
204    }
205
206    fn encode_key_holder_be(&self, holder: &mut crate::dds_type::PlainCdr2BeKeyHolder) {
207        holder.write_bytes(&self.key.to_bytes());
208    }
209}
210
211// ---------------------------------------------------------------------
212// 3) DCPSPublication
213// ---------------------------------------------------------------------
214
215/// Sample-Typ des `DCPSPublication`-Builtin-Topics (DDS 1.4 §2.2.5.3).
216///
217/// Repraesentiert einen entdeckten remote `DataWriter`.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct PublicationBuiltinTopicData {
220    /// Identitaet des Endpoints (= Writer-GUID).
221    pub key: Guid,
222    /// GUID des Participants, dem der Writer gehoert.
223    pub participant_key: Guid,
224    /// Topic-Name.
225    pub topic_name: String,
226    /// IDL-Type-Name.
227    pub type_name: String,
228    /// Durability-QoS-Kind.
229    pub durability: zerodds_qos::DurabilityKind,
230    /// Reliability-QoS-Kind.
231    pub reliability: zerodds_qos::ReliabilityKind,
232    /// Ownership-QoS-Kind.
233    pub ownership: zerodds_qos::OwnershipKind,
234    /// Ownership-Strength.
235    pub ownership_strength: i32,
236    /// Liveliness-Lease (Sekunden, gerundet).
237    pub liveliness_lease_seconds: i32,
238    /// Deadline-Period (Sekunden, gerundet).
239    pub deadline_seconds: i32,
240    /// Lifespan-Duration (Sekunden, gerundet).
241    pub lifespan_seconds: i32,
242    /// Partition-Liste.
243    pub partition: Vec<String>,
244}
245
246impl PublicationBuiltinTopicData {
247    /// Konstruiert aus dem Wire-Datentyp.
248    #[must_use]
249    pub fn from_wire(w: &wire_pub::PublicationBuiltinTopicData) -> Self {
250        Self {
251            key: w.key,
252            participant_key: w.participant_key,
253            topic_name: w.topic_name.clone(),
254            type_name: w.type_name.clone(),
255            durability: w.durability,
256            reliability: w.reliability.kind,
257            ownership: w.ownership,
258            ownership_strength: w.ownership_strength,
259            liveliness_lease_seconds: w.liveliness.lease_duration.seconds,
260            deadline_seconds: w.deadline.period.seconds,
261            lifespan_seconds: w.lifespan.duration.seconds,
262            partition: w.partition.clone(),
263        }
264    }
265}
266
267impl DdsType for PublicationBuiltinTopicData {
268    const TYPE_NAME: &'static str = "DDS::PublicationBuiltinTopicData";
269    const HAS_KEY: bool = true;
270    const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(16);
271
272    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
273        out.extend_from_slice(&self.key.to_bytes());
274        out.extend_from_slice(&self.participant_key.to_bytes());
275        encode_string_le(&self.topic_name, out)?;
276        encode_string_le(&self.type_name, out)?;
277        out.push(self.durability as u8);
278        out.push(self.reliability as u8);
279        out.push(self.ownership as u8);
280        out.extend_from_slice(&self.ownership_strength.to_le_bytes());
281        out.extend_from_slice(&self.liveliness_lease_seconds.to_le_bytes());
282        out.extend_from_slice(&self.deadline_seconds.to_le_bytes());
283        out.extend_from_slice(&self.lifespan_seconds.to_le_bytes());
284        encode_string_seq_le(&self.partition, out)?;
285        Ok(())
286    }
287
288    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
289        let mut r = Reader::new(bytes);
290        let key = r.read_guid()?;
291        let participant_key = r.read_guid()?;
292        let topic_name = r.read_string()?;
293        let type_name = r.read_string()?;
294        let durability = decode_durability(r.read_u8()?)?;
295        let reliability = decode_reliability(r.read_u8()?)?;
296        let ownership = decode_ownership(r.read_u8()?)?;
297        let ownership_strength = r.read_i32()?;
298        let liveliness_lease_seconds = r.read_i32()?;
299        let deadline_seconds = r.read_i32()?;
300        let lifespan_seconds = r.read_i32()?;
301        let partition = r.read_string_seq()?;
302        Ok(Self {
303            key,
304            participant_key,
305            topic_name,
306            type_name,
307            durability,
308            reliability,
309            ownership,
310            ownership_strength,
311            liveliness_lease_seconds,
312            deadline_seconds,
313            lifespan_seconds,
314            partition,
315        })
316    }
317
318    fn encode_key_holder_be(&self, holder: &mut crate::dds_type::PlainCdr2BeKeyHolder) {
319        holder.write_bytes(&self.key.to_bytes());
320    }
321}
322
323// ---------------------------------------------------------------------
324// 4) DCPSSubscription
325// ---------------------------------------------------------------------
326
327/// Sample-Typ des `DCPSSubscription`-Builtin-Topics (DDS 1.4 §2.2.5.4).
328///
329/// Repraesentiert einen entdeckten remote `DataReader`.
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct SubscriptionBuiltinTopicData {
332    /// Identitaet des Endpoints (= Reader-GUID).
333    pub key: Guid,
334    /// GUID des Participants, dem der Reader gehoert.
335    pub participant_key: Guid,
336    /// Topic-Name.
337    pub topic_name: String,
338    /// IDL-Type-Name.
339    pub type_name: String,
340    /// Durability-QoS-Kind.
341    pub durability: zerodds_qos::DurabilityKind,
342    /// Reliability-QoS-Kind.
343    pub reliability: zerodds_qos::ReliabilityKind,
344    /// Ownership-QoS-Kind.
345    pub ownership: zerodds_qos::OwnershipKind,
346    /// Liveliness-Lease (Sekunden).
347    pub liveliness_lease_seconds: i32,
348    /// Deadline-Period (Sekunden).
349    pub deadline_seconds: i32,
350    /// Partition-Liste.
351    pub partition: Vec<String>,
352}
353
354impl SubscriptionBuiltinTopicData {
355    /// Konstruiert aus dem Wire-Datentyp.
356    #[must_use]
357    pub fn from_wire(w: &wire_sub::SubscriptionBuiltinTopicData) -> Self {
358        Self {
359            key: w.key,
360            participant_key: w.participant_key,
361            topic_name: w.topic_name.clone(),
362            type_name: w.type_name.clone(),
363            durability: w.durability,
364            reliability: w.reliability.kind,
365            ownership: w.ownership,
366            liveliness_lease_seconds: w.liveliness.lease_duration.seconds,
367            deadline_seconds: w.deadline.period.seconds,
368            partition: w.partition.clone(),
369        }
370    }
371}
372
373impl DdsType for SubscriptionBuiltinTopicData {
374    const TYPE_NAME: &'static str = "DDS::SubscriptionBuiltinTopicData";
375    const HAS_KEY: bool = true;
376    const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(16);
377
378    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
379        out.extend_from_slice(&self.key.to_bytes());
380        out.extend_from_slice(&self.participant_key.to_bytes());
381        encode_string_le(&self.topic_name, out)?;
382        encode_string_le(&self.type_name, out)?;
383        out.push(self.durability as u8);
384        out.push(self.reliability as u8);
385        out.push(self.ownership as u8);
386        out.extend_from_slice(&self.liveliness_lease_seconds.to_le_bytes());
387        out.extend_from_slice(&self.deadline_seconds.to_le_bytes());
388        encode_string_seq_le(&self.partition, out)?;
389        Ok(())
390    }
391
392    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
393        let mut r = Reader::new(bytes);
394        let key = r.read_guid()?;
395        let participant_key = r.read_guid()?;
396        let topic_name = r.read_string()?;
397        let type_name = r.read_string()?;
398        let durability = decode_durability(r.read_u8()?)?;
399        let reliability = decode_reliability(r.read_u8()?)?;
400        let ownership = decode_ownership(r.read_u8()?)?;
401        let liveliness_lease_seconds = r.read_i32()?;
402        let deadline_seconds = r.read_i32()?;
403        let partition = r.read_string_seq()?;
404        Ok(Self {
405            key,
406            participant_key,
407            topic_name,
408            type_name,
409            durability,
410            reliability,
411            ownership,
412            liveliness_lease_seconds,
413            deadline_seconds,
414            partition,
415        })
416    }
417
418    fn encode_key_holder_be(&self, holder: &mut crate::dds_type::PlainCdr2BeKeyHolder) {
419        holder.write_bytes(&self.key.to_bytes());
420    }
421}
422
423// ---------------------------------------------------------------------
424// Internes ZeroDDS-Encoding (kein Wire — bleibt prozess-intern).
425// ---------------------------------------------------------------------
426
427fn encode_string_le(s: &str, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
428    let len = u32::try_from(s.len()).map_err(|_| EncodeError::Invalid {
429        what: "builtin-topic string > 4 GiB",
430    })?;
431    out.extend_from_slice(&len.to_le_bytes());
432    out.extend_from_slice(s.as_bytes());
433    Ok(())
434}
435
436fn encode_bytes_le(b: &[u8], out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
437    let len = u32::try_from(b.len()).map_err(|_| EncodeError::Invalid {
438        what: "builtin-topic bytes > 4 GiB",
439    })?;
440    out.extend_from_slice(&len.to_le_bytes());
441    out.extend_from_slice(b);
442    Ok(())
443}
444
445fn encode_string_seq_le(
446    seq: &[String],
447    out: &mut Vec<u8>,
448) -> core::result::Result<(), EncodeError> {
449    let len = u32::try_from(seq.len()).map_err(|_| EncodeError::Invalid {
450        what: "builtin-topic seq len > 4 GiB",
451    })?;
452    out.extend_from_slice(&len.to_le_bytes());
453    for s in seq {
454        encode_string_le(s, out)?;
455    }
456    Ok(())
457}
458
459struct Reader<'a> {
460    bytes: &'a [u8],
461    pos: usize,
462}
463
464impl<'a> Reader<'a> {
465    fn new(bytes: &'a [u8]) -> Self {
466        Self { bytes, pos: 0 }
467    }
468
469    fn need(&self, n: usize) -> core::result::Result<(), DecodeError> {
470        if self.pos.saturating_add(n) > self.bytes.len() {
471            return Err(DecodeError::Invalid {
472                what: "builtin-topic truncated",
473            });
474        }
475        Ok(())
476    }
477
478    fn read_u8(&mut self) -> core::result::Result<u8, DecodeError> {
479        self.need(1)?;
480        let v = self.bytes[self.pos];
481        self.pos += 1;
482        Ok(v)
483    }
484
485    fn read_u32(&mut self) -> core::result::Result<u32, DecodeError> {
486        self.need(4)?;
487        let mut a = [0u8; 4];
488        a.copy_from_slice(&self.bytes[self.pos..self.pos + 4]);
489        self.pos += 4;
490        Ok(u32::from_le_bytes(a))
491    }
492
493    fn read_i32(&mut self) -> core::result::Result<i32, DecodeError> {
494        self.read_u32().map(|v| v as i32)
495    }
496
497    fn read_guid(&mut self) -> core::result::Result<Guid, DecodeError> {
498        self.need(16)?;
499        let mut b = [0u8; 16];
500        b.copy_from_slice(&self.bytes[self.pos..self.pos + 16]);
501        self.pos += 16;
502        Ok(Guid::from_bytes(b))
503    }
504
505    fn read_string(&mut self) -> core::result::Result<String, DecodeError> {
506        let len = self.read_u32()? as usize;
507        self.need(len)?;
508        let s = core::str::from_utf8(&self.bytes[self.pos..self.pos + len])
509            .map_err(|_| DecodeError::Invalid {
510                what: "builtin-topic invalid utf8",
511            })?
512            .to_string();
513        self.pos += len;
514        Ok(s)
515    }
516
517    fn read_bytes(&mut self) -> core::result::Result<Vec<u8>, DecodeError> {
518        let len = self.read_u32()? as usize;
519        self.need(len)?;
520        let v = self.bytes[self.pos..self.pos + len].to_vec();
521        self.pos += len;
522        Ok(v)
523    }
524
525    fn read_string_seq(&mut self) -> core::result::Result<Vec<String>, DecodeError> {
526        let n = self.read_u32()? as usize;
527        let mut v = Vec::with_capacity(n);
528        for _ in 0..n {
529            v.push(self.read_string()?);
530        }
531        Ok(v)
532    }
533}
534
535fn decode_durability(b: u8) -> core::result::Result<zerodds_qos::DurabilityKind, DecodeError> {
536    use zerodds_qos::DurabilityKind::*;
537    Ok(match b {
538        0 => Volatile,
539        1 => TransientLocal,
540        2 => Transient,
541        3 => Persistent,
542        _ => {
543            return Err(DecodeError::Invalid {
544                what: "durability kind out of range",
545            });
546        }
547    })
548}
549
550fn decode_reliability(b: u8) -> core::result::Result<zerodds_qos::ReliabilityKind, DecodeError> {
551    use zerodds_qos::ReliabilityKind::*;
552    Ok(match b {
553        1 => BestEffort,
554        2 => Reliable,
555        _ => {
556            return Err(DecodeError::Invalid {
557                what: "reliability kind out of range",
558            });
559        }
560    })
561}
562
563fn decode_ownership(b: u8) -> core::result::Result<zerodds_qos::OwnershipKind, DecodeError> {
564    use zerodds_qos::OwnershipKind::*;
565    Ok(match b {
566        0 => Shared,
567        1 => Exclusive,
568        _ => {
569            return Err(DecodeError::Invalid {
570                what: "ownership kind out of range",
571            });
572        }
573    })
574}
575
576/// FNV-1a 64-bit Hash (no_std, deterministic).
577fn fnv1a64(data: &[u8], seed: u64) -> u64 {
578    let mut h = seed;
579    for &b in data {
580        h ^= u64::from(b);
581        h = h.wrapping_mul(0x0000_0100_0000_01b3);
582    }
583    h
584}
585
586#[cfg(test)]
587#[allow(clippy::expect_used, clippy::unwrap_used)]
588mod tests {
589    use super::*;
590    use zerodds_rtps::wire_types::GuidPrefix;
591
592    fn mk_guid(seed: u8) -> Guid {
593        let mut b = [0u8; 16];
594        for (i, slot) in b.iter_mut().enumerate() {
595            *slot = seed.wrapping_add(i as u8);
596        }
597        Guid::from_bytes(b)
598    }
599
600    #[test]
601    fn participant_dds_type_name_matches_spec() {
602        assert_eq!(
603            <ParticipantBuiltinTopicData as DdsType>::TYPE_NAME,
604            "DDS::ParticipantBuiltinTopicData"
605        );
606    }
607
608    #[test]
609    fn topic_dds_type_name_matches_spec() {
610        assert_eq!(
611            <TopicBuiltinTopicData as DdsType>::TYPE_NAME,
612            "DDS::TopicBuiltinTopicData"
613        );
614    }
615
616    #[test]
617    fn publication_dds_type_name_matches_spec() {
618        assert_eq!(
619            <PublicationBuiltinTopicData as DdsType>::TYPE_NAME,
620            "DDS::PublicationBuiltinTopicData"
621        );
622    }
623
624    #[test]
625    fn subscription_dds_type_name_matches_spec() {
626        assert_eq!(
627            <SubscriptionBuiltinTopicData as DdsType>::TYPE_NAME,
628            "DDS::SubscriptionBuiltinTopicData"
629        );
630    }
631
632    #[test]
633    fn participant_roundtrip() {
634        let p = ParticipantBuiltinTopicData {
635            key: mk_guid(0xA0),
636            user_data: alloc::vec![1, 2, 3],
637        };
638        let mut buf = Vec::new();
639        p.encode(&mut buf).unwrap();
640        let back = ParticipantBuiltinTopicData::decode(&buf).unwrap();
641        assert_eq!(p, back);
642    }
643
644    #[test]
645    fn topic_roundtrip() {
646        let t = TopicBuiltinTopicData {
647            key: TopicBuiltinTopicData::synthesize_key("Chatter", "std::String"),
648            name: "Chatter".to_string(),
649            type_name: "std::String".to_string(),
650            durability: zerodds_qos::DurabilityKind::TransientLocal,
651            reliability: zerodds_qos::ReliabilityKind::Reliable,
652        };
653        let mut buf = Vec::new();
654        t.encode(&mut buf).unwrap();
655        let back = TopicBuiltinTopicData::decode(&buf).unwrap();
656        assert_eq!(t, back);
657    }
658
659    #[test]
660    fn topic_synthesize_key_is_stable_and_distinct() {
661        let a = TopicBuiltinTopicData::synthesize_key("X", "Foo");
662        let a2 = TopicBuiltinTopicData::synthesize_key("X", "Foo");
663        let b = TopicBuiltinTopicData::synthesize_key("X", "Bar");
664        let c = TopicBuiltinTopicData::synthesize_key("Y", "Foo");
665        assert_eq!(a, a2);
666        assert_ne!(a, b);
667        assert_ne!(a, c);
668        assert_ne!(b, c);
669    }
670
671    #[test]
672    fn publication_roundtrip() {
673        let p = PublicationBuiltinTopicData {
674            key: mk_guid(0xB0),
675            participant_key: mk_guid(0xC0),
676            topic_name: "T".to_string(),
677            type_name: "Tt".to_string(),
678            durability: zerodds_qos::DurabilityKind::Volatile,
679            reliability: zerodds_qos::ReliabilityKind::BestEffort,
680            ownership: zerodds_qos::OwnershipKind::Exclusive,
681            ownership_strength: 17,
682            liveliness_lease_seconds: 30,
683            deadline_seconds: 5,
684            lifespan_seconds: 60,
685            partition: alloc::vec!["A".to_string(), "B".to_string()],
686        };
687        let mut buf = Vec::new();
688        p.encode(&mut buf).unwrap();
689        let back = PublicationBuiltinTopicData::decode(&buf).unwrap();
690        assert_eq!(p, back);
691    }
692
693    #[test]
694    fn subscription_roundtrip() {
695        let s = SubscriptionBuiltinTopicData {
696            key: mk_guid(0xD0),
697            participant_key: mk_guid(0xE0),
698            topic_name: "T2".to_string(),
699            type_name: "T2t".to_string(),
700            durability: zerodds_qos::DurabilityKind::Persistent,
701            reliability: zerodds_qos::ReliabilityKind::Reliable,
702            ownership: zerodds_qos::OwnershipKind::Shared,
703            liveliness_lease_seconds: 10,
704            deadline_seconds: 1,
705            partition: alloc::vec![],
706        };
707        let mut buf = Vec::new();
708        s.encode(&mut buf).unwrap();
709        let back = SubscriptionBuiltinTopicData::decode(&buf).unwrap();
710        assert_eq!(s, back);
711    }
712
713    #[test]
714    fn participant_keyhash_is_first_16_bytes_of_guid() {
715        let p = ParticipantBuiltinTopicData {
716            key: mk_guid(7),
717            user_data: alloc::vec![],
718        };
719        let kh = p.compute_key_hash().expect("keyed");
720        assert_eq!(kh, p.key.to_bytes());
721    }
722
723    #[test]
724    fn participant_from_wire_strips_to_guid() {
725        use zerodds_rtps::wire_types::{EntityId, ProtocolVersion, VendorId};
726        let g = Guid::new(GuidPrefix::from_bytes([7; 12]), EntityId::PARTICIPANT);
727        let w = wire_part::ParticipantBuiltinTopicData {
728            guid: g,
729            protocol_version: ProtocolVersion::V2_5,
730            vendor_id: VendorId::ZERODDS,
731            default_unicast_locator: None,
732            default_multicast_locator: None,
733            metatraffic_unicast_locator: None,
734            metatraffic_multicast_locator: None,
735            domain_id: None,
736            builtin_endpoint_set: 0,
737            lease_duration: zerodds_qos::Duration::from_secs(100),
738            user_data: alloc::vec::Vec::new(),
739            properties: Default::default(),
740            identity_token: None,
741            permissions_token: None,
742            identity_status_token: None,
743            sig_algo_info: None,
744            kx_algo_info: None,
745            sym_cipher_algo_info: None,
746        };
747        let dcps = ParticipantBuiltinTopicData::from_wire(&w);
748        assert_eq!(dcps.key, g);
749        assert!(dcps.user_data.is_empty());
750    }
751
752    #[test]
753    fn publication_from_wire_copies_topic_and_type() {
754        let g_w = mk_guid(1);
755        let g_p = mk_guid(2);
756        let w = wire_pub::PublicationBuiltinTopicData {
757            key: g_w,
758            participant_key: g_p,
759            topic_name: "MyT".to_string(),
760            type_name: "MyType".to_string(),
761            durability: zerodds_qos::DurabilityKind::TransientLocal,
762            reliability: zerodds_qos::ReliabilityQosPolicy {
763                kind: zerodds_qos::ReliabilityKind::Reliable,
764                max_blocking_time: zerodds_qos::Duration::from_millis(100),
765            },
766            ownership: zerodds_qos::OwnershipKind::Shared,
767            ownership_strength: 0,
768            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
769            deadline: zerodds_qos::DeadlineQosPolicy::default(),
770            lifespan: zerodds_qos::LifespanQosPolicy::default(),
771            partition: alloc::vec![],
772            user_data: alloc::vec![],
773            topic_data: alloc::vec![],
774            group_data: alloc::vec![],
775            type_information: None,
776            data_representation: alloc::vec![],
777            security_info: None,
778            service_instance_name: None,
779            related_entity_guid: None,
780            topic_aliases: None,
781            type_identifier: zerodds_types::TypeIdentifier::None,
782        };
783        let d = PublicationBuiltinTopicData::from_wire(&w);
784        assert_eq!(d.key, g_w);
785        assert_eq!(d.participant_key, g_p);
786        assert_eq!(d.topic_name, "MyT");
787        assert_eq!(d.type_name, "MyType");
788        assert_eq!(d.durability, zerodds_qos::DurabilityKind::TransientLocal);
789        assert_eq!(d.reliability, zerodds_qos::ReliabilityKind::Reliable);
790    }
791
792    #[test]
793    fn subscription_from_wire_copies_topic_and_type() {
794        let g_r = mk_guid(3);
795        let g_p = mk_guid(4);
796        let w = wire_sub::SubscriptionBuiltinTopicData {
797            key: g_r,
798            participant_key: g_p,
799            topic_name: "S".to_string(),
800            type_name: "St".to_string(),
801            durability: zerodds_qos::DurabilityKind::Volatile,
802            reliability: zerodds_qos::ReliabilityQosPolicy::default(),
803            ownership: zerodds_qos::OwnershipKind::Exclusive,
804            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
805            deadline: zerodds_qos::DeadlineQosPolicy::default(),
806            partition: alloc::vec!["A".to_string()],
807            user_data: alloc::vec![],
808            topic_data: alloc::vec![],
809            group_data: alloc::vec![],
810            type_information: None,
811            data_representation: alloc::vec![],
812            content_filter: None,
813            security_info: None,
814            service_instance_name: None,
815            related_entity_guid: None,
816            topic_aliases: None,
817            type_identifier: zerodds_types::TypeIdentifier::None,
818        };
819        let d = SubscriptionBuiltinTopicData::from_wire(&w);
820        assert_eq!(d.key, g_r);
821        assert_eq!(d.participant_key, g_p);
822        assert_eq!(d.partition, alloc::vec!["A".to_string()]);
823    }
824
825    #[test]
826    fn topic_from_publication_synthesizes_consistent_key() {
827        let g_w = mk_guid(5);
828        let g_p = mk_guid(6);
829        let w = wire_pub::PublicationBuiltinTopicData {
830            key: g_w,
831            participant_key: g_p,
832            topic_name: "Same".to_string(),
833            type_name: "T".to_string(),
834            durability: zerodds_qos::DurabilityKind::Volatile,
835            reliability: zerodds_qos::ReliabilityQosPolicy::default(),
836            ownership: zerodds_qos::OwnershipKind::Shared,
837            ownership_strength: 0,
838            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
839            deadline: zerodds_qos::DeadlineQosPolicy::default(),
840            lifespan: zerodds_qos::LifespanQosPolicy::default(),
841            partition: alloc::vec![],
842            user_data: alloc::vec![],
843            topic_data: alloc::vec![],
844            group_data: alloc::vec![],
845            type_information: None,
846            data_representation: alloc::vec![],
847            security_info: None,
848            service_instance_name: None,
849            related_entity_guid: None,
850            topic_aliases: None,
851            type_identifier: zerodds_types::TypeIdentifier::None,
852        };
853        let t = TopicBuiltinTopicData::from_publication(&w);
854        let expected_key = TopicBuiltinTopicData::synthesize_key("Same", "T");
855        assert_eq!(t.key, expected_key);
856        assert_eq!(t.name, "Same");
857    }
858
859    #[test]
860    fn decode_rejects_truncated() {
861        let bad = alloc::vec![1u8, 2, 3];
862        assert!(ParticipantBuiltinTopicData::decode(&bad).is_err());
863        assert!(TopicBuiltinTopicData::decode(&bad).is_err());
864        assert!(PublicationBuiltinTopicData::decode(&bad).is_err());
865        assert!(SubscriptionBuiltinTopicData::decode(&bad).is_err());
866    }
867
868    #[test]
869    fn decode_rejects_invalid_durability_kind() {
870        // Encode a topic with valid prefix, then patch the durability byte to invalid.
871        let t = TopicBuiltinTopicData {
872            key: mk_guid(0),
873            name: "x".to_string(),
874            type_name: "y".to_string(),
875            durability: zerodds_qos::DurabilityKind::Volatile,
876            reliability: zerodds_qos::ReliabilityKind::Reliable,
877        };
878        let mut buf = Vec::new();
879        t.encode(&mut buf).unwrap();
880        // last 2 bytes are durability (penult.) + reliability (last).
881        let dur_idx = buf.len() - 2;
882        buf[dur_idx] = 99;
883        assert!(TopicBuiltinTopicData::decode(&buf).is_err());
884    }
885
886    #[test]
887    fn decode_rejects_invalid_reliability_kind() {
888        let t = TopicBuiltinTopicData {
889            key: mk_guid(0),
890            name: "x".to_string(),
891            type_name: "y".to_string(),
892            durability: zerodds_qos::DurabilityKind::Volatile,
893            reliability: zerodds_qos::ReliabilityKind::Reliable,
894        };
895        let mut buf = Vec::new();
896        t.encode(&mut buf).unwrap();
897        *buf.last_mut().unwrap() = 99;
898        assert!(TopicBuiltinTopicData::decode(&buf).is_err());
899    }
900
901    #[test]
902    fn decode_rejects_invalid_ownership_kind() {
903        let p = PublicationBuiltinTopicData {
904            key: mk_guid(0),
905            participant_key: mk_guid(1),
906            topic_name: "T".to_string(),
907            type_name: "T".to_string(),
908            durability: zerodds_qos::DurabilityKind::Volatile,
909            reliability: zerodds_qos::ReliabilityKind::Reliable,
910            ownership: zerodds_qos::OwnershipKind::Shared,
911            ownership_strength: 0,
912            liveliness_lease_seconds: 0,
913            deadline_seconds: 0,
914            lifespan_seconds: 0,
915            partition: alloc::vec![],
916        };
917        let mut buf = Vec::new();
918        p.encode(&mut buf).unwrap();
919        // ownership byte is at offset: 16 (key) + 16 (pkey) + 4+1 (T) + 4+1 (T) + 1 (dur) + 1 (rel) = 44
920        // bytes[44] should be ownership.
921        buf[44] = 99;
922        assert!(PublicationBuiltinTopicData::decode(&buf).is_err());
923    }
924}