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//! Content of the SPDP beacon DATA submessage. Carried as a PL_CDR_LE-encoded
6//! ParameterList in the `serialized_payload` of the DATA submessage
7//! (with a 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, LocatorKind, 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/// table 9.4 + 9.5; DDS-Security 1.2 §7.4.7.1, table 8 for bits
22/// 16..27). Exchanged via the `PID_BUILTIN_ENDPOINT_SET` (0x0058) PID
23/// in the SPDP `ParticipantBuiltinTopicData` as a 32-bit bitmask.
24///
25/// Bits 6..9 are spec-reserved (historical participant-proxy features
26/// in DDSI 2.1 / topics from 2.5 are not assigned here). Bits 16..27
27/// are the secure-discovery endpoints from DDS-Security. Bits 28..29
28/// are the XTypes topics-discovery endpoints. Bits 30..31 are
29/// spec-reserved.
30///
31/// Cyclone DDS and Fast-DDS create their SEDP proxies based on these
32/// bits — if a bit is missing, the peer does not build the
33/// corresponding reader/writer and endpoint discovery fails.
34/// Therefore we MUST announce all endpoints we offer locally in the
35/// `builtin_endpoint_set`.
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` — sends WLP heartbeats
59    /// (`ParticipantMessageData`) on the 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` — receives 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` — the TypeLookup
71    /// request endpoint pair (writer + reader). XTypes 1.3 §7.6.3.3.4
72    /// assigns bit 12 to the request pair.
73    pub const TYPE_LOOKUP_REQUEST: u32 = 1 << 12;
74    /// `TYPE_LOOKUP_SERVICE_REPLY_DATA_WRITER/READER` — the TypeLookup
75    /// reply endpoint pair (writer + reader). XTypes 1.3 §7.6.3.3.4
76    /// assigns bit 13 to the 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 reference the spec constant names
82    // (`DISC_BUILTIN_ENDPOINT_*`) for cross-crate audits with the
83    // `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    /// Mask of all 12 secure-discovery bits (16..27). Mixed in by the
139    /// DCPS runtime when the `security` feature is active.
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    /// Mask of all standard bits (0..5, 10..13) without security and
154    /// without SEDP topics. Represents the ZeroDDS standard discovery
155    /// capability for a participant without the `security` feature.
156    /// Includes the TypeLookup service (bits 12+13, XTypes 1.3
157    /// §7.6.3.3.4).
158    ///
159    /// SEDP topics endpoints (bits 28/29) are optional per RTPS 2.5
160    /// §8.5.4.4. Since ZeroDDS derives DCPSTopic samples synthetically
161    /// from publications/subscriptions, we do not announce the topics
162    /// capability — bits 28/29 would promise peers a non-existent
163    /// endpoint pairing. Vendors that implement the native topic
164    /// endpoints themselves can mix
165    /// [`TOPICS_ANNOUNCER`]/[`TOPICS_DETECTOR`] into the mask.
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-exports the type here for backward compatibility. All call sites
182/// use the qos type.
183pub use zerodds_qos::Duration;
184
185/// SPDP discovered-participant data. 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-data multicast.
197    pub default_multicast_locator: Option<Locator>,
198    /// Metatraffic unicast locator — where peers send SEDP unicast.
199    /// Indispensable for SEDP interop (e.g. Cyclone): Cyclone routes
200    /// publications/subscriptions to exactly this locator after a match.
201    pub metatraffic_unicast_locator: Option<Locator>,
202    /// Metatraffic multicast locator — the SPDP/SEDP multicast group.
203    pub metatraffic_multicast_locator: Option<Locator>,
204    /// DDS domain ID. Cyclone filters beacons from other domains; if the
205    /// PID is missing, domain 0 is usually assumed.
206    pub domain_id: Option<u32>,
207    /// Bitmask of available builtin endpoints
208    /// (see [`endpoint_flag`]).
209    pub builtin_endpoint_set: u32,
210    /// How long the participant is considered "alive" without a renewed
211    /// beacon.
212    pub lease_duration: Duration,
213    /// UserData QoS at the participant (spec §2.2.3.1) — opaque
214    /// sequence<octet>, propagated over SPDP.
215    pub user_data: Vec<u8>,
216    /// Property list (`PID_PROPERTY_LIST`, 0x0059). Carrier for
217    /// security-plugin classes, permissions tokens and ZeroDDS
218    /// heterogeneous-security caps (WP 4H-b). Empty for peers without
219    /// security announcements (legacy compatibility).
220    pub properties: WirePropertyList,
221    /// Raw CDR-encoded `IdentityToken` blob (DDS-Security 1.2 §7.4.1.4
222    /// Tab.16, `PID_IDENTITY_TOKEN`=0x1001). Parsed by the DDS-Security
223    /// layer (`zerodds_security::token::DataHolder`) — RTPS only passes
224    /// the bytes through, so this crate stays transport-free.
225    /// `None` for legacy peers without a security announce.
226    pub identity_token: Option<Vec<u8>>,
227    /// Raw CDR-encoded `PermissionsToken` blob (DDS-Security 1.2
228    /// §7.4.1.5 Tab.17, `PID_PERMISSIONS_TOKEN`=0x1002).
229    pub permissions_token: Option<Vec<u8>>,
230    /// Raw CDR-encoded `IdentityStatusToken` blob (DDS-Security 1.2
231    /// §7.4.1.6, `PID_IDENTITY_STATUS_TOKEN`=0x1006). Optional, carries
232    /// the OCSP live status.
233    pub identity_status_token: Option<Vec<u8>>,
234    /// `ParticipantSecurityDigitalSignatureAlgorithmInfo` (DDS-Security
235    /// 1.2 §7.3.11, `PID=0x1010`). Optional — `None` = spec default
236    /// (RSASSA-PSS + ECDSA-P256).
237    pub sig_algo_info:
238        Option<crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo>,
239    /// `ParticipantSecurityKeyEstablishmentAlgorithmInfo` (DDS-Security
240    /// 1.2 §7.3.12, `PID=0x1011`). Optional — `None` = spec default
241    /// (DHE-MODP-2048 + ECDHE-CEUM-P256).
242    pub kx_algo_info:
243        Option<crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo>,
244    /// `ParticipantSecuritySymmetricCipherAlgorithmInfo` (DDS-Security
245    /// 1.2 §7.3.13, `PID=0x1012`). Optional — `None` = spec default
246    /// (AES128|AES256 supported, AES128 required).
247    pub sym_cipher_algo_info:
248        Option<crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo>,
249    /// `ParticipantSecurityInfo` (DDS-Security 1.2 §7.4.1.6,
250    /// `PID_PARTICIPANT_SECURITY_INFO`=0x1005). Two attribute bitmasks at
251    /// participant level. **Mandatory for cross-vendor interop**: foreign
252    /// vendors (cyclone/FastDDS) classify a participant WITHOUT this PID as
253    /// NON-SECURE and reject its endpoints. `None` = legacy/plain.
254    pub participant_security_info:
255        Option<crate::participant_security_info::ParticipantSecurityInfo>,
256}
257
258impl ParticipantBuiltinTopicData {
259    /// Encodes to PL_CDR_LE bytes (with a 4-byte encapsulation header).
260    /// The output is directly usable as the `serialized_payload` of a DATA
261    /// submessage.
262    #[must_use]
263    pub fn to_pl_cdr_le(&self) -> Vec<u8> {
264        let mut params = ParameterList::new();
265
266        // PROTOCOL_VERSION: 2 byte + 2 padding
267        let mut pv = Vec::with_capacity(4);
268        pv.extend_from_slice(&self.protocol_version.to_bytes());
269        pv.extend_from_slice(&[0, 0]);
270        params.push(Parameter::new(pid::PROTOCOL_VERSION, pv));
271
272        // VENDOR_ID: 2 byte + 2 padding
273        let mut vid = Vec::with_capacity(4);
274        vid.extend_from_slice(&self.vendor_id.to_bytes());
275        vid.extend_from_slice(&[0, 0]);
276        params.push(Parameter::new(pid::VENDOR_ID, vid));
277
278        // PARTICIPANT_GUID: 16 byte
279        params.push(Parameter::new(
280            pid::PARTICIPANT_GUID,
281            self.guid.to_bytes().to_vec(),
282        ));
283
284        // DEFAULT_UNICAST_LOCATOR (optional): 24 byte
285        if let Some(loc) = self.default_unicast_locator {
286            params.push(Parameter::new(
287                pid::DEFAULT_UNICAST_LOCATOR,
288                loc.to_bytes_le().to_vec(),
289            ));
290        }
291
292        // DEFAULT_MULTICAST_LOCATOR (optional): 24 byte
293        if let Some(loc) = self.default_multicast_locator {
294            params.push(Parameter::new(
295                pid::DEFAULT_MULTICAST_LOCATOR,
296                loc.to_bytes_le().to_vec(),
297            ));
298        }
299
300        // METATRAFFIC_UNICAST_LOCATOR (optional): 24 byte
301        if let Some(loc) = self.metatraffic_unicast_locator {
302            params.push(Parameter::new(
303                pid::METATRAFFIC_UNICAST_LOCATOR,
304                loc.to_bytes_le().to_vec(),
305            ));
306        }
307
308        // METATRAFFIC_MULTICAST_LOCATOR (optional): 24 byte
309        if let Some(loc) = self.metatraffic_multicast_locator {
310            params.push(Parameter::new(
311                pid::METATRAFFIC_MULTICAST_LOCATOR,
312                loc.to_bytes_le().to_vec(),
313            ));
314        }
315
316        // DOMAIN_ID (optional): 4 byte u32
317        if let Some(dom) = self.domain_id {
318            params.push(Parameter::new(pid::DOMAIN_ID, dom.to_le_bytes().to_vec()));
319        }
320
321        // BUILTIN_ENDPOINT_SET: 4 byte u32
322        params.push(Parameter::new(
323            pid::BUILTIN_ENDPOINT_SET,
324            self.builtin_endpoint_set.to_le_bytes().to_vec(),
325        ));
326
327        // LEASE_DURATION: 8 byte
328        params.push(Parameter::new(
329            pid::PARTICIPANT_LEASE_DURATION,
330            self.lease_duration.to_bytes_le().to_vec(),
331        ));
332
333        // USER_DATA — opaque sequence<octet>, only if set.
334        if !self.user_data.is_empty() {
335            if let Ok(v) = crate::publication_data::encode_octet_seq_le(&self.user_data) {
336                params.push(Parameter::new(pid::USER_DATA, v));
337            }
338        }
339
340        // IDENTITY_TOKEN / PERMISSIONS_TOKEN / IDENTITY_STATUS_TOKEN
341        // (DDS-Security 1.2 §7.4.1.4-6). The caller has already
342        // CDR-encoded the DataHolder — we pass the bytes through.
343        if let Some(blob) = self.identity_token.as_ref() {
344            params.push(Parameter::new(pid::IDENTITY_TOKEN, blob.clone()));
345        }
346        if let Some(blob) = self.permissions_token.as_ref() {
347            params.push(Parameter::new(pid::PERMISSIONS_TOKEN, blob.clone()));
348        }
349        if let Some(blob) = self.identity_status_token.as_ref() {
350            params.push(Parameter::new(pid::IDENTITY_STATUS_TOKEN, blob.clone()));
351        }
352        // PID_PARTICIPANT_SECURITY_INFO (0x1005, §7.4.1.6) — 8 Byte LE.
353        // Without this PID, cyclone/FastDDS classify the participant as
354        // non-secure and reject its endpoints.
355        if let Some(psi) = self.participant_security_info.as_ref() {
356            params.push(Parameter::new(
357                pid::PARTICIPANT_SECURITY_INFO,
358                psi.to_le_bytes().to_vec(),
359            ));
360        }
361
362        // Algorithm-info PIDs (spec §7.3.11-13, C3.5 remainder). The
363        // default (None) is NOT sent — the receiver uses the spec default.
364        if let Some(sig) = self.sig_algo_info.as_ref() {
365            params.push(Parameter::new(
366                pid::PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO,
367                sig.to_bytes(true).to_vec(),
368            ));
369        }
370        if let Some(kx) = self.kx_algo_info.as_ref() {
371            params.push(Parameter::new(
372                pid::PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO,
373                kx.to_bytes(true).to_vec(),
374            ));
375        }
376        if let Some(sym) = self.sym_cipher_algo_info.as_ref() {
377            params.push(Parameter::new(
378                pid::PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO,
379                sym.to_bytes(true).to_vec(),
380            ));
381        }
382
383        // PROPERTY_LIST (optional — only if non-empty). The PropertyList
384        // encoder must not fail if the bytes conform to MAX_PROPERTIES;
385        // on overflow silently omit it, so the SPDP-beacon encoding
386        // never fails. The caller must apply caps before filling.
387        if !self.properties.is_empty() {
388            if let Ok(bytes) = self.properties.encode(true) {
389                params.push(Parameter::new(pid::PROPERTY_LIST, bytes));
390            }
391        }
392
393        // Encapsulation header: 4 byte (PL_CDR_LE + options=0).
394        let mut out = Vec::new();
395        out.extend_from_slice(&ENCAPSULATION_PL_CDR_LE);
396        out.extend_from_slice(&[0, 0]); // options
397        out.extend_from_slice(&params.to_bytes(true));
398        out
399    }
400
401    /// Encodes to PL_CDR_**BE** bytes (encapsulation 0x0002, all values
402    /// big-endian). For the handshake `c.pdata` (DDS-Security §9.3.2.5.2:
403    /// "CDR Big-Endian serialization of the ParticipantBuiltinTopicData") —
404    /// cyclone/FastDDS deserialize c.pdata strictly as a BE ParameterList;
405    /// LE yields "Deserialize parameter failed: payload too long".
406    ///
407    /// The credential tokens (identity_token/permissions_token/
408    /// identity_status_token) are OMITTED here: they are LE-encoded
409    /// DataHolder blobs and travel separately in the handshake as
410    /// c.id/c.perm — Cyclone's c.pdata also does not contain them.
411    #[must_use]
412    pub fn to_pl_cdr_be(&self) -> Vec<u8> {
413        let mut params = ParameterList::new();
414
415        let mut pv = Vec::with_capacity(4);
416        pv.extend_from_slice(&self.protocol_version.to_bytes());
417        pv.extend_from_slice(&[0, 0]);
418        params.push(Parameter::new(pid::PROTOCOL_VERSION, pv));
419
420        let mut vid = Vec::with_capacity(4);
421        vid.extend_from_slice(&self.vendor_id.to_bytes());
422        vid.extend_from_slice(&[0, 0]);
423        params.push(Parameter::new(pid::VENDOR_ID, vid));
424
425        params.push(Parameter::new(
426            pid::PARTICIPANT_GUID,
427            self.guid.to_bytes().to_vec(),
428        ));
429
430        for (id, loc) in [
431            (pid::DEFAULT_UNICAST_LOCATOR, self.default_unicast_locator),
432            (
433                pid::DEFAULT_MULTICAST_LOCATOR,
434                self.default_multicast_locator,
435            ),
436            (
437                pid::METATRAFFIC_UNICAST_LOCATOR,
438                self.metatraffic_unicast_locator,
439            ),
440            (
441                pid::METATRAFFIC_MULTICAST_LOCATOR,
442                self.metatraffic_multicast_locator,
443            ),
444        ] {
445            if let Some(loc) = loc {
446                params.push(Parameter::new(id, loc.to_bytes_be().to_vec()));
447            }
448        }
449
450        if let Some(dom) = self.domain_id {
451            params.push(Parameter::new(pid::DOMAIN_ID, dom.to_be_bytes().to_vec()));
452        }
453
454        params.push(Parameter::new(
455            pid::BUILTIN_ENDPOINT_SET,
456            self.builtin_endpoint_set.to_be_bytes().to_vec(),
457        ));
458
459        params.push(Parameter::new(
460            pid::PARTICIPANT_LEASE_DURATION,
461            self.lease_duration.to_bytes_be().to_vec(),
462        ));
463
464        if let Some(psi) = self.participant_security_info.as_ref() {
465            params.push(Parameter::new(
466                pid::PARTICIPANT_SECURITY_INFO,
467                psi.to_be_bytes().to_vec(),
468            ));
469        }
470        if let Some(sig) = self.sig_algo_info.as_ref() {
471            params.push(Parameter::new(
472                pid::PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO,
473                sig.to_bytes(false).to_vec(),
474            ));
475        }
476        if let Some(kx) = self.kx_algo_info.as_ref() {
477            params.push(Parameter::new(
478                pid::PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO,
479                kx.to_bytes(false).to_vec(),
480            ));
481        }
482        if let Some(sym) = self.sym_cipher_algo_info.as_ref() {
483            params.push(Parameter::new(
484                pid::PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO,
485                sym.to_bytes(false).to_vec(),
486            ));
487        }
488        if !self.properties.is_empty() {
489            if let Ok(bytes) = self.properties.encode(false) {
490                params.push(Parameter::new(pid::PROPERTY_LIST, bytes));
491            }
492        }
493
494        // Encapsulation-Header: PL_CDR_BE (0x0002) + options=0.
495        let mut out = Vec::new();
496        out.extend_from_slice(&[0x00, 0x02]);
497        out.extend_from_slice(&[0, 0]);
498        out.extend_from_slice(&params.to_bytes(false));
499        out
500    }
501
502    /// Decoded from PL_CDR_LE bytes (with encapsulation header).
503    ///
504    /// # Errors
505    /// `WireError::UnexpectedEof` if the bytes are too short; PIDs
506    /// without a spec-conformant length are ignored (forward-compat).
507    pub fn from_pl_cdr_le(bytes: &[u8]) -> Result<Self, WireError> {
508        if bytes.len() < 4 {
509            return Err(WireError::UnexpectedEof {
510                needed: 4,
511                offset: 0,
512            });
513        }
514        // Check the encapsulation header — we accept PL_CDR_LE
515        // (00 03) and PL_CDR_BE (00 02). Others → error.
516        let little_endian = match &bytes[..2] {
517            b if b == ENCAPSULATION_PL_CDR_LE => true,
518            [0x00, 0x02] => false,
519            other => {
520                return Err(WireError::UnsupportedEncapsulation {
521                    kind: [other[0], other[1]],
522                });
523            }
524        };
525        let pl = ParameterList::from_bytes(&bytes[4..], little_endian)?;
526
527        let guid = pl
528            .find(pid::PARTICIPANT_GUID)
529            .and_then(|p| {
530                if p.value.len() == 16 {
531                    let mut g = [0u8; 16];
532                    g.copy_from_slice(&p.value);
533                    Some(Guid::from_bytes(g))
534                } else {
535                    None
536                }
537            })
538            .ok_or(WireError::ValueOutOfRange {
539                message: "PARTICIPANT_GUID missing or wrong length",
540            })?;
541
542        let protocol_version = pl
543            .find(pid::PROTOCOL_VERSION)
544            .and_then(|p| {
545                if p.value.len() >= 2 {
546                    let mut bs = [0u8; 2];
547                    bs.copy_from_slice(&p.value[..2]);
548                    Some(ProtocolVersion::from_bytes(bs))
549                } else {
550                    None
551                }
552            })
553            .unwrap_or_default();
554
555        let vendor_id = pl
556            .find(pid::VENDOR_ID)
557            .and_then(|p| {
558                if p.value.len() >= 2 {
559                    let mut bs = [0u8; 2];
560                    bs.copy_from_slice(&p.value[..2]);
561                    Some(VendorId::from_bytes(bs))
562                } else {
563                    None
564                }
565            })
566            .unwrap_or(VendorId::UNKNOWN);
567
568        // Unicast locators may be announced multiple times (multi-homed peer):
569        // decode all and prefer the routable one instead of blindly the first (M-1).
570        let default_unicast_locator = pick_routable_locator(
571            pl.find_all(pid::DEFAULT_UNICAST_LOCATOR)
572                .filter_map(|p| decode_locator(&p.value, little_endian)),
573        );
574
575        let default_multicast_locator = pl
576            .find(pid::DEFAULT_MULTICAST_LOCATOR)
577            .and_then(|p| decode_locator(&p.value, little_endian));
578
579        let metatraffic_unicast_locator = pick_routable_locator(
580            pl.find_all(pid::METATRAFFIC_UNICAST_LOCATOR)
581                .filter_map(|p| decode_locator(&p.value, little_endian)),
582        );
583
584        let metatraffic_multicast_locator = pl
585            .find(pid::METATRAFFIC_MULTICAST_LOCATOR)
586            .and_then(|p| decode_locator(&p.value, little_endian));
587
588        let domain_id = pl.find(pid::DOMAIN_ID).and_then(|p| {
589            if p.value.len() == 4 {
590                let mut bs = [0u8; 4];
591                bs.copy_from_slice(&p.value);
592                Some(if little_endian {
593                    u32::from_le_bytes(bs)
594                } else {
595                    u32::from_be_bytes(bs)
596                })
597            } else {
598                None
599            }
600        });
601
602        let builtin_endpoint_set = pl
603            .find(pid::BUILTIN_ENDPOINT_SET)
604            .and_then(|p| {
605                if p.value.len() == 4 {
606                    let mut bs = [0u8; 4];
607                    bs.copy_from_slice(&p.value);
608                    Some(if little_endian {
609                        u32::from_le_bytes(bs)
610                    } else {
611                        u32::from_be_bytes(bs)
612                    })
613                } else {
614                    None
615                }
616            })
617            .unwrap_or(0);
618
619        let lease_duration = pl
620            .find(pid::PARTICIPANT_LEASE_DURATION)
621            .and_then(|p| {
622                if p.value.len() == 8 {
623                    let mut bs = [0u8; 8];
624                    bs.copy_from_slice(&p.value);
625                    Some(Duration::from_bytes_le(bs))
626                } else {
627                    None
628                }
629            })
630            .unwrap_or(Duration::from_secs(100));
631
632        let user_data = pl
633            .find(pid::USER_DATA)
634            .and_then(|p| crate::publication_data::decode_octet_seq(&p.value, little_endian))
635            .unwrap_or_default();
636
637        // PROPERTY_LIST: empty if the peer sends no security
638        // announcements (legacy compatibility); decoder errors lead to
639        // an empty list instead of a hard rejection, so a malicious peer
640        // cannot push us out of the SPDP process with a malformed
641        // PropertyList.
642        let properties = pl
643            .find(pid::PROPERTY_LIST)
644            .and_then(|p| WirePropertyList::decode(&p.value, little_endian).ok())
645            .unwrap_or_default();
646
647        // Pass the raw token bytes through (parsing is done by the
648        // security layer). Identity/Permissions/IdentityStatus are
649        // optional; legacy peers do not send them.
650        let identity_token = pl.find(pid::IDENTITY_TOKEN).map(|p| p.value.clone());
651        let permissions_token = pl.find(pid::PERMISSIONS_TOKEN).map(|p| p.value.clone());
652        let identity_status_token = pl.find(pid::IDENTITY_STATUS_TOKEN).map(|p| p.value.clone());
653        // PID_PARTICIPANT_SECURITY_INFO (0x1005) — decode error → silent None.
654        let participant_security_info = pl.find(pid::PARTICIPANT_SECURITY_INFO).and_then(|p| {
655            use crate::participant_security_info::ParticipantSecurityInfo;
656            if little_endian {
657                ParticipantSecurityInfo::from_le_bytes(&p.value).ok()
658            } else {
659                ParticipantSecurityInfo::from_be_bytes(&p.value).ok()
660            }
661        });
662
663        // Algorithm-info PIDs (spec §7.3.11-13, C3.5 remainder). A decode
664        // error → silent None (forward-compat: a peer with altered wire
665        // formats must not push us out of SPDP).
666        let sig_algo_info = pl
667            .find(pid::PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO)
668            .and_then(|p| {
669                crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo::from_bytes(
670                    &p.value,
671                    little_endian,
672                )
673                .ok()
674            });
675        let kx_algo_info = pl
676            .find(pid::PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO)
677            .and_then(|p| {
678                crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo::from_bytes(
679                    &p.value,
680                    little_endian,
681                )
682                .ok()
683            });
684        let sym_cipher_algo_info = pl
685            .find(pid::PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO)
686            .and_then(|p| {
687                crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo::from_bytes(
688                    &p.value,
689                    little_endian,
690                )
691                .ok()
692            });
693
694        Ok(Self {
695            guid,
696            protocol_version,
697            vendor_id,
698            default_unicast_locator,
699            default_multicast_locator,
700            metatraffic_unicast_locator,
701            metatraffic_multicast_locator,
702            domain_id,
703            builtin_endpoint_set,
704            lease_duration,
705            user_data,
706            properties,
707            identity_token,
708            permissions_token,
709            identity_status_token,
710            sig_algo_info,
711            kx_algo_info,
712            sym_cipher_algo_info,
713            participant_security_info,
714        })
715    }
716}
717
718/// `true` if a UDPv4 locator is plausibly reachable — i.e. NOT
719/// link-local (169.254.0.0/16) or unspecified (0.0.0.0). Loopback (127.0.0.0/8)
720/// counts as routable (same-host). Non-UDPv4 kinds (TCP/SHM/UDS/IPv6) are
721/// not heuristically downgraded.
722fn locator_looks_routable(loc: &Locator) -> bool {
723    if loc.kind != LocatorKind::UdpV4 {
724        return true;
725    }
726    let ip = loc.ipv4();
727    let unspecified = ip == [0, 0, 0, 0];
728    let link_local = ip[0] == 169 && ip[1] == 254;
729    !(unspecified || link_local)
730}
731
732/// Picks from several announced locators (multi-homed peer, DDSI-RTPS
733/// §8.5.3.2 / §9.6.1.1: a `*_UNICAST_LOCATOR` PID may appear multiple times)
734/// the most likely reachable one: a plausibly routable one ([`locator_looks_routable`])
735/// is preferred, otherwise the first announced. Fixes the misroute where a
736/// non-routable FIRST locator (link-local listed first) sent the reverse SPDP/
737/// SEDP/VolatileSecure reply to an unreachable target.
738fn pick_routable_locator(candidates: impl Iterator<Item = Locator>) -> Option<Locator> {
739    let mut first = None;
740    let mut best = None;
741    for loc in candidates {
742        if first.is_none() {
743            first = Some(loc);
744        }
745        if best.is_none() && locator_looks_routable(&loc) {
746            best = Some(loc);
747        }
748    }
749    best.or(first)
750}
751
752fn decode_locator(value: &[u8], little_endian: bool) -> Option<Locator> {
753    if value.len() != Locator::WIRE_SIZE {
754        return None;
755    }
756    if !little_endian {
757        // Limitation: BE locator not implemented.
758        return None;
759    }
760    let mut bs = [0u8; 24];
761    bs.copy_from_slice(value);
762    Locator::from_bytes_le(bs).ok()
763}
764
765#[cfg(test)]
766mod tests {
767    #![allow(clippy::expect_used, clippy::unwrap_used)]
768    use super::*;
769    use crate::wire_types::{EntityId, GuidPrefix};
770    use alloc::vec;
771
772    #[test]
773    fn pick_routable_prefers_non_link_local() {
774        // Regression M-1: a multi-homed peer may list the link-local
775        // locator FIRST. pick_routable_locator must still choose the
776        // routable one, otherwise the reverse-discovery reply goes to
777        // 169.254.x.x into the void.
778        let link_local = Locator::udp_v4([169, 254, 1, 5], 7410);
779        let routable = Locator::udp_v4([192, 168, 1, 10], 7410);
780        assert_eq!(
781            pick_routable_locator([link_local, routable].into_iter()),
782            Some(routable)
783        );
784        // Only link-local → fall back to the first (better than nothing).
785        assert_eq!(
786            pick_routable_locator([link_local].into_iter()),
787            Some(link_local)
788        );
789        // unspecified is downgraded too.
790        let unspec = Locator::udp_v4([0, 0, 0, 0], 7410);
791        assert_eq!(
792            pick_routable_locator([unspec, routable].into_iter()),
793            Some(routable)
794        );
795        // Loopback counts as routable (same-host operation).
796        let loopback = Locator::udp_v4([127, 0, 0, 1], 7410);
797        assert_eq!(
798            pick_routable_locator([loopback].into_iter()),
799            Some(loopback)
800        );
801        assert_eq!(pick_routable_locator(core::iter::empty()), None);
802    }
803
804    fn sample_data() -> ParticipantBuiltinTopicData {
805        ParticipantBuiltinTopicData {
806            guid: Guid::new(
807                GuidPrefix::from_bytes([0xA, 0xB, 0xC, 0xD, 1, 2, 3, 4, 5, 6, 7, 8]),
808                EntityId::PARTICIPANT,
809            ),
810            protocol_version: ProtocolVersion::V2_5,
811            vendor_id: VendorId::ZERODDS,
812            default_unicast_locator: Some(Locator::udp_v4([192, 168, 1, 100], 7410)),
813            default_multicast_locator: Some(Locator::udp_v4([239, 255, 0, 1], 7400)),
814            metatraffic_unicast_locator: None,
815            metatraffic_multicast_locator: None,
816            domain_id: None,
817            builtin_endpoint_set: endpoint_flag::PARTICIPANT_ANNOUNCER
818                | endpoint_flag::PARTICIPANT_DETECTOR,
819            lease_duration: Duration::from_secs(100),
820            user_data: alloc::vec::Vec::new(),
821            properties: Default::default(),
822            identity_token: None,
823            permissions_token: None,
824            identity_status_token: None,
825            sig_algo_info: None,
826            kx_algo_info: None,
827            sym_cipher_algo_info: None,
828            participant_security_info: None,
829        }
830    }
831
832    #[test]
833    fn to_pl_cdr_be_is_big_endian_roundtrips_and_omits_credential_tokens() {
834        use crate::participant_security_info::{ParticipantSecurityInfo, attrs, plugin_attrs};
835        let mut d = sample_data();
836        d.participant_security_info = Some(ParticipantSecurityInfo {
837            participant_security_attributes: attrs::IS_VALID,
838            plugin_participant_security_attributes: plugin_attrs::IS_VALID,
839        });
840        // Credential token set — must NOT appear in c.pdata.
841        d.identity_token = Some(alloc::vec![0xAB; 32]);
842        d.permissions_token = Some(alloc::vec![0xCD; 16]);
843        let be = d.to_pl_cdr_be();
844        // PL_CDR_BE encapsulation (Spec §9.3.2.5.2: c.pdata is big-endian).
845        assert_eq!(
846            &be[..4],
847            &[0x00, 0x02, 0x00, 0x00],
848            "c.pdata must be PL_CDR_BE"
849        );
850        // Roundtrip (from_pl_cdr_le accepts BE via the encapsulation kind).
851        let back = ParticipantBuiltinTopicData::from_pl_cdr_le(&be).unwrap();
852        assert_eq!(back.guid, d.guid);
853        assert_eq!(back.builtin_endpoint_set, d.builtin_endpoint_set);
854        assert_eq!(back.participant_security_info, d.participant_security_info);
855        // Credential-Token wurden weggelassen (reisen via c.id/c.perm).
856        assert!(
857            back.identity_token.is_none(),
858            "identity_token does not belong in c.pdata"
859        );
860        assert!(
861            back.permissions_token.is_none(),
862            "permissions_token does not belong in c.pdata"
863        );
864    }
865
866    #[test]
867    fn participant_security_info_pid_roundtrip() {
868        // FU2 cross-vendor: PID_PARTICIPANT_SECURITY_INFO (0x1005) must
869        // appear in the SPDP PL_CDR + roundtrip, otherwise cyclone/
870        // FastDDS classify us as non-secure.
871        use crate::participant_security_info::{ParticipantSecurityInfo, attrs, plugin_attrs};
872        let mut d = sample_data();
873        d.participant_security_info = Some(ParticipantSecurityInfo {
874            participant_security_attributes: attrs::IS_VALID,
875            plugin_participant_security_attributes: plugin_attrs::IS_VALID,
876        });
877        let bytes = d.to_pl_cdr_le();
878        // PID 0x1005 LE = 05 10 must be in the stream.
879        let has_pid = bytes.windows(2).any(|w| w == [0x05, 0x10]);
880        assert!(
881            has_pid,
882            "PID_PARTICIPANT_SECURITY_INFO missing from PL_CDR_LE"
883        );
884        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
885        assert_eq!(
886            decoded.participant_security_info,
887            d.participant_security_info
888        );
889        assert!(decoded.participant_security_info.unwrap().is_valid());
890    }
891
892    #[test]
893    fn duration_roundtrip_le() {
894        let d = Duration {
895            seconds: 30,
896            fraction: 500_000_000,
897        };
898        assert_eq!(Duration::from_bytes_le(d.to_bytes_le()), d);
899    }
900
901    #[test]
902    fn participant_data_roundtrip_full() {
903        let d = sample_data();
904        let bytes = d.to_pl_cdr_le();
905        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
906        assert_eq!(decoded, d);
907    }
908
909    #[test]
910    fn participant_data_first_4_bytes_are_pl_cdr_le_encapsulation() {
911        let d = sample_data();
912        let bytes = d.to_pl_cdr_le();
913        assert_eq!(&bytes[..4], &[0x00, 0x03, 0x00, 0x00]);
914    }
915
916    #[test]
917    fn participant_data_properties_roundtrip() {
918        use crate::property_list::WireProperty;
919        let mut d = sample_data();
920        d.properties.push(WireProperty::new(
921            "dds.sec.auth.plugin_class",
922            "DDS:Auth:PKI-DH:1.2",
923        ));
924        d.properties.push(WireProperty::new(
925            "zerodds.sec.offered_protection",
926            "ENCRYPT",
927        ));
928        let bytes = d.to_pl_cdr_le();
929        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
930        assert_eq!(decoded.properties, d.properties);
931        assert_eq!(
932            decoded.properties.get("zerodds.sec.offered_protection"),
933            Some("ENCRYPT")
934        );
935    }
936
937    #[test]
938    fn participant_data_empty_properties_omits_pid() {
939        // Empty PropertyList → PID_PROPERTY_LIST should NOT appear in
940        // the bytes (backward compatibility: legacy peers that do not
941        // know the PID must not be confused).
942        let d = sample_data();
943        assert!(d.properties.is_empty());
944        let bytes = d.to_pl_cdr_le();
945        // PID_PROPERTY_LIST = 0x0059 LE = 59 00 ; search in the stream
946        // (naive — enough for this test).
947        let has_pid = bytes.windows(2).any(|w| w == [0x59, 0x00]);
948        assert!(!has_pid, "empty properties must omit the PID");
949    }
950
951    #[test]
952    fn participant_data_legacy_peer_without_properties_parses_ok() {
953        // A peer that sends no PID_PROPERTY_LIST → decoded.properties
954        // is empty. This scenario is the default for all legacy
955        // ZeroDDS peers + all Cyclone/Fast-DDS without security.
956        let d = sample_data();
957        let bytes = d.to_pl_cdr_le();
958        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
959        assert!(decoded.properties.is_empty());
960    }
961
962    #[test]
963    fn participant_data_identity_token_pid_roundtrip() {
964        // PID_IDENTITY_TOKEN (0x1001) — opaque value (CDR-encoded
965        // DataHolder, parsed by the security layer). RTPS passes it
966        // through byte-identically.
967        let mut d = sample_data();
968        // Value 4-byte aligned, because the ParameterList pads the PID
969        // value with zero-padding to the 4-byte boundary — the
970        // parameter_length in the PID header is the padded length, and
971        // the decoder cannot distinguish trailing zeros from real value
972        // content. The security-layer codec (DataHolder) ignores
973        // trailing zeros via its parser behavior.
974        d.identity_token = Some(vec![0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04]);
975        let bytes = d.to_pl_cdr_le();
976        // PID tag 0x1001 LE = 01 10 must appear in the stream.
977        let has_pid = bytes.windows(2).any(|w| w == [0x01, 0x10]);
978        assert!(
979            has_pid,
980            "PID_IDENTITY_TOKEN missing from the PL_CDR_LE stream"
981        );
982        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
983        assert_eq!(decoded.identity_token, d.identity_token);
984    }
985
986    #[test]
987    fn participant_data_permissions_token_pid_roundtrip() {
988        let mut d = sample_data();
989        d.permissions_token = Some(vec![0xDE, 0xAD, 0xBE, 0xEF]);
990        let bytes = d.to_pl_cdr_le();
991        let has_pid = bytes.windows(2).any(|w| w == [0x02, 0x10]);
992        assert!(has_pid, "PID_PERMISSIONS_TOKEN missing");
993        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
994        assert_eq!(decoded.permissions_token, d.permissions_token);
995    }
996
997    #[test]
998    fn participant_data_identity_status_token_pid_roundtrip() {
999        let mut d = sample_data();
1000        d.identity_status_token = Some(vec![0x77, 0x88, 0x99, 0xAA]);
1001        let bytes = d.to_pl_cdr_le();
1002        let has_pid = bytes.windows(2).any(|w| w == [0x06, 0x10]);
1003        assert!(has_pid, "PID_IDENTITY_STATUS_TOKEN missing");
1004        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1005        assert_eq!(decoded.identity_status_token, d.identity_status_token);
1006    }
1007
1008    #[test]
1009    fn participant_data_no_token_pids_in_legacy_announce() {
1010        // Default sample (no security) → none of the three token PIDs
1011        // appears, so legacy peers are not confused.
1012        let d = sample_data();
1013        let bytes = d.to_pl_cdr_le();
1014        for pid_le in [[0x01u8, 0x10], [0x02, 0x10], [0x06, 0x10]] {
1015            let found = bytes.windows(2).any(|w| w == pid_le);
1016            assert!(!found, "token PID {pid_le:?} must not appear in legacy");
1017        }
1018    }
1019
1020    #[test]
1021    fn participant_data_three_tokens_combined_roundtrip() {
1022        // Realistic security announce: all three tokens at once.
1023        let mut d = sample_data();
1024        d.identity_token = Some(vec![0x01; 64]);
1025        d.permissions_token = Some(vec![0x02; 32]);
1026        d.identity_status_token = Some(vec![0x03; 16]);
1027        let bytes = d.to_pl_cdr_le();
1028        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1029        assert_eq!(decoded, d);
1030    }
1031
1032    // Algorithm-Info-PIDs (Spec §7.3.11-13, C3.5-Rest)
1033
1034    #[test]
1035    fn participant_data_sig_algo_info_roundtrip() {
1036        let mut d = sample_data();
1037        d.sig_algo_info =
1038            Some(crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo::spec_default());
1039        let bytes = d.to_pl_cdr_le();
1040        // PID 0x1010 LE = [0x10, 0x10] must be in the stream.
1041        assert!(
1042            bytes.windows(2).any(|w| w == [0x10, 0x10]),
1043            "PID 0x1010 missing from the PL_CDR_LE stream"
1044        );
1045        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1046        assert_eq!(decoded.sig_algo_info, d.sig_algo_info);
1047    }
1048
1049    #[test]
1050    fn participant_data_kx_algo_info_roundtrip() {
1051        let mut d = sample_data();
1052        d.kx_algo_info =
1053            Some(crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo::spec_default());
1054        let bytes = d.to_pl_cdr_le();
1055        assert!(
1056            bytes.windows(2).any(|w| w == [0x11, 0x10]),
1057            "PID 0x1011 missing"
1058        );
1059        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1060        assert_eq!(decoded.kx_algo_info, d.kx_algo_info);
1061    }
1062
1063    #[test]
1064    fn participant_data_sym_cipher_algo_info_roundtrip() {
1065        let mut d = sample_data();
1066        d.sym_cipher_algo_info =
1067            Some(crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo::spec_default());
1068        let bytes = d.to_pl_cdr_le();
1069        assert!(
1070            bytes.windows(2).any(|w| w == [0x12, 0x10]),
1071            "PID 0x1012 missing"
1072        );
1073        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1074        assert_eq!(decoded.sym_cipher_algo_info, d.sym_cipher_algo_info);
1075    }
1076
1077    #[test]
1078    fn participant_data_no_algo_info_in_legacy_announce() {
1079        // Default sample → none of the three algo-info PIDs appears.
1080        let d = sample_data();
1081        let bytes = d.to_pl_cdr_le();
1082        for pid_le in [[0x10u8, 0x10], [0x11, 0x10], [0x12, 0x10]] {
1083            assert!(
1084                !bytes.windows(2).any(|w| w == pid_le),
1085                "algo PID {pid_le:?} must not appear in legacy"
1086            );
1087        }
1088    }
1089
1090    #[test]
1091    fn participant_data_all_three_algo_infos_combined() {
1092        let mut d = sample_data();
1093        d.sig_algo_info =
1094            Some(crate::security_algo_info::ParticipantSecurityDigitalSignatureAlgorithmInfo::spec_default());
1095        d.kx_algo_info =
1096            Some(crate::security_algo_info::ParticipantSecurityKeyEstablishmentAlgorithmInfo::spec_default());
1097        d.sym_cipher_algo_info =
1098            Some(crate::security_algo_info::ParticipantSecuritySymmetricCipherAlgorithmInfo::spec_default());
1099        let bytes = d.to_pl_cdr_le();
1100        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1101        assert_eq!(decoded, d);
1102    }
1103
1104    #[test]
1105    fn participant_data_without_optional_locators() {
1106        let mut d = sample_data();
1107        d.default_unicast_locator = None;
1108        d.default_multicast_locator = None;
1109        let bytes = d.to_pl_cdr_le();
1110        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1111        assert!(decoded.default_unicast_locator.is_none());
1112        assert!(decoded.default_multicast_locator.is_none());
1113    }
1114
1115    #[test]
1116    fn participant_data_decode_rejects_unknown_encapsulation() {
1117        let mut bytes = vec![0x99, 0x99, 0, 0]; // unknown encap
1118        bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // sentinel
1119        let res = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes);
1120        assert!(matches!(
1121            res,
1122            Err(WireError::UnsupportedEncapsulation { kind: [0x99, 0x99] })
1123        ));
1124    }
1125
1126    #[test]
1127    fn participant_data_decode_requires_guid_pid() {
1128        // Encapsulation + nur Sentinel.
1129        let bytes = vec![0x00, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
1130        let res = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes);
1131        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
1132    }
1133
1134    #[test]
1135    fn endpoint_flags_have_distinct_bits() {
1136        // Sanity: no two flags occupy the same bit.
1137        let flags = [
1138            endpoint_flag::PARTICIPANT_ANNOUNCER,
1139            endpoint_flag::PARTICIPANT_DETECTOR,
1140            endpoint_flag::PUBLICATIONS_ANNOUNCER,
1141            endpoint_flag::PUBLICATIONS_DETECTOR,
1142            endpoint_flag::SUBSCRIPTIONS_ANNOUNCER,
1143            endpoint_flag::SUBSCRIPTIONS_DETECTOR,
1144            endpoint_flag::PARTICIPANT_MESSAGE_DATA_WRITER,
1145            endpoint_flag::PARTICIPANT_MESSAGE_DATA_READER,
1146            endpoint_flag::PUBLICATIONS_SECURE_WRITER,
1147            endpoint_flag::PUBLICATIONS_SECURE_READER,
1148            endpoint_flag::SUBSCRIPTIONS_SECURE_WRITER,
1149            endpoint_flag::SUBSCRIPTIONS_SECURE_READER,
1150            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_WRITER,
1151            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_READER,
1152            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER,
1153            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER,
1154            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
1155            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
1156            endpoint_flag::PARTICIPANT_SECURE_WRITER,
1157            endpoint_flag::PARTICIPANT_SECURE_READER,
1158            endpoint_flag::TOPICS_ANNOUNCER,
1159            endpoint_flag::TOPICS_DETECTOR,
1160        ];
1161        for (i, &a) in flags.iter().enumerate() {
1162            for &b in &flags[i + 1..] {
1163                assert_eq!(a & b, 0, "flag bits must be distinct");
1164            }
1165        }
1166    }
1167
1168    #[test]
1169    fn endpoint_flag_bit_positions_match_spec() {
1170        // Bit positions must match the spec table exactly
1171        // (DDSI-RTPS 2.5 §9.3.2.12 + DDS-Security 1.2 §7.4.7.1).
1172        // Cyclone DDS and Fast-DDS rely on these exact bits — an offset
1173        // of 1 position breaks endpoint discovery.
1174        assert_eq!(endpoint_flag::PARTICIPANT_ANNOUNCER, 0x0000_0001);
1175        assert_eq!(endpoint_flag::PARTICIPANT_DETECTOR, 0x0000_0002);
1176        assert_eq!(endpoint_flag::PUBLICATIONS_ANNOUNCER, 0x0000_0004);
1177        assert_eq!(endpoint_flag::PUBLICATIONS_DETECTOR, 0x0000_0008);
1178        assert_eq!(endpoint_flag::SUBSCRIPTIONS_ANNOUNCER, 0x0000_0010);
1179        assert_eq!(endpoint_flag::SUBSCRIPTIONS_DETECTOR, 0x0000_0020);
1180        assert_eq!(endpoint_flag::PARTICIPANT_MESSAGE_DATA_WRITER, 0x0000_0400);
1181        assert_eq!(endpoint_flag::PARTICIPANT_MESSAGE_DATA_READER, 0x0000_0800);
1182        assert_eq!(endpoint_flag::PUBLICATIONS_SECURE_WRITER, 0x0001_0000);
1183        assert_eq!(endpoint_flag::PUBLICATIONS_SECURE_READER, 0x0002_0000);
1184        assert_eq!(endpoint_flag::SUBSCRIPTIONS_SECURE_WRITER, 0x0004_0000);
1185        assert_eq!(endpoint_flag::SUBSCRIPTIONS_SECURE_READER, 0x0008_0000);
1186        assert_eq!(
1187            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_WRITER,
1188            0x0010_0000
1189        );
1190        assert_eq!(
1191            endpoint_flag::PARTICIPANT_MESSAGE_SECURE_READER,
1192            0x0020_0000
1193        );
1194        assert_eq!(
1195            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER,
1196            0x0040_0000
1197        );
1198        assert_eq!(
1199            endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER,
1200            0x0080_0000
1201        );
1202        assert_eq!(
1203            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
1204            0x0100_0000
1205        );
1206        assert_eq!(
1207            endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
1208            0x0200_0000
1209        );
1210        assert_eq!(endpoint_flag::PARTICIPANT_SECURE_WRITER, 0x0400_0000);
1211        assert_eq!(endpoint_flag::PARTICIPANT_SECURE_READER, 0x0800_0000);
1212        assert_eq!(endpoint_flag::TOPICS_ANNOUNCER, 0x1000_0000);
1213        assert_eq!(endpoint_flag::TOPICS_DETECTOR, 0x2000_0000);
1214    }
1215
1216    #[test]
1217    fn endpoint_flag_all_secure_covers_bits_16_to_27() {
1218        // ALL_SECURE must set exactly the 12 bits 16..=27, no bit more
1219        // and no bit less (otherwise the default build leaks security
1220        // bits into insecure peers).
1221        let mask = endpoint_flag::ALL_SECURE;
1222        for bit in 16u32..=27 {
1223            assert!(
1224                mask & (1u32 << bit) != 0,
1225                "bit {bit} missing from ALL_SECURE"
1226            );
1227        }
1228        // No bits outside 16..=27.
1229        let outside_mask: u32 = !((1u32 << 28) - (1u32 << 16));
1230        assert_eq!(
1231            mask & outside_mask,
1232            0,
1233            "ALL_SECURE may only set bits 16..27"
1234        );
1235    }
1236
1237    #[test]
1238    fn endpoint_flag_all_standard_excludes_secure_bits() {
1239        // The default standard bundle must contain NO security bits.
1240        // Otherwise we leak secure-endpoint promises into peers without
1241        // the `security` feature being active.
1242        assert_eq!(endpoint_flag::ALL_STANDARD & endpoint_flag::ALL_SECURE, 0);
1243    }
1244
1245    #[test]
1246    fn endpoint_flag_roundtrip_through_pl_cdr() {
1247        // The encoder must carry all 16 bits unaltered over PL_CDR_LE —
1248        // otherwise peers lose secure/WLP/topics bits.
1249        let combined = endpoint_flag::ALL_STANDARD | endpoint_flag::ALL_SECURE;
1250        let mut d = sample_data();
1251        d.builtin_endpoint_set = combined;
1252        let bytes = d.to_pl_cdr_le();
1253        let decoded = ParticipantBuiltinTopicData::from_pl_cdr_le(&bytes).unwrap();
1254        assert_eq!(decoded.builtin_endpoint_set, combined);
1255    }
1256}