Skip to main content

zerodds_rtps/
participant_data.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! ParticipantBuiltinTopicData (DDSI-RTPS 2.5 §8.5.4.2).
4//!
5//! Inhalt der SPDP-Beacon-DATA-Submessage. Wird als PL_CDR_LE-encoded
6//! ParameterList in der `serialized_payload` der DATA-Submessage
7//! transportiert (mit 4-byte Encapsulation-Header).
8
9extern crate alloc;
10use alloc::vec::Vec;
11
12use crate::error::WireError;
13use crate::parameter_list::{Parameter, ParameterList, pid};
14use crate::property_list::WirePropertyList;
15use crate::wire_types::{Guid, Locator, ProtocolVersion, VendorId};
16
17/// PL_CDR_LE Encapsulation-Kind (Spec §10.2).
18pub const ENCAPSULATION_PL_CDR_LE: [u8; 2] = [0x00, 0x03];
19
20/// `BuiltinEndpointSet`-Bitmask-Flags (DDSI-RTPS 2.5 §9.3.2.12,
21/// Tabelle 9.4 + 9.5; DDS-Security 1.2 §7.4.7.1, Tabelle 8 fuer Bits
22/// 16..27). Wird ueber den `PID_BUILTIN_ENDPOINT_SET` (0x0058) PID in
23/// SPDP-`ParticipantBuiltinTopicData` als 32-Bit-Bitmaske ausgetauscht.
24///
25/// Bits 6..9 sind Spec-reserviert (historische Particpant-Proxy-
26/// Features in DDSI 2.1 / Topics aus 2.5 sind hier nicht vergeben).
27/// Bits 16..27 sind die Secure-Discovery-Endpoints aus DDS-Security.
28/// Bits 28..29 sind die XTypes-Topics-Discovery-Endpoints. Bit 30..31
29/// sind Spec-reserviert.
30///
31/// Cyclone DDS und Fast-DDS legen ihre SEDP-Proxies anhand dieser
32/// Bits an — wenn ein Bit fehlt, baut der Peer den korrespondierenden
33/// Reader/Writer nicht auf und das Endpoint-Discovery scheitert.
34/// Daher MUESSEN wir alle Endpoints, die wir lokal anbieten, im
35/// `builtin_endpoint_set` annoncieren.
36pub mod endpoint_flag {
37    // ---------------------------------------------------------------
38    // Standard-Discovery-Endpoints (DDSI-RTPS 2.5 §9.3.2.12, Tab. 9.4)
39    // ---------------------------------------------------------------
40
41    /// Participant SPDP Writer announce (Bit 0).
42    pub const PARTICIPANT_ANNOUNCER: u32 = 1 << 0;
43    /// Participant SPDP Reader detector (Bit 1).
44    pub const PARTICIPANT_DETECTOR: u32 = 1 << 1;
45    /// SEDP Publications Writer (Bit 2).
46    pub const PUBLICATIONS_ANNOUNCER: u32 = 1 << 2;
47    /// SEDP Publications Reader (Bit 3).
48    pub const PUBLICATIONS_DETECTOR: u32 = 1 << 3;
49    /// SEDP Subscriptions Writer (Bit 4).
50    pub const SUBSCRIPTIONS_ANNOUNCER: u32 = 1 << 4;
51    /// SEDP Subscriptions Reader (Bit 5).
52    pub const SUBSCRIPTIONS_DETECTOR: u32 = 1 << 5;
53
54    // ---------------------------------------------------------------
55    // Writer-Liveliness-Protocol (DDSI-RTPS 2.5 §8.4.13, §9.3.2.12)
56    // ---------------------------------------------------------------
57
58    /// `PARTICIPANT_MESSAGE_DATA_WRITER` — sendet WLP-Heartbeats
59    /// (`ParticipantMessageData`) im Topic
60    /// `DCPSParticipantMessage` (Bit 10, RTPS 2.5 §8.4.13).
61    pub const PARTICIPANT_MESSAGE_DATA_WRITER: u32 = 1 << 10;
62    /// `PARTICIPANT_MESSAGE_DATA_READER` — empfaengt WLP-Heartbeats
63    /// (Bit 11, RTPS 2.5 §8.4.13).
64    pub const PARTICIPANT_MESSAGE_DATA_READER: u32 = 1 << 11;
65
66    // ---------------------------------------------------------------
67    // TypeLookup-Service (XTypes 1.3 §7.6.3.3.4)
68    // ---------------------------------------------------------------
69
70    /// `TYPE_LOOKUP_SERVICE_REQUEST_DATA_WRITER/READER` — TypeLookup-
71    /// Request-Endpoint-Pair (Writer + Reader). XTypes 1.3 §7.6.3.3.4
72    /// belegt Bit 12 für das Request-Pair.
73    pub const TYPE_LOOKUP_REQUEST: u32 = 1 << 12;
74    /// `TYPE_LOOKUP_SERVICE_REPLY_DATA_WRITER/READER` — TypeLookup-
75    /// Reply-Endpoint-Pair (Writer + Reader). XTypes 1.3 §7.6.3.3.4
76    /// belegt Bit 13 für das Reply-Pair.
77    pub const TYPE_LOOKUP_REPLY: u32 = 1 << 13;
78
79    // ---------------------------------------------------------------
80    // DDS-Security 1.2 §7.4.7.1, Tab. 8 — Secure-Discovery-Endpoints
81    // (Bits 16..27). Doc-Comments referenzieren die Spec-Konstanten-
82    // Namen (`DISC_BUILTIN_ENDPOINT_*`) fuer Cross-Crate-Audits mit
83    // dem `zerodds-security-rtps`-Crate.
84    // ---------------------------------------------------------------
85
86    /// `DISC_BUILTIN_ENDPOINT_PUBLICATIONS_SECURE_WRITER` — Secure
87    /// SEDP Publications Writer (Bit 16, DDS-Security 1.2 §7.4.7.1).
88    pub const PUBLICATIONS_SECURE_WRITER: u32 = 1 << 16;
89    /// `DISC_BUILTIN_ENDPOINT_PUBLICATIONS_SECURE_READER` — Secure
90    /// SEDP Publications Reader (Bit 17, DDS-Security 1.2 §7.4.7.1).
91    pub const PUBLICATIONS_SECURE_READER: u32 = 1 << 17;
92    /// `DISC_BUILTIN_ENDPOINT_SUBSCRIPTIONS_SECURE_WRITER` — Secure
93    /// SEDP Subscriptions Writer (Bit 18, DDS-Security 1.2 §7.4.7.1).
94    pub const SUBSCRIPTIONS_SECURE_WRITER: u32 = 1 << 18;
95    /// `DISC_BUILTIN_ENDPOINT_SUBSCRIPTIONS_SECURE_READER` — Secure
96    /// SEDP Subscriptions Reader (Bit 19, DDS-Security 1.2 §7.4.7.1).
97    pub const SUBSCRIPTIONS_SECURE_READER: u32 = 1 << 19;
98    /// `BUILTIN_ENDPOINT_PARTICIPANT_MESSAGE_SECURE_WRITER` — Secure
99    /// WLP-Writer (Bit 20, DDS-Security 1.2 §7.4.7.1).
100    pub const PARTICIPANT_MESSAGE_SECURE_WRITER: u32 = 1 << 20;
101    /// `BUILTIN_ENDPOINT_PARTICIPANT_MESSAGE_SECURE_READER` — Secure
102    /// WLP-Reader (Bit 21, DDS-Security 1.2 §7.4.7.1).
103    pub const PARTICIPANT_MESSAGE_SECURE_READER: u32 = 1 << 21;
104    /// `BUILTIN_ENDPOINT_PARTICIPANT_STATELESS_MESSAGE_WRITER` —
105    /// Auth-Stateless-Writer (Bit 22, DDS-Security 1.2 §7.4.7.1).
106    pub const PARTICIPANT_STATELESS_MESSAGE_WRITER: u32 = 1 << 22;
107    /// `BUILTIN_ENDPOINT_PARTICIPANT_STATELESS_MESSAGE_READER` —
108    /// Auth-Stateless-Reader (Bit 23, DDS-Security 1.2 §7.4.7.1).
109    pub const PARTICIPANT_STATELESS_MESSAGE_READER: u32 = 1 << 23;
110    /// `BUILTIN_ENDPOINT_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER`
111    /// — Crypto-KeyExchange-Writer (Bit 24, DDS-Security 1.2 §7.4.7.1).
112    pub const PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER: u32 = 1 << 24;
113    /// `BUILTIN_ENDPOINT_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER`
114    /// — Crypto-KeyExchange-Reader (Bit 25, DDS-Security 1.2 §7.4.7.1).
115    pub const PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER: u32 = 1 << 25;
116    /// `BUILTIN_ENDPOINT_PARTICIPANT_SECURE_WRITER` — DCPSParticipants-
117    /// Secure-Writer (Bit 26, DDS-Security 1.2 §7.4.7.1).
118    pub const PARTICIPANT_SECURE_WRITER: u32 = 1 << 26;
119    /// `BUILTIN_ENDPOINT_PARTICIPANT_SECURE_READER` — DCPSParticipants-
120    /// Secure-Reader (Bit 27, DDS-Security 1.2 §7.4.7.1).
121    pub const PARTICIPANT_SECURE_READER: u32 = 1 << 27;
122
123    // ---------------------------------------------------------------
124    // XTypes-Topics-Discovery (DDSI-RTPS 2.5 §9.3.2.12)
125    // ---------------------------------------------------------------
126
127    /// `DISC_BUILTIN_ENDPOINT_TOPICS_ANNOUNCER` — Topics-Builtin-
128    /// Topic-Announcer (Bit 28, RTPS 2.5 §9.3.2.12).
129    pub const TOPICS_ANNOUNCER: u32 = 1 << 28;
130    /// `DISC_BUILTIN_ENDPOINT_TOPICS_DETECTOR` — Topics-Builtin-
131    /// Topic-Detector (Bit 29, RTPS 2.5 §9.3.2.12).
132    pub const TOPICS_DETECTOR: u32 = 1 << 29;
133
134    // ---------------------------------------------------------------
135    // Convenience-Bundles
136    // ---------------------------------------------------------------
137
138    /// Maske aller 12 Secure-Discovery-Bits (16..27). Wird vom DCPS-
139    /// Runtime zugemixt, wenn das `security`-Feature aktiv ist.
140    pub const ALL_SECURE: u32 = PUBLICATIONS_SECURE_WRITER
141        | PUBLICATIONS_SECURE_READER
142        | SUBSCRIPTIONS_SECURE_WRITER
143        | SUBSCRIPTIONS_SECURE_READER
144        | PARTICIPANT_MESSAGE_SECURE_WRITER
145        | PARTICIPANT_MESSAGE_SECURE_READER
146        | PARTICIPANT_STATELESS_MESSAGE_WRITER
147        | PARTICIPANT_STATELESS_MESSAGE_READER
148        | PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER
149        | PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER
150        | PARTICIPANT_SECURE_WRITER
151        | PARTICIPANT_SECURE_READER;
152
153    /// Maske aller Standard-Bits (0..5, 10..13) ohne Security
154    /// und ohne SEDP-Topics. Repraesentiert die ZeroDDS-Standard-
155    /// Discovery-Capability fuer einen Participant ohne
156    /// `security`-Feature. Inkludiert TypeLookup-Service (Bits 12+13,
157    /// XTypes 1.3 §7.6.3.3.4).
158    ///
159    /// SEDP-Topics-Endpoints (Bits 28/29) sind per RTPS 2.5 §8.5.4.4
160    /// optional. Da ZeroDDS DCPSTopic-Samples synthetisch aus
161    /// Publications/Subscriptions ableitet, annoncen wir die
162    /// Topics-Capability nicht — Bits 28/29 wuerden Peers eine nicht
163    /// existente Endpoint-Paarung versprechen. Vendors, die die
164    /// nativen Topic-Endpoints selbst implementieren, koennen
165    /// [`TOPICS_ANNOUNCER`]/[`TOPICS_DETECTOR`] zur Maske hinzumixen.
166    pub const ALL_STANDARD: u32 = PARTICIPANT_ANNOUNCER
167        | PARTICIPANT_DETECTOR
168        | PUBLICATIONS_ANNOUNCER
169        | PUBLICATIONS_DETECTOR
170        | SUBSCRIPTIONS_ANNOUNCER
171        | SUBSCRIPTIONS_DETECTOR
172        | PARTICIPANT_MESSAGE_DATA_WRITER
173        | PARTICIPANT_MESSAGE_DATA_READER
174        | TYPE_LOOKUP_REQUEST
175        | TYPE_LOOKUP_REPLY;
176}
177
178/// Duration_t (Spec §9.4.2.2): seconds + nanoseconds.
179///
180/// Canonical definition lives in [`zerodds_qos::Duration`]; RTPS
181/// re-exportiert den Typ hier für Abwärtskompatibilität. Alle Call-
182/// sites nutzen den qos-Typ.
183pub use zerodds_qos::Duration;
184
185/// SPDP-Discovered-Participant-Daten. Subset.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct ParticipantBuiltinTopicData {
188    /// GUID des Participants.
189    pub guid: Guid,
190    /// Protokoll-Version (typisch 2.5).
191    pub protocol_version: ProtocolVersion,
192    /// Vendor-Identifier.
193    pub vendor_id: VendorId,
194    /// Default-Unicast-Locator — wohin Peers User-Daten schicken.
195    pub default_unicast_locator: Option<Locator>,
196    /// Default-Multicast-Locator — User-Daten-Multicast.
197    pub default_multicast_locator: Option<Locator>,
198    /// Metatraffic-Unicast-Locator — wohin Peers SEDP-Unicast schicken.
199    /// Fuer SEDP-Interop (z.B. Cyclone) unverzichtbar: Cyclone routet
200    /// Publications/Subscriptions an genau diesen Locator nach match.
201    pub metatraffic_unicast_locator: Option<Locator>,
202    /// Metatraffic-Multicast-Locator — SPDP/SEDP-Multicast-Gruppe.
203    pub metatraffic_multicast_locator: Option<Locator>,
204    /// DDS-Domain-ID. Cyclone filtert Beacons aus anderen Domains,
205    /// wenn die PID fehlt, wird i.d.R. Domain 0 angenommen.
206    pub domain_id: Option<u32>,
207    /// Bitmaske der verfuegbaren Builtin-Endpoints
208    /// (siehe [`endpoint_flag`]).
209    pub builtin_endpoint_set: u32,
210    /// Wie lange der Participant ohne erneuten Beacon "lebendig" gilt.
211    pub lease_duration: Duration,
212    /// UserData-QoS am Participant (Spec §2.2.3.1) — opaque
213    /// sequence<octet>, ueber SPDP propagiert.
214    pub user_data: Vec<u8>,
215    /// Property-Liste (`PID_PROPERTY_LIST`, 0x0059). Traeger fuer
216    /// Security-Plugin-Klassen, Permissions-Tokens und ZeroDDS-
217    /// Heterogeneous-Security-Caps (WP 4H-b). Leer bei Peers ohne
218    /// Security-Announcements (Legacy-Kompatibilitaet).
219    pub properties: WirePropertyList,
220    /// Roher CDR-encoded `IdentityToken`-Blob (DDS-Security 1.2 §7.4.1.4
221    /// Tab.16, `PID_IDENTITY_TOKEN`=0x1001). Wird vom DDS-Security-Layer
222    /// (`zerodds_security::token::DataHolder`) geparst — RTPS reicht nur
223    /// die Bytes durch, damit diese Crate transport-frei bleibt.
224    /// `None` bei Legacy-Peers ohne Security-Annonce.
225    pub identity_token: Option<Vec<u8>>,
226    /// Roher CDR-encoded `PermissionsToken`-Blob (DDS-Security 1.2
227    /// §7.4.1.5 Tab.17, `PID_PERMISSIONS_TOKEN`=0x1002).
228    pub permissions_token: Option<Vec<u8>>,
229    /// Roher CDR-encoded `IdentityStatusToken`-Blob (DDS-Security 1.2
230    /// §7.4.1.6, `PID_IDENTITY_STATUS_TOKEN`=0x1006). Optional, traegt
231    /// OCSP-Live-Status.
232    pub identity_status_token: Option<Vec<u8>>,
233    /// `ParticipantSecurityDigitalSignatureAlgorithmInfo` (DDS-Security
234    /// 1.2 §7.3.11, `PID=0x1010`). Optional — `None` = Spec-Default
235    /// (RSASSA-PSS + ECDSA-P256).
236    pub sig_algo_info:
237        Option<crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo>,
238    /// `ParticipantSecurityKeyEstablishmentAlgorithmInfo` (DDS-Security
239    /// 1.2 §7.3.12, `PID=0x1011`). Optional — `None` = Spec-Default
240    /// (DHE-MODP-2048 + ECDHE-CEUM-P256).
241    pub kx_algo_info:
242        Option<crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo>,
243    /// `ParticipantSecuritySymmetricCipherAlgorithmInfo` (DDS-Security
244    /// 1.2 §7.3.13, `PID=0x1012`). Optional — `None` = Spec-Default
245    /// (AES128|AES256 supported, AES128 required).
246    pub sym_cipher_algo_info:
247        Option<crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo>,
248}
249
250impl ParticipantBuiltinTopicData {
251    /// Encoded zu PL_CDR_LE-Bytes (mit 4-byte Encapsulation-Header).
252    /// Output ist direkt als `serialized_payload` einer DATA-
253    /// Submessage verwendbar.
254    #[must_use]
255    pub fn to_pl_cdr_le(&self) -> Vec<u8> {
256        let mut params = ParameterList::new();
257
258        // PROTOCOL_VERSION: 2 byte + 2 padding
259        let mut pv = Vec::with_capacity(4);
260        pv.extend_from_slice(&self.protocol_version.to_bytes());
261        pv.extend_from_slice(&[0, 0]);
262        params.push(Parameter::new(pid::PROTOCOL_VERSION, pv));
263
264        // VENDOR_ID: 2 byte + 2 padding
265        let mut vid = Vec::with_capacity(4);
266        vid.extend_from_slice(&self.vendor_id.to_bytes());
267        vid.extend_from_slice(&[0, 0]);
268        params.push(Parameter::new(pid::VENDOR_ID, vid));
269
270        // PARTICIPANT_GUID: 16 byte
271        params.push(Parameter::new(
272            pid::PARTICIPANT_GUID,
273            self.guid.to_bytes().to_vec(),
274        ));
275
276        // DEFAULT_UNICAST_LOCATOR (optional): 24 byte
277        if let Some(loc) = self.default_unicast_locator {
278            params.push(Parameter::new(
279                pid::DEFAULT_UNICAST_LOCATOR,
280                loc.to_bytes_le().to_vec(),
281            ));
282        }
283
284        // DEFAULT_MULTICAST_LOCATOR (optional): 24 byte
285        if let Some(loc) = self.default_multicast_locator {
286            params.push(Parameter::new(
287                pid::DEFAULT_MULTICAST_LOCATOR,
288                loc.to_bytes_le().to_vec(),
289            ));
290        }
291
292        // METATRAFFIC_UNICAST_LOCATOR (optional): 24 byte
293        if let Some(loc) = self.metatraffic_unicast_locator {
294            params.push(Parameter::new(
295                pid::METATRAFFIC_UNICAST_LOCATOR,
296                loc.to_bytes_le().to_vec(),
297            ));
298        }
299
300        // METATRAFFIC_MULTICAST_LOCATOR (optional): 24 byte
301        if let Some(loc) = self.metatraffic_multicast_locator {
302            params.push(Parameter::new(
303                pid::METATRAFFIC_MULTICAST_LOCATOR,
304                loc.to_bytes_le().to_vec(),
305            ));
306        }
307
308        // DOMAIN_ID (optional): 4 byte u32
309        if let Some(dom) = self.domain_id {
310            params.push(Parameter::new(pid::DOMAIN_ID, dom.to_le_bytes().to_vec()));
311        }
312
313        // BUILTIN_ENDPOINT_SET: 4 byte u32
314        params.push(Parameter::new(
315            pid::BUILTIN_ENDPOINT_SET,
316            self.builtin_endpoint_set.to_le_bytes().to_vec(),
317        ));
318
319        // LEASE_DURATION: 8 byte
320        params.push(Parameter::new(
321            pid::PARTICIPANT_LEASE_DURATION,
322            self.lease_duration.to_bytes_le().to_vec(),
323        ));
324
325        // USER_DATA — opaque sequence<octet>, nur wenn gesetzt.
326        if !self.user_data.is_empty() {
327            if let Ok(v) = crate::publication_data::encode_octet_seq_le(&self.user_data) {
328                params.push(Parameter::new(pid::USER_DATA, v));
329            }
330        }
331
332        // IDENTITY_TOKEN / PERMISSIONS_TOKEN / IDENTITY_STATUS_TOKEN
333        // (DDS-Security 1.2 §7.4.1.4-6). Caller hat den DataHolder
334        // bereits CDR-encoded — wir reichen die Bytes durch.
335        if let Some(blob) = self.identity_token.as_ref() {
336            params.push(Parameter::new(pid::IDENTITY_TOKEN, blob.clone()));
337        }
338        if let Some(blob) = self.permissions_token.as_ref() {
339            params.push(Parameter::new(pid::PERMISSIONS_TOKEN, blob.clone()));
340        }
341        if let Some(blob) = self.identity_status_token.as_ref() {
342            params.push(Parameter::new(pid::IDENTITY_STATUS_TOKEN, blob.clone()));
343        }
344
345        // Algorithm-Info-PIDs (Spec §7.3.11-13, C3.5-Rest). Default
346        // (None) wird NICHT geschickt — Empfaenger nutzt Spec-Default.
347        if let Some(sig) = self.sig_algo_info.as_ref() {
348            params.push(Parameter::new(
349                pid::PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO,
350                sig.to_bytes(true).to_vec(),
351            ));
352        }
353        if let Some(kx) = self.kx_algo_info.as_ref() {
354            params.push(Parameter::new(
355                pid::PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO,
356                kx.to_bytes(true).to_vec(),
357            ));
358        }
359        if let Some(sym) = self.sym_cipher_algo_info.as_ref() {
360            params.push(Parameter::new(
361                pid::PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO,
362                sym.to_bytes(true).to_vec(),
363            ));
364        }
365
366        // PROPERTY_LIST (optional — nur wenn nicht leer). Der Encoder
367        // der PropertyList darf nicht fehlschlagen, wenn die Bytes
368        // konform zu MAX_PROPERTIES sind; bei Ueberschreitung
369        // schweigend auslassen, damit das SPDP-Beacon-Encoding nie
370        // fehlschlaegt. Caller muss vor Befuellen Caps applizieren.
371        if !self.properties.is_empty() {
372            if let Ok(bytes) = self.properties.encode(true) {
373                params.push(Parameter::new(pid::PROPERTY_LIST, bytes));
374            }
375        }
376
377        // Encapsulation-Header: 4 byte (PL_CDR_LE + options=0).
378        let mut out = Vec::new();
379        out.extend_from_slice(&ENCAPSULATION_PL_CDR_LE);
380        out.extend_from_slice(&[0, 0]); // options
381        out.extend_from_slice(&params.to_bytes(true));
382        out
383    }
384
385    /// Decoded aus PL_CDR_LE-Bytes (mit Encapsulation-Header).
386    ///
387    /// # Errors
388    /// `WireError::UnexpectedEof` wenn Bytes zu kurz; PIDs ohne
389    /// Spec-konforme Laenge werden ignoriert (forward-compat).
390    pub fn from_pl_cdr_le(bytes: &[u8]) -> Result<Self, WireError> {
391        if bytes.len() < 4 {
392            return Err(WireError::UnexpectedEof {
393                needed: 4,
394                offset: 0,
395            });
396        }
397        // Encapsulation-Header pruefen — wir akzeptieren PL_CDR_LE
398        // (00 03) und PL_CDR_BE (00 02). Andere → Error.
399        let little_endian = match &bytes[..2] {
400            b if b == ENCAPSULATION_PL_CDR_LE => true,
401            [0x00, 0x02] => false,
402            other => {
403                return Err(WireError::UnsupportedEncapsulation {
404                    kind: [other[0], other[1]],
405                });
406            }
407        };
408        let pl = ParameterList::from_bytes(&bytes[4..], little_endian)?;
409
410        let guid = pl
411            .find(pid::PARTICIPANT_GUID)
412            .and_then(|p| {
413                if p.value.len() == 16 {
414                    let mut g = [0u8; 16];
415                    g.copy_from_slice(&p.value);
416                    Some(Guid::from_bytes(g))
417                } else {
418                    None
419                }
420            })
421            .ok_or(WireError::ValueOutOfRange {
422                message: "PARTICIPANT_GUID missing or wrong length",
423            })?;
424
425        let protocol_version = pl
426            .find(pid::PROTOCOL_VERSION)
427            .and_then(|p| {
428                if p.value.len() >= 2 {
429                    let mut bs = [0u8; 2];
430                    bs.copy_from_slice(&p.value[..2]);
431                    Some(ProtocolVersion::from_bytes(bs))
432                } else {
433                    None
434                }
435            })
436            .unwrap_or_default();
437
438        let vendor_id = pl
439            .find(pid::VENDOR_ID)
440            .and_then(|p| {
441                if p.value.len() >= 2 {
442                    let mut bs = [0u8; 2];
443                    bs.copy_from_slice(&p.value[..2]);
444                    Some(VendorId::from_bytes(bs))
445                } else {
446                    None
447                }
448            })
449            .unwrap_or(VendorId::UNKNOWN);
450
451        let default_unicast_locator = pl
452            .find(pid::DEFAULT_UNICAST_LOCATOR)
453            .and_then(|p| decode_locator(&p.value, little_endian));
454
455        let default_multicast_locator = pl
456            .find(pid::DEFAULT_MULTICAST_LOCATOR)
457            .and_then(|p| decode_locator(&p.value, little_endian));
458
459        let metatraffic_unicast_locator = pl
460            .find(pid::METATRAFFIC_UNICAST_LOCATOR)
461            .and_then(|p| decode_locator(&p.value, little_endian));
462
463        let metatraffic_multicast_locator = pl
464            .find(pid::METATRAFFIC_MULTICAST_LOCATOR)
465            .and_then(|p| decode_locator(&p.value, little_endian));
466
467        let domain_id = pl.find(pid::DOMAIN_ID).and_then(|p| {
468            if p.value.len() == 4 {
469                let mut bs = [0u8; 4];
470                bs.copy_from_slice(&p.value);
471                Some(if little_endian {
472                    u32::from_le_bytes(bs)
473                } else {
474                    u32::from_be_bytes(bs)
475                })
476            } else {
477                None
478            }
479        });
480
481        let builtin_endpoint_set = pl
482            .find(pid::BUILTIN_ENDPOINT_SET)
483            .and_then(|p| {
484                if p.value.len() == 4 {
485                    let mut bs = [0u8; 4];
486                    bs.copy_from_slice(&p.value);
487                    Some(if little_endian {
488                        u32::from_le_bytes(bs)
489                    } else {
490                        u32::from_be_bytes(bs)
491                    })
492                } else {
493                    None
494                }
495            })
496            .unwrap_or(0);
497
498        let lease_duration = pl
499            .find(pid::PARTICIPANT_LEASE_DURATION)
500            .and_then(|p| {
501                if p.value.len() == 8 {
502                    let mut bs = [0u8; 8];
503                    bs.copy_from_slice(&p.value);
504                    Some(Duration::from_bytes_le(bs))
505                } else {
506                    None
507                }
508            })
509            .unwrap_or(Duration::from_secs(100));
510
511        let user_data = pl
512            .find(pid::USER_DATA)
513            .and_then(|p| crate::publication_data::decode_octet_seq(&p.value, little_endian))
514            .unwrap_or_default();
515
516        // PROPERTY_LIST: leer wenn Peer keine Security-Announcements
517        // schickt (Legacy-Kompatibilitaet); Decoder-Fehler fuehren zu
518        // leerer Liste statt harter Ablehnung, damit ein boeser Peer
519        // uns nicht per malformed PropertyList aus dem SPDP-Prozess
520        // drueckt.
521        let properties = pl
522            .find(pid::PROPERTY_LIST)
523            .and_then(|p| WirePropertyList::decode(&p.value, little_endian).ok())
524            .unwrap_or_default();
525
526        // Roh-Bytes der Tokens durchreichen (Parsing macht der Security-
527        // Layer). Identity/Permissions/IdentityStatus sind optional;
528        // Legacy-Peers schicken sie nicht.
529        let identity_token = pl.find(pid::IDENTITY_TOKEN).map(|p| p.value.clone());
530        let permissions_token = pl.find(pid::PERMISSIONS_TOKEN).map(|p| p.value.clone());
531        let identity_status_token = pl.find(pid::IDENTITY_STATUS_TOKEN).map(|p| p.value.clone());
532
533        // Algorithm-Info-PIDs (Spec §7.3.11-13, C3.5-Rest). Decode-
534        // Fehler → silent None (forward-compat: ein Peer mit veraenderten
535        // Wire-Formaten darf uns nicht aus dem SPDP druecken).
536        let sig_algo_info = pl
537            .find(pid::PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO)
538            .and_then(|p| {
539                crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo::from_bytes(
540                    &p.value,
541                    little_endian,
542                )
543                .ok()
544            });
545        let kx_algo_info = pl
546            .find(pid::PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO)
547            .and_then(|p| {
548                crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo::from_bytes(
549                    &p.value,
550                    little_endian,
551                )
552                .ok()
553            });
554        let sym_cipher_algo_info = pl
555            .find(pid::PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO)
556            .and_then(|p| {
557                crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo::from_bytes(
558                    &p.value,
559                    little_endian,
560                )
561                .ok()
562            });
563
564        Ok(Self {
565            guid,
566            protocol_version,
567            vendor_id,
568            default_unicast_locator,
569            default_multicast_locator,
570            metatraffic_unicast_locator,
571            metatraffic_multicast_locator,
572            domain_id,
573            builtin_endpoint_set,
574            lease_duration,
575            user_data,
576            properties,
577            identity_token,
578            permissions_token,
579            identity_status_token,
580            sig_algo_info,
581            kx_algo_info,
582            sym_cipher_algo_info,
583        })
584    }
585}
586
587fn decode_locator(value: &[u8], little_endian: bool) -> Option<Locator> {
588    if value.len() != Locator::WIRE_SIZE {
589        return None;
590    }
591    if !little_endian {
592        // Limit: BE-Locator nicht implementiert.
593        return None;
594    }
595    let mut bs = [0u8; 24];
596    bs.copy_from_slice(value);
597    Locator::from_bytes_le(bs).ok()
598}
599
600#[cfg(test)]
601mod tests {
602    #![allow(clippy::expect_used, clippy::unwrap_used)]
603    use super::*;
604    use crate::wire_types::{EntityId, GuidPrefix};
605    use alloc::vec;
606
607    fn sample_data() -> ParticipantBuiltinTopicData {
608        ParticipantBuiltinTopicData {
609            guid: Guid::new(
610                GuidPrefix::from_bytes([0xA, 0xB, 0xC, 0xD, 1, 2, 3, 4, 5, 6, 7, 8]),
611                EntityId::PARTICIPANT,
612            ),
613            protocol_version: ProtocolVersion::V2_5,
614            vendor_id: VendorId::ZERODDS,
615            default_unicast_locator: Some(Locator::udp_v4([192, 168, 1, 100], 7410)),
616            default_multicast_locator: Some(Locator::udp_v4([239, 255, 0, 1], 7400)),
617            metatraffic_unicast_locator: None,
618            metatraffic_multicast_locator: None,
619            domain_id: None,
620            builtin_endpoint_set: endpoint_flag::PARTICIPANT_ANNOUNCER
621                | endpoint_flag::PARTICIPANT_DETECTOR,
622            lease_duration: Duration::from_secs(100),
623            user_data: alloc::vec::Vec::new(),
624            properties: Default::default(),
625            identity_token: None,
626            permissions_token: None,
627            identity_status_token: None,
628            sig_algo_info: None,
629            kx_algo_info: None,
630            sym_cipher_algo_info: None,
631        }
632    }
633
634    #[test]
635    fn duration_roundtrip_le() {
636        let d = Duration {
637            seconds: 30,
638            fraction: 500_000_000,
639        };
640        assert_eq!(Duration::from_bytes_le(d.to_bytes_le()), d);
641    }
642
643    #[test]
644    fn participant_data_roundtrip_full() {
645        let d = sample_data();
646        let bytes = d.to_pl_cdr_le();
647        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
648        assert_eq!(decoded, d);
649    }
650
651    #[test]
652    fn participant_data_first_4_bytes_are_pl_cdr_le_encapsulation() {
653        let d = sample_data();
654        let bytes = d.to_pl_cdr_le();
655        assert_eq!(&bytes[..4], &[0x00, 0x03, 0x00, 0x00]);
656    }
657
658    #[test]
659    fn participant_data_properties_roundtrip() {
660        use crate::property_list::WireProperty;
661        let mut d = sample_data();
662        d.properties.push(WireProperty::new(
663            "dds.sec.auth.plugin_class",
664            "DDS:Auth:PKI-DH:1.2",
665        ));
666        d.properties.push(WireProperty::new(
667            "zerodds.sec.offered_protection",
668            "ENCRYPT",
669        ));
670        let bytes = d.to_pl_cdr_le();
671        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
672        assert_eq!(decoded.properties, d.properties);
673        assert_eq!(
674            decoded.properties.get("zerodds.sec.offered_protection"),
675            Some("ENCRYPT")
676        );
677    }
678
679    #[test]
680    fn participant_data_empty_properties_omits_pid() {
681        // Leere PropertyList → PID_PROPERTY_LIST soll NICHT in den
682        // Bytes auftauchen (Abwaerts-Kompatibilitaet: Legacy-Peers
683        // die den PID nicht kennen duerfen nicht verwirrt werden).
684        let d = sample_data();
685        assert!(d.properties.is_empty());
686        let bytes = d.to_pl_cdr_le();
687        // PID_PROPERTY_LIST = 0x0059 LE = 59 00 ; im Stream suchen
688        // (naiv — reicht fuer diesen Test).
689        let has_pid = bytes.windows(2).any(|w| w == [0x59, 0x00]);
690        assert!(!has_pid, "leere properties muessen PID weglassen");
691    }
692
693    #[test]
694    fn participant_data_legacy_peer_without_properties_parses_ok() {
695        // Peer der keine PID_PROPERTY_LIST schickt → decoded.properties
696        // ist leer. Dieses Scenario ist der Default für alle Legacy-
697        // ZeroDDS-Peers + alle Cyclone/Fast-DDS ohne Security.
698        let d = sample_data();
699        let bytes = d.to_pl_cdr_le();
700        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
701        assert!(decoded.properties.is_empty());
702    }
703
704    #[test]
705    fn participant_data_identity_token_pid_roundtrip() {
706        // PID_IDENTITY_TOKEN (0x1001) — opaker Wert (CDR-encoded
707        // DataHolder, vom Security-Layer geparst). RTPS reicht ihn
708        // byte-identisch durch.
709        let mut d = sample_data();
710        // Wert auf 4-byte aligned, weil ParameterList den PID-Wert
711        // mit Zero-Padding bis zur 4-byte-Grenze auffuellt — die
712        // parameter_length im PID-Header ist die gepaddete Laenge,
713        // und der Decoder kann trailing zeros nicht von echtem Wert-
714        // Inhalt unterscheiden. Der Security-Layer-Codec (DataHolder)
715        // ignoriert trailing zeros durch sein Parser-Verhalten.
716        d.identity_token = Some(vec![0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04]);
717        let bytes = d.to_pl_cdr_le();
718        // PID-Tag 0x1001 LE = 01 10 muss im Stream auftauchen.
719        let has_pid = bytes.windows(2).any(|w| w == [0x01, 0x10]);
720        assert!(has_pid, "PID_IDENTITY_TOKEN fehlt im PL_CDR_LE-Stream");
721        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
722        assert_eq!(decoded.identity_token, d.identity_token);
723    }
724
725    #[test]
726    fn participant_data_permissions_token_pid_roundtrip() {
727        let mut d = sample_data();
728        d.permissions_token = Some(vec![0xDE, 0xAD, 0xBE, 0xEF]);
729        let bytes = d.to_pl_cdr_le();
730        let has_pid = bytes.windows(2).any(|w| w == [0x02, 0x10]);
731        assert!(has_pid, "PID_PERMISSIONS_TOKEN fehlt");
732        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
733        assert_eq!(decoded.permissions_token, d.permissions_token);
734    }
735
736    #[test]
737    fn participant_data_identity_status_token_pid_roundtrip() {
738        let mut d = sample_data();
739        d.identity_status_token = Some(vec![0x77, 0x88, 0x99, 0xAA]);
740        let bytes = d.to_pl_cdr_le();
741        let has_pid = bytes.windows(2).any(|w| w == [0x06, 0x10]);
742        assert!(has_pid, "PID_IDENTITY_STATUS_TOKEN fehlt");
743        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
744        assert_eq!(decoded.identity_status_token, d.identity_status_token);
745    }
746
747    #[test]
748    fn participant_data_no_token_pids_in_legacy_announce() {
749        // Default-Sample (kein Security) → keiner der drei Token-PIDs
750        // taucht auf, damit Legacy-Peers nicht verwirrt werden.
751        let d = sample_data();
752        let bytes = d.to_pl_cdr_le();
753        for pid_le in [[0x01u8, 0x10], [0x02, 0x10], [0x06, 0x10]] {
754            let found = bytes.windows(2).any(|w| w == pid_le);
755            assert!(
756                !found,
757                "Token-PID {pid_le:?} darf in Legacy nicht auftauchen"
758            );
759        }
760    }
761
762    #[test]
763    fn participant_data_three_tokens_combined_roundtrip() {
764        // Realistisches Security-Announce: alle drei Tokens
765        // gleichzeitig.
766        let mut d = sample_data();
767        d.identity_token = Some(vec![0x01; 64]);
768        d.permissions_token = Some(vec![0x02; 32]);
769        d.identity_status_token = Some(vec![0x03; 16]);
770        let bytes = d.to_pl_cdr_le();
771        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
772        assert_eq!(decoded, d);
773    }
774
775    // Algorithm-Info-PIDs (Spec §7.3.11-13, C3.5-Rest)
776
777    #[test]
778    fn participant_data_sig_algo_info_roundtrip() {
779        let mut d = sample_data();
780        d.sig_algo_info =
781            Some(crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo::spec_default());
782        let bytes = d.to_pl_cdr_le();
783        // PID 0x1010 LE = [0x10, 0x10] muss im Stream sein.
784        assert!(
785            bytes.windows(2).any(|w| w == [0x10, 0x10]),
786            "PID 0x1010 fehlt im PL_CDR_LE-Stream"
787        );
788        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
789        assert_eq!(decoded.sig_algo_info, d.sig_algo_info);
790    }
791
792    #[test]
793    fn participant_data_kx_algo_info_roundtrip() {
794        let mut d = sample_data();
795        d.kx_algo_info =
796            Some(crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo::spec_default());
797        let bytes = d.to_pl_cdr_le();
798        assert!(
799            bytes.windows(2).any(|w| w == [0x11, 0x10]),
800            "PID 0x1011 fehlt"
801        );
802        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
803        assert_eq!(decoded.kx_algo_info, d.kx_algo_info);
804    }
805
806    #[test]
807    fn participant_data_sym_cipher_algo_info_roundtrip() {
808        let mut d = sample_data();
809        d.sym_cipher_algo_info =
810            Some(crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo::spec_default());
811        let bytes = d.to_pl_cdr_le();
812        assert!(
813            bytes.windows(2).any(|w| w == [0x12, 0x10]),
814            "PID 0x1012 fehlt"
815        );
816        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
817        assert_eq!(decoded.sym_cipher_algo_info, d.sym_cipher_algo_info);
818    }
819
820    #[test]
821    fn participant_data_no_algo_info_in_legacy_announce() {
822        // Default-Sample → keiner der drei Algo-Info-PIDs taucht auf.
823        let d = sample_data();
824        let bytes = d.to_pl_cdr_le();
825        for pid_le in [[0x10u8, 0x10], [0x11, 0x10], [0x12, 0x10]] {
826            assert!(
827                !bytes.windows(2).any(|w| w == pid_le),
828                "Algo-PID {pid_le:?} darf in Legacy nicht auftauchen"
829            );
830        }
831    }
832
833    #[test]
834    fn participant_data_all_three_algo_infos_combined() {
835        let mut d = sample_data();
836        d.sig_algo_info =
837            Some(crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo::spec_default());
838        d.kx_algo_info =
839            Some(crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo::spec_default());
840        d.sym_cipher_algo_info =
841            Some(crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo::spec_default());
842        let bytes = d.to_pl_cdr_le();
843        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
844        assert_eq!(decoded, d);
845    }
846
847    #[test]
848    fn participant_data_without_optional_locators() {
849        let mut d = sample_data();
850        d.default_unicast_locator = None;
851        d.default_multicast_locator = None;
852        let bytes = d.to_pl_cdr_le();
853        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
854        assert!(decoded.default_unicast_locator.is_none());
855        assert!(decoded.default_multicast_locator.is_none());
856    }
857
858    #[test]
859    fn participant_data_decode_rejects_unknown_encapsulation() {
860        let mut bytes = vec![0x99, 0x99, 0, 0]; // unknown encap
861        bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // sentinel
862        let res = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes);
863        assert!(matches!(
864            res,
865            Err(WireError::UnsupportedEncapsulation { kind: [0x99, 0x99] })
866        ));
867    }
868
869    #[test]
870    fn participant_data_decode_requires_guid_pid() {
871        // Encapsulation + nur Sentinel.
872        let bytes = vec![0x00, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
873        let res = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes);
874        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
875    }
876
877    #[test]
878    fn endpoint_flags_have_distinct_bits() {
879        // Sanity: keine zwei Flags belegen den gleichen Bit.
880        let flags = [
881            endpoint_flag::PARTICIPANT_ANNOUNCER,
882            endpoint_flag::PARTICIPANT_DETECTOR,
883            endpoint_flag::PUBLICATIONS_ANNOUNCER,
884            endpoint_flag::PUBLICATIONS_DETECTOR,
885            endpoint_flag::SUBSCRIPTIONS_ANNOUNCER,
886            endpoint_flag::SUBSCRIPTIONS_DETECTOR,
887            endpoint_flag::PARTICIPANT_MESSAGE_DATA_WRITER,
888            endpoint_flag::PARTICIPANT_MESSAGE_DATA_READER,
889            endpoint_flag::PUBLICATIONS_SECURE_WRITER,
890            endpoint_flag::PUBLICATIONS_SECURE_READER,
891            endpoint_flag::SUBSCRIPTIONS_SECURE_WRITER,
892            endpoint_flag::SUBSCRIPTIONS_SECURE_READER,
893            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_WRITER,
894            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_READER,
895            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER,
896            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER,
897            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
898            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
899            endpoint_flag::PARTICIPANT_SECURE_WRITER,
900            endpoint_flag::PARTICIPANT_SECURE_READER,
901            endpoint_flag::TOPICS_ANNOUNCER,
902            endpoint_flag::TOPICS_DETECTOR,
903        ];
904        for (i, &a) in flags.iter().enumerate() {
905            for &b in &flags[i + 1..] {
906                assert_eq!(a & b, 0, "flag bits must be distinct");
907            }
908        }
909    }
910
911    #[test]
912    fn endpoint_flag_bit_positions_match_spec() {
913        // Bit-Positionen muessen exakt der Spec-Tabelle entsprechen
914        // (DDSI-RTPS 2.5 §9.3.2.12 + DDS-Security 1.2 §7.4.7.1).
915        // Cyclone DDS und Fast-DDS verlassen sich auf diese exakten
916        // Bits — Versatz um 1 Position bricht das Endpoint-Discovery.
917        assert_eq!(endpoint_flag::PARTICIPANT_ANNOUNCER, 0x0000_0001);
918        assert_eq!(endpoint_flag::PARTICIPANT_DETECTOR, 0x0000_0002);
919        assert_eq!(endpoint_flag::PUBLICATIONS_ANNOUNCER, 0x0000_0004);
920        assert_eq!(endpoint_flag::PUBLICATIONS_DETECTOR, 0x0000_0008);
921        assert_eq!(endpoint_flag::SUBSCRIPTIONS_ANNOUNCER, 0x0000_0010);
922        assert_eq!(endpoint_flag::SUBSCRIPTIONS_DETECTOR, 0x0000_0020);
923        assert_eq!(endpoint_flag::PARTICIPANT_MESSAGE_DATA_WRITER, 0x0000_0400);
924        assert_eq!(endpoint_flag::PARTICIPANT_MESSAGE_DATA_READER, 0x0000_0800);
925        assert_eq!(endpoint_flag::PUBLICATIONS_SECURE_WRITER, 0x0001_0000);
926        assert_eq!(endpoint_flag::PUBLICATIONS_SECURE_READER, 0x0002_0000);
927        assert_eq!(endpoint_flag::SUBSCRIPTIONS_SECURE_WRITER, 0x0004_0000);
928        assert_eq!(endpoint_flag::SUBSCRIPTIONS_SECURE_READER, 0x0008_0000);
929        assert_eq!(
930            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_WRITER,
931            0x0010_0000
932        );
933        assert_eq!(
934            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_READER,
935            0x0020_0000
936        );
937        assert_eq!(
938            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER,
939            0x0040_0000
940        );
941        assert_eq!(
942            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER,
943            0x0080_0000
944        );
945        assert_eq!(
946            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
947            0x0100_0000
948        );
949        assert_eq!(
950            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
951            0x0200_0000
952        );
953        assert_eq!(endpoint_flag::PARTICIPANT_SECURE_WRITER, 0x0400_0000);
954        assert_eq!(endpoint_flag::PARTICIPANT_SECURE_READER, 0x0800_0000);
955        assert_eq!(endpoint_flag::TOPICS_ANNOUNCER, 0x1000_0000);
956        assert_eq!(endpoint_flag::TOPICS_DETECTOR, 0x2000_0000);
957    }
958
959    #[test]
960    fn endpoint_flag_all_secure_covers_bits_16_to_27() {
961        // ALL_SECURE muss exakt die 12 Bits 16..=27 setzen, kein Bit
962        // mehr und kein Bit weniger (sonst leakt der Default-Build
963        // Security-Bits in unsichere Peers).
964        let mask = endpoint_flag::ALL_SECURE;
965        for bit in 16u32..=27 {
966            assert!(mask & (1u32 << bit) != 0, "bit {bit} fehlt in ALL_SECURE");
967        }
968        // Keine Bits ausserhalb 16..=27.
969        let outside_mask: u32 = !((1u32 << 28) - (1u32 << 16));
970        assert_eq!(
971            mask & outside_mask,
972            0,
973            "ALL_SECURE darf nur Bits 16..27 setzen"
974        );
975    }
976
977    #[test]
978    fn endpoint_flag_all_standard_excludes_secure_bits() {
979        // Default-Standard-Bundle darf KEINE Security-Bits enthalten.
980        // Sonst leaken wir Secure-Endpoint-Promises in Peers, ohne
981        // dass das `security`-Feature aktiv ist.
982        assert_eq!(endpoint_flag::ALL_STANDARD & endpoint_flag::ALL_SECURE, 0);
983    }
984
985    #[test]
986    fn endpoint_flag_roundtrip_through_pl_cdr() {
987        // Encoder muss alle 16 Bits unverfaelscht ueber PL_CDR_LE
988        // tragen — sonst verlieren Peers Secure-/WLP-/Topics-Bits.
989        let combined = endpoint_flag::ALL_STANDARD | endpoint_flag::ALL_SECURE;
990        let mut d = sample_data();
991        d.builtin_endpoint_set = combined;
992        let bytes = d.to_pl_cdr_le();
993        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
994        assert_eq!(decoded.builtin_endpoint_set, combined);
995    }
996}