Skip to main content

zerodds_rtps/
parameter_list.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! ParameterList (DDSI-RTPS 2.5 §9.4.2.11).
4//!
5//! Tag-length-value format for SPDP/SEDP builtin topic data. Each
6//! parameter:
7//!
8//! ```text
9//!   0                   1                   2                   3
10//!   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
11//!  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12//!  |         parameter_id          |            length             |
13//!  +---------------+---------------+---------------+---------------+
14//!  |                          value (length bytes)                 |
15//!  +---------------+---------------+---------------+---------------+
16//! ```
17//!
18//! Terminator: `parameter_id = PID_SENTINEL (0x0001)`, `length = 0`,
19//! no value.
20//!
21//! Encoding is always with the submessage endianness; this module works
22//! on raw bytes with an explicit `little_endian` parameter.
23
24extern crate alloc;
25use alloc::vec::Vec;
26
27use crate::error::WireError;
28
29/// Standard parameter IDs (spec §9.6.4 + table 9.13).
30///
31/// The 12 QoS-policy PIDs from DDS 1.4 §2.2.3 are re-exported from
32/// [`zerodds_qos::Pid`] (single source of truth). rtps-specific PIDs
33/// (locators, GUIDs, security tokens, …) stay declared here, since they
34/// are outside the QoS-policy subset.
35pub mod pid {
36    use zerodds_qos::Pid as QosPid;
37
38    // ---- Re-exports from zerodds_qos::Pid (12 policy PIDs) ----
39    /// Sentinel — terminator of the ParameterList. (Re-export from `zerodds_qos::Pid::SENTINEL`.)
40    pub const SENTINEL: u16 = QosPid::SENTINEL;
41    /// Reliability QoS. (Re-export from `zerodds_qos::Pid::RELIABILITY`.)
42    pub const RELIABILITY: u16 = QosPid::RELIABILITY;
43    /// Durability QoS. (Re-export from `zerodds_qos::Pid::DURABILITY`.)
44    pub const DURABILITY: u16 = QosPid::DURABILITY;
45    /// Ownership QoS. (Re-export from `zerodds_qos::Pid::OWNERSHIP`.)
46    pub const OWNERSHIP: u16 = QosPid::OWNERSHIP;
47    /// Ownership-Strength QoS. (Re-export from `zerodds_qos::Pid::OWNERSHIP_STRENGTH`.)
48    pub const OWNERSHIP_STRENGTH: u16 = QosPid::OWNERSHIP_STRENGTH;
49    /// Liveliness QoS. (Re-export from `zerodds_qos::Pid::LIVELINESS`.)
50    pub const LIVELINESS: u16 = QosPid::LIVELINESS;
51    /// Deadline QoS. (Re-export from `zerodds_qos::Pid::DEADLINE`.)
52    pub const DEADLINE: u16 = QosPid::DEADLINE;
53    /// Lifespan QoS. (Re-export from `zerodds_qos::Pid::LIFESPAN`.)
54    pub const LIFESPAN: u16 = QosPid::LIFESPAN;
55    /// Partition QoS. (Re-export from `zerodds_qos::Pid::PARTITION`.)
56    pub const PARTITION: u16 = QosPid::PARTITION;
57    /// UserData QoS. (Re-export from `zerodds_qos::Pid::USER_DATA`.)
58    pub const USER_DATA: u16 = QosPid::USER_DATA;
59    /// GroupData QoS. (Re-export from `zerodds_qos::Pid::GROUP_DATA`.)
60    pub const GROUP_DATA: u16 = QosPid::GROUP_DATA;
61    /// TopicData QoS. (Re-export from `zerodds_qos::Pid::TOPIC_DATA`.)
62    pub const TOPIC_DATA: u16 = QosPid::TOPIC_DATA;
63
64    // ---- rtps-spezifische PIDs (Discovery / Locators / Security / Wire) ----
65    /// Participant lease duration (Duration_t = i32 sec + u32 nanosec).
66    pub const PARTICIPANT_LEASE_DURATION: u16 = 0x0002;
67    /// Topic-Name (CDR-String).
68    pub const TOPIC_NAME: u16 = 0x0005;
69    /// Type-Name (CDR-String).
70    pub const TYPE_NAME: u16 = 0x0007;
71    /// ProtocolVersion (2 byte + 2 padding).
72    pub const PROTOCOL_VERSION: u16 = 0x0015;
73    /// VendorId (2 byte + 2 padding).
74    pub const VENDOR_ID: u16 = 0x0016;
75    /// Content-Filter-Property (Spec §9.6.3.4 Table 9.14): Topic-
76    /// Filter-Name (String) + related-Topic-Name (String) + Filter-
77    /// Class-Name (String) + Filter-Expression (String) +
78    /// Expression-Parameters (sequence<String>).
79    pub const CONTENT_FILTER_PROPERTY: u16 = 0x0035;
80    /// Default unicast locator (24 byte) — for user data.
81    pub const DEFAULT_UNICAST_LOCATOR: u16 = 0x0031;
82    /// Metatraffic unicast locator (24 byte) — where peers send SEDP
83    /// unicast. Indispensable for Cyclone interop.
84    pub const METATRAFFIC_UNICAST_LOCATOR: u16 = 0x0032;
85    /// Metatraffic multicast locator (24 byte) — where peers send
86    /// SPDP/SEDP multicast.
87    pub const METATRAFFIC_MULTICAST_LOCATOR: u16 = 0x0033;
88    /// Domain id (4 byte u32) — participant domain.
89    pub const DOMAIN_ID: u16 = 0x000f;
90    /// Default multicast locator (24 byte).
91    pub const DEFAULT_MULTICAST_LOCATOR: u16 = 0x0048;
92    /// Endpoint unicast locator (24 byte) — where peers send user data
93    /// to *this* reader/writer. Spec §8.5.3.2/§8.5.3.3:
94    /// `DiscoveredReaderData.readerProxy.unicastLocatorList`. One
95    /// parameter per locator (a list = repeated PID).
96    /// Takes precedence over the participant `DEFAULT_UNICAST_LOCATOR` —
97    /// OpenDDS sends only the placeholder 127.0.0.1:12345 as the
98    /// participant default and stores the real locators exclusively here.
99    pub const UNICAST_LOCATOR: u16 = 0x002f;
100    /// Endpoint multicast locator (24 byte) — spec §8.5.3.2.
101    pub const MULTICAST_LOCATOR: u16 = 0x0030;
102    /// Participant GUID (16 byte).
103    pub const PARTICIPANT_GUID: u16 = 0x0050;
104    /// Endpoint GUID (16 byte) — for publication/subscription discovery.
105    pub const ENDPOINT_GUID: u16 = 0x005a;
106    /// Property list (spec OMG DDS-Security 1.1 §7.2.1). A sequence of
107    /// (name, value) string pairs plus an empty or filled
108    /// BinaryPropertySeq. Carrier for security-plugin classes,
109    /// permissions tokens and ZeroDDS heterogeneous-security caps
110    /// (WP 4H-b).
111    pub const PROPERTY_LIST: u16 = 0x0059;
112    /// Endpoint security info (spec OMG DDS-Security 1.1 §7.4.1.5).
113    /// 2x u32 masks: `endpoint_security_attributes` +
114    /// `plugin_endpoint_security_attributes`. Carrier for
115    /// endpoint-level protection flags (WP 4H-c).
116    pub const ENDPOINT_SECURITY_INFO: u16 = 0x1004;
117    /// Participant security info (spec DDS-Security 1.2 §7.4.1.6
118    /// Tab.18+19). 2x u32 masks at participant level —
119    /// `participant_security_attributes` + `plugin_participant_security_
120    /// attributes`. Controls RTPS-submessage / discovery / liveliness
121    /// protection flags for the whole participant.
122    pub const PARTICIPANT_SECURITY_INFO: u16 = 0x1005;
123    /// PID_IDENTITY_TOKEN (DDS-Security 1.2 §7.4.1.4 Tab.16). The value
124    /// is a CDR-encoded `DataHolder` (`class_id="DDS:Auth:PKI-DH:1.2"` +
125    /// properties `dds.cert.sn`, `dds.cert.algo`, `dds.ca.sn`,
126    /// `dds.ca.algo`). Enables discovery routing and cert-chain bind
127    /// without a full cert in the SPDP announce. Mandatory from
128    /// DDS-Security 1.2 — Cyclone DDS / FastDDS rely on this PID.
129    pub const IDENTITY_TOKEN: u16 = 0x1001;
130    /// PID_PERMISSIONS_TOKEN (DDS-Security 1.2 §7.4.1.5 Tab.17).
131    /// The value is a CDR-encoded `DataHolder`
132    /// (`class_id="DDS:Access:Permissions:1.2"` + properties
133    /// `dds.perm_ca.sn`, `dds.perm_ca.algo`).
134    pub const PERMISSIONS_TOKEN: u16 = 0x1002;
135    /// PID_IDENTITY_STATUS_TOKEN (DDS-Security 1.2 §7.4.1.6, §10.3.2
136    /// Tab.53). The value is a CDR-encoded `DataHolder`. Carrier for the
137    /// OCSP live status (`AuthenticationListener.on_revoke_identity` etc.).
138    pub const IDENTITY_STATUS_TOKEN: u16 = 0x1006;
139    /// PID_PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO
140    /// (DDS-Security 1.2 §7.3.11 + §7.5.1.4). 16 byte: 2 ×
141    /// `AlgorithmRequirements` (trust_chain + message_auth). Spec
142    /// default: RSASSA-PSS + ECDSA-P256.
143    pub const PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO: u16 = 0x1010;
144    /// PID_PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO
145    /// (DDS-Security 1.2 §7.3.12 + §7.5.1.4). 8 byte:
146    /// `AlgorithmRequirements` for DH/ECDH. Spec default:
147    /// DHE-MODP-2048 + ECDHE-CEUM-P256.
148    pub const PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO: u16 = 0x1011;
149    /// PID_PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO
150    /// (DDS-Security 1.2 §7.3.13 + §7.5.1.4). 16 byte: 4 × u32
151    /// (supported + 3 required masks). Spec default:
152    /// AES128 | AES256 supported, AES128 required for all endpoint
153    /// classes.
154    pub const PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO: u16 = 0x1012;
155    /// PID_ENDPOINT_SYMMETRIC_CIPHER_ALGORITHM_INFO (DDS-Security 1.2
156    /// §7.3.15 + §7.5.1.5). 4 byte: required_mask. Per DataWriter/
157    /// DataReader in the Pub/SubscriptionBuiltinTopicData.
158    pub const ENDPOINT_SYMMETRIC_CIPHER_ALGORITHM_INFO: u16 = 0x1013;
159    /// Builtin endpoint set (4 byte u32 bitmask).
160    pub const BUILTIN_ENDPOINT_SET: u16 = 0x0058;
161    /// Data representation (sequence<int16>) — XCDR1/XCDR2 negotiation
162    /// (XTypes §7.6.3.2.2).
163    pub const DATA_REPRESENTATION: u16 = 0x0073;
164    /// Type-Information (TypeInformation payload) — XTypes §7.6.3.2.2.
165    pub const TYPE_INFORMATION: u16 = 0x0075;
166    /// Type-Consistency-Enforcement (4 byte kind + flags) — XTypes
167    /// §7.6.3.7.
168    pub const TYPE_CONSISTENCY_ENFORCEMENT: u16 = 0x0074;
169    /// PID_KEY_HASH (DDSI-RTPS 2.5 §9.6.4.8 + XTypes 1.3 §7.6.8): a
170    /// 16-byte instance identifier in the inline QoS of a DATA/DATA_FRAG.
171    /// Readers and the persistence service correlate samples of the same
172    /// instance via this hash. Computation: PLAIN_CDR2-BE of the @key
173    /// holder, zero-padded if max_size <= 16, otherwise MD5(stream).
174    pub const KEY_HASH: u16 = 0x0070;
175    /// PID_STATUS_INFO (DDSI-RTPS 2.5 §9.6.3.9): 4 byte status word;
176    /// bit 0 = DISPOSED, bit 1 = UNREGISTERED, bit 2 = FILTERED. Sent as
177    /// inline QoS when the sample lifecycle requires it
178    /// (DataWriter::dispose / unregister or content-filter match=false).
179    pub const STATUS_INFO: u16 = 0x0071;
180    /// PID_SHM_LOCATOR (ZeroDDS vendor PID 0x8001, zerodds-flatdata-1.0 §3.1).
181    /// Value: u32 hostname_hash + u32 uid + u32 slot_count + u32 slot_size +
182    /// CDR-string segment_path. From the writer in the discovery sample, a
183    /// reader on the same host (uid + hostname_hash match) attaches to the
184    /// SHM segment. Vendor PID WITHOUT a MUST_UNDERSTAND bit — foreign vendors ignore it.
185    pub const SHM_LOCATOR: u16 = 0x8001;
186    /// PID_ZERODDS_TYPE_ID (ZeroDDS vendor PID 0x8002).
187    /// Value: CDR-encoded `zerodds_types::TypeIdentifier` (XTypes §7.3.4.2),
188    /// little-endian (submessage endianness). Carries the TypeIdentifier
189    /// discrimination of the topic type for XTypes-aware reader-writer
190    /// matching (XTypes §7.6.3.7 + DDS 1.4 §2.2.3 TypeConsistencyEnforcement).
191    /// Vendor PID WITHOUT a MUST_UNDERSTAND bit — foreign vendors ignore it
192    /// and the reader match falls back to a pure `type_name` comparison
193    /// (DDS 1.4 §2.2.3 default path).
194    pub const ZERODDS_TYPE_ID: u16 = 0x8002;
195    /// PID_VENDOR_TRACE_CONTEXT (zerodds-monitor-1.1 §4): an inline-QoS PID
196    /// for W3C trace-context propagation. Value: 2 CDR strings
197    /// (`traceparent` + `tracestate`). It sits in the standard PID range,
198    /// because cross-vendor adoption is desired; receivers without PID
199    /// knowledge ignore it transparently (no MUST_UNDERSTAND bit). The
200    /// encoder/decoder is in the spec-consuming `zerodds-monitor::TraceContextPid`.
201    pub const VENDOR_TRACE_CONTEXT: u16 = 0x0D00;
202    /// PID_COHERENT_SET (DDSI-RTPS 2.5 §9.6.4.2): 8 byte SequenceNumber
203    /// = sequence_number of the first sample in the coherent set. All
204    /// DATA/DATA_FRAG of a set carry this PID in inline QoS. The end of
205    /// the set is signaled by a DATA with PID_COHERENT_SET=new_sn or
206    /// without the PID. Implements WP 2.9 (C2.9 coherent sets).
207    pub const COHERENT_SET: u16 = 0x0056;
208    /// PID_GROUP_COHERENT_SET (DDSI-RTPS 2.5 §9.6.4.3): 8 byte
209    /// SequenceNumber = group_sequence_number of the first sample in the
210    /// group-coherent set (PRESENTATION.access_scope = GROUP).
211    pub const GROUP_COHERENT_SET: u16 = 0x0063;
212    /// PID_GROUP_SEQ_NUM (DDSI-RTPS 2.5 §9.6.4.4): 8 byte SequenceNumber
213    /// = group sequence number of the sample. A mandatory tag for
214    /// publishers with access_scope=GROUP.
215    pub const GROUP_SEQ_NUM: u16 = 0x0064;
216
217    // ----------------------------------------------------------------
218    // DDS-RPC 1.0 discovery PIDs (formal/16-12-04 §7.8.2 + §7.6.2). Set on
219    // the SEDP announce of one half of an RPC endpoint pair and used in the
220    // inline QoS of a reply DATA (`PID_RELATED_SAMPLE_IDENTITY`).
221    // ----------------------------------------------------------------
222
223    /// PID_SERVICE_INSTANCE_NAME (DDS-RPC 1.0 §7.8.2). CDR string =
224    /// logical service-instance name. Allows multiple instances of the
225    /// same service type on one participant.
226    pub const SERVICE_INSTANCE_NAME: u16 = 0x0080;
227    /// PID_RELATED_ENTITY_GUID (DDS-RPC 1.0 §7.8.2). 16 byte = GUID of
228    /// the counterpart endpoint (request writer ↔ reply reader, or
229    /// request reader ↔ reply writer). Binds the two topics into one
230    /// logical RPC endpoint pair.
231    pub const RELATED_ENTITY_GUID: u16 = 0x0081;
232    /// PID_TOPIC_ALIASES (DDS-RPC 1.0 §7.8.2). `sequence<string>` =
233    /// alternative topic names for routing/compat. Order is significant
234    /// (the first element = the preferred alias).
235    pub const TOPIC_ALIASES: u16 = 0x0082;
236    /// PID_RELATED_SAMPLE_IDENTITY (DDS-RPC 1.0 §7.8.2). An inline-QoS PID
237    /// on a reply DATA submessage. 24 byte XCDR2 `SampleIdentity` =
238    /// `request_id` of the correlated request, so the requester can map
239    /// the reply to the corresponding request.
240    pub const RELATED_SAMPLE_IDENTITY: u16 = 0x0083;
241
242    /// PID_IGNORE (XTypes 1.3 §7.4.1.2.1). A padding/filler PID. The
243    /// receiver MUST skip the value and not include it in the
244    /// ParameterList (spec: "Used to ignore parameters which can be
245    /// safely ignored"). Used by encoders as padding between
246    /// variable-length parameters, without disturbing the decoder.
247    pub const IGNORE: u16 = 0x3F03;
248    /// PID_DIRECTED_WRITE (DDSI-RTPS 2.5 §8.7.7 / §9.6.2.2.5). Inline QoS
249    /// on a DATA/DATA_FRAG that addresses exactly one target reader (by
250    /// GUID, 16 byte). Other readers that receive the sample MUST discard
251    /// it. Enables point-to-point paths over a multicast writer (e.g. the
252    /// auth handshake).
253    pub const DIRECTED_WRITE: u16 = 0x0057;
254    /// PID_TYPE_MAX_SIZE_SERIALIZED (spec §9.6.4.7). 4 byte u32 — the
255    /// max wire size of a sample payload in CDR. Used by subscribers to
256    /// check upfront whether the payload fits in `max_dataMaxSize` (DoS
257    /// protection).
258    pub const TYPE_MAX_SIZE_SERIALIZED: u16 = 0x0060;
259    /// PID_ORIGINAL_WRITER_INFO (spec §8.7.9). 24 byte: GUID +
260    /// SequenceNumber of the original writer. Set by the persistence
261    /// service as inline QoS when it forwards a stored sample on behalf
262    /// of another writer (late-joiner replay).
263    pub const ORIGINAL_WRITER_INFO: u16 = 0x0061;
264    /// PID_WRITER_GROUP_INFO (spec §8.7.5 + §9.6.2.2.6). The group
265    /// sequence number of the writer within a publisher group-coherent
266    /// set. Carried in HEARTBEAT.GroupInfo + inline QoS.
267    pub const WRITER_GROUP_INFO: u16 = 0x0062;
268}
269
270/// `true` if `masked_pid` (without the must-understand and vendor bits)
271/// is a PID known in the DDSI-RTPS 2.5 + DDS-Security 1.2 spec set.
272/// Used by [`ParameterList::validate_must_understand_in_data_pipeline`]
273/// to implement the must-understand reject logic (spec §9.4.2.11.2).
274#[must_use]
275pub fn is_standard_pid(masked_pid: u16) -> bool {
276    use pid::*;
277    matches!(
278        masked_pid,
279        SENTINEL
280            | PARTICIPANT_LEASE_DURATION
281            | TOPIC_NAME
282            | TYPE_NAME
283            | PROTOCOL_VERSION
284            | VENDOR_ID
285            | RELIABILITY
286            | DURABILITY
287            | OWNERSHIP
288            | OWNERSHIP_STRENGTH
289            | LIVELINESS
290            | DEADLINE
291            | LIFESPAN
292            | PARTITION
293            | USER_DATA
294            | GROUP_DATA
295            | TOPIC_DATA
296            | CONTENT_FILTER_PROPERTY
297            | DEFAULT_UNICAST_LOCATOR
298            | METATRAFFIC_UNICAST_LOCATOR
299            | METATRAFFIC_MULTICAST_LOCATOR
300            | DOMAIN_ID
301            | DEFAULT_MULTICAST_LOCATOR
302            | UNICAST_LOCATOR
303            | MULTICAST_LOCATOR
304            | PARTICIPANT_GUID
305            | ENDPOINT_GUID
306            | PROPERTY_LIST
307            | ENDPOINT_SECURITY_INFO
308            | PARTICIPANT_SECURITY_INFO
309            | IDENTITY_TOKEN
310            | PERMISSIONS_TOKEN
311            | IDENTITY_STATUS_TOKEN
312            | PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO
313            | PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO
314            | PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO
315            | ENDPOINT_SYMMETRIC_CIPHER_ALGORITHM_INFO
316            | BUILTIN_ENDPOINT_SET
317            | DATA_REPRESENTATION
318            | TYPE_INFORMATION
319            | TYPE_CONSISTENCY_ENFORCEMENT
320            | KEY_HASH
321            | STATUS_INFO
322            | COHERENT_SET
323            | GROUP_COHERENT_SET
324            | GROUP_SEQ_NUM
325            | SERVICE_INSTANCE_NAME
326            | RELATED_ENTITY_GUID
327            | TOPIC_ALIASES
328            | RELATED_SAMPLE_IDENTITY
329            | IGNORE
330            | DIRECTED_WRITE
331            | TYPE_MAX_SIZE_SERIALIZED
332            | ORIGINAL_WRITER_INFO
333            | WRITER_GROUP_INFO
334    )
335}
336
337/// A single parameter (tag + bytes value).
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct Parameter {
340    /// Parameter id (see [`pid`]).
341    pub id: u16,
342    /// Raw value (without padding bytes; the encoder inserts padding up
343    /// to the 4-byte boundary).
344    pub value: Vec<u8>,
345}
346
347impl Parameter {
348    /// Constructor.
349    #[must_use]
350    pub fn new(id: u16, value: Vec<u8>) -> Self {
351        Self { id, value }
352    }
353
354    /// Spec §9.4.2.11.2 — sets the must-understand bit (`0x4000`) on the
355    /// PID. Sender-side: every parameter whose understanding is critical
356    /// for the receiver (e.g. `PID_KEY_HASH` with custom keys) must have
357    /// the bit set.
358    #[must_use]
359    pub fn with_must_understand(mut self) -> Self {
360        self.id |= MUST_UNDERSTAND_BIT;
361        self
362    }
363
364    /// `true` if the must-understand bit is set.
365    #[must_use]
366    pub fn has_must_understand(&self) -> bool {
367        (self.id & MUST_UNDERSTAND_BIT) != 0
368    }
369}
370
371/// ParameterList = a sequence of parameters + a sentinel terminator.
372#[derive(Debug, Clone, PartialEq, Eq)]
373pub struct ParameterList {
374    /// List of parameters (without the sentinel — it is appended
375    /// automatically on encode).
376    pub parameters: Vec<Parameter>,
377}
378
379impl ParameterList {
380    /// Empty list.
381    #[must_use]
382    pub fn new() -> Self {
383        Self {
384            parameters: Vec::new(),
385        }
386    }
387
388    /// Append a parameter.
389    pub fn push(&mut self, param: Parameter) {
390        self.parameters.push(param);
391    }
392
393    /// Find the first parameter with `id`.
394    #[must_use]
395    pub fn find(&self, id: u16) -> Option<&Parameter> {
396        self.parameters.iter().find(|p| p.id == id)
397    }
398
399    /// ALL parameters with `id` (DDSI-RTPS 2.5 §9.4.2.11: a PID may
400    /// appear multiple times, e.g. several `*_UNICAST_LOCATOR` of a
401    /// multi-homed peer).
402    pub fn find_all(&self, id: u16) -> impl Iterator<Item = &Parameter> {
403        self.parameters.iter().filter(move |p| p.id == id)
404    }
405
406    /// Validates the ParameterList against the must-understand rule
407    /// (DDSI-RTPS 2.5 §9.4.2.11.2).
408    ///
409    /// Spec behavior:
410    /// > "If the receiver does not understand a parameter and the
411    /// >  must_understand bit (0x4000) is set, the entire RTPS message
412    /// >  carrying this ParameterList MUST be discarded."
413    ///
414    /// `is_known` is a classifier: returns `true` for all PIDs the
415    /// receiver understands (without the must-understand and vendor
416    /// bits, i.e. the masked PID).
417    ///
418    /// # Errors
419    /// `ValueOutOfRange` with a marker message on violation — the caller
420    /// MUST discard the whole message.
421    pub fn validate_must_understand<F>(&self, is_known: F) -> Result<(), WireError>
422    where
423        F: Fn(u16) -> bool,
424    {
425        for p in &self.parameters {
426            let must_understand = (p.id & MUST_UNDERSTAND_BIT) != 0;
427            if must_understand {
428                let masked = p.id & !(MUST_UNDERSTAND_BIT | VENDOR_SPECIFIC_BIT);
429                if !is_known(masked) {
430                    return Err(WireError::ValueOutOfRange {
431                        message: "ParameterList contains unknown must-understand PID",
432                    });
433                }
434            }
435        }
436        Ok(())
437    }
438
439    /// Convenience wrapper around [`Self::validate_must_understand`]
440    /// with the standard-conformant [`is_standard_pid`] classifier.
441    /// Called in the receiver pipeline hot path
442    /// (`crates/rtps/src/datagram.rs::decode_datagram`), so the
443    /// spec §9.4.2.11.2 reject rule applies automatically.
444    ///
445    /// Vendor-specific PIDs (top bit set) are explicitly allowed — the
446    /// vendor-bit path survives the check in
447    /// `validate_must_understand`, because `masked` strips the vendor
448    /// bit but `is_standard_pid` does not add it back; the caller treats
449    /// vendor PIDs as "can be ignored".
450    ///
451    /// # Errors
452    /// `ValueOutOfRange` on violation — the caller discards the message.
453    pub fn validate_must_understand_in_data_pipeline(&self) -> Result<(), WireError> {
454        for p in &self.parameters {
455            let must_understand = (p.id & MUST_UNDERSTAND_BIT) != 0;
456            if must_understand {
457                // Skip vendor-specific PIDs — the vendor decides for
458                // itself, the standard receiver may ignore them even
459                // with the must-understand bit (spec §9.6.2).
460                if (p.id & VENDOR_SPECIFIC_BIT) != 0 {
461                    continue;
462                }
463                let masked = p.id & !(MUST_UNDERSTAND_BIT | VENDOR_SPECIFIC_BIT);
464                if !is_standard_pid(masked) {
465                    return Err(WireError::ValueOutOfRange {
466                        message: "ParameterList contains unknown must-understand PID",
467                    });
468                }
469            }
470        }
471        Ok(())
472    }
473
474    /// Encodes to bytes with the given endianness. Padding to the
475    /// 4-byte boundary is inserted automatically per value; the sentinel
476    /// is appended.
477    #[must_use]
478    pub fn to_bytes(&self, little_endian: bool) -> Vec<u8> {
479        let mut out = Vec::new();
480        for p in &self.parameters {
481            let padded = padded_to_4(p.value.len());
482            let len_field = padded as u16;
483            write_u16(&mut out, p.id, little_endian);
484            write_u16(&mut out, len_field, little_endian);
485            out.extend_from_slice(&p.value);
486            out.resize(out.len() + (padded - p.value.len()), 0);
487        }
488        // Sentinel: id=0x0001, length=0, no value.
489        write_u16(&mut out, pid::SENTINEL, little_endian);
490        write_u16(&mut out, 0, little_endian);
491        out
492    }
493
494    /// Decodes a ParameterList from bytes. Stops at the sentinel.
495    ///
496    /// # Errors
497    /// `UnexpectedEof` on truncated input; `ValueOutOfRange` if the
498    /// length is not 4-byte aligned; `ValueOutOfRange` if the parameter
499    /// count exceeds [`MAX_PARAMETERS`] (DoS cap).
500    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
501        let mut parameters = Vec::new();
502        let mut pos = 0usize;
503        loop {
504            if bytes.len() < pos + 4 {
505                return Err(WireError::UnexpectedEof {
506                    needed: 4,
507                    offset: pos,
508                });
509            }
510            let id = read_u16(&bytes[pos..pos + 2], little_endian);
511            let length = read_u16(&bytes[pos + 2..pos + 4], little_endian) as usize;
512            pos += 4;
513            if id == pid::SENTINEL {
514                return Ok(Self { parameters });
515            }
516            if length % 4 != 0 {
517                return Err(WireError::ValueOutOfRange {
518                    message: "ParameterList length not 4-byte aligned",
519                });
520            }
521            if bytes.len() < pos + length {
522                return Err(WireError::UnexpectedEof {
523                    needed: length,
524                    offset: pos,
525                });
526            }
527            // PID_IGNORE: spec §7.4.1.2.1 — silently skip without adding
528            // to `parameters`. Still consume the length field + body.
529            if id == pid::IGNORE {
530                pos += length;
531                continue;
532            }
533            if parameters.len() >= MAX_PARAMETERS {
534                return Err(WireError::ValueOutOfRange {
535                    message: "ParameterList exceeds MAX_PARAMETERS cap",
536                });
537            }
538            parameters.push(Parameter {
539                id,
540                value: bytes[pos..pos + length].to_vec(),
541            });
542            pos += length;
543        }
544    }
545}
546
547/// DoS cap for the parameter count in a ParameterList (SEDP/SPDP
548/// amplification protection). 4 096 fits all payloads (realistically
549/// &lt;100 per message); malicious peers can announce u16::MAX=65_535
550/// times `{pid=XXXX, length=0}` and, without a cap, trigger hours-long
551/// iteration.
552pub const MAX_PARAMETERS: usize = 4_096;
553
554/// Spec §9.4.2.11.2 — the must-understand bit of the parameter id. If it
555/// is set and the receiver does not know the PID, the whole message
556/// MUST be discarded.
557pub const MUST_UNDERSTAND_BIT: u16 = 0x4000;
558
559/// Spec §9.4.2.11.2 — Vendor-spezifische PIDs ab `0x8000`.
560pub const VENDOR_SPECIFIC_BIT: u16 = 0x8000;
561
562impl Default for ParameterList {
563    fn default() -> Self {
564        Self::new()
565    }
566}
567
568// ---- Bit-Helpers ----
569
570fn padded_to_4(len: usize) -> usize {
571    (len + 3) & !3
572}
573
574fn write_u16(out: &mut Vec<u8>, v: u16, le: bool) {
575    let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
576    out.extend_from_slice(&bytes);
577}
578
579fn read_u16(bytes: &[u8], le: bool) -> u16 {
580    let mut buf = [0u8; 2];
581    buf.copy_from_slice(&bytes[..2]);
582    if le {
583        u16::from_le_bytes(buf)
584    } else {
585        u16::from_be_bytes(buf)
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    #![allow(clippy::expect_used, clippy::unwrap_used)]
592    use super::*;
593    use alloc::vec;
594
595    #[test]
596    fn padded_to_4_examples() {
597        assert_eq!(padded_to_4(0), 0);
598        assert_eq!(padded_to_4(1), 4);
599        assert_eq!(padded_to_4(3), 4);
600        assert_eq!(padded_to_4(4), 4);
601        assert_eq!(padded_to_4(5), 8);
602        assert_eq!(padded_to_4(16), 16);
603    }
604
605    #[test]
606    fn empty_parameter_list_is_just_sentinel() {
607        let pl = ParameterList::new();
608        let bytes = pl.to_bytes(true);
609        // Sentinel LE: 01 00 00 00 (id=0x0001, length=0)
610        assert_eq!(bytes, vec![0x01, 0x00, 0x00, 0x00]);
611    }
612
613    #[test]
614    fn single_parameter_encodes_id_length_value() {
615        let mut pl = ParameterList::new();
616        pl.push(Parameter::new(0x0015, vec![0x02, 0x05, 0x00, 0x00]));
617        let bytes = pl.to_bytes(true);
618        // id=0x0015 LE = 15 00, length=4 LE = 04 00, value = 02 05 00 00,
619        // sentinel = 01 00 00 00
620        assert_eq!(
621            bytes,
622            vec![
623                0x15, 0x00, 0x04, 0x00, 0x02, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00
624            ]
625        );
626    }
627
628    #[test]
629    fn parameter_value_is_padded_to_4_bytes() {
630        let mut pl = ParameterList::new();
631        // 2 byte value → 2 byte padding
632        pl.push(Parameter::new(0x0015, vec![0x02, 0x05]));
633        let bytes = pl.to_bytes(true);
634        // length field = 4 (padded), value = 02 05 00 00, then the sentinel
635        assert_eq!(bytes[2], 4);
636        assert_eq!(&bytes[4..8], &[0x02, 0x05, 0, 0]);
637    }
638
639    #[test]
640    fn roundtrip_single_parameter() {
641        let mut pl = ParameterList::new();
642        pl.push(Parameter::new(
643            0x0050,
644            vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
645        ));
646        let bytes = pl.to_bytes(true);
647        let decoded = ParameterList::from_bytes(&bytes, true).unwrap();
648        assert_eq!(decoded, pl);
649    }
650
651    #[test]
652    fn roundtrip_multiple_parameters() {
653        let mut pl = ParameterList::new();
654        pl.push(Parameter::new(pid::PROTOCOL_VERSION, vec![2, 5, 0, 0]));
655        pl.push(Parameter::new(pid::VENDOR_ID, vec![0x01, 0xF0, 0, 0]));
656        pl.push(Parameter::new(pid::PARTICIPANT_GUID, vec![0xAA; 16]));
657        let bytes = pl.to_bytes(true);
658        let decoded = ParameterList::from_bytes(&bytes, true).unwrap();
659        assert_eq!(decoded, pl);
660    }
661
662    #[test]
663    fn find_returns_first_parameter_with_id() {
664        let mut pl = ParameterList::new();
665        pl.push(Parameter::new(pid::VENDOR_ID, vec![0x01, 0xF0, 0, 0]));
666        pl.push(Parameter::new(pid::PROTOCOL_VERSION, vec![2, 5, 0, 0]));
667        let p = pl.find(pid::VENDOR_ID).unwrap();
668        assert_eq!(p.value, vec![0x01, 0xF0, 0, 0]);
669    }
670
671    #[test]
672    fn find_returns_none_for_missing_id() {
673        let pl = ParameterList::new();
674        assert!(pl.find(pid::VENDOR_ID).is_none());
675    }
676
677    #[test]
678    fn decode_rejects_non_aligned_length() {
679        // id=0x0015, length=3 (not aligned), value=3 bytes
680        let bytes = vec![0x15, 0x00, 0x03, 0x00, 1, 2, 3, 0x01, 0x00, 0x00, 0x00];
681        let res = ParameterList::from_bytes(&bytes, true);
682        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
683    }
684
685    #[test]
686    fn decode_rejects_truncated_value() {
687        let bytes = vec![0x15, 0x00, 0x08, 0x00, 1, 2, 3]; // length=8, nur 3 byte da
688        let res = ParameterList::from_bytes(&bytes, true);
689        assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
690    }
691
692    #[test]
693    fn decode_rejects_missing_sentinel() {
694        // Only one parameter, then EOF — no sentinel.
695        let bytes = vec![0x15, 0x00, 0x04, 0x00, 1, 2, 3, 4];
696        let res = ParameterList::from_bytes(&bytes, true);
697        assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
698    }
699
700    #[test]
701    fn must_understand_known_pid_passes() {
702        let mut pl = ParameterList::new();
703        // PID 0x4015 = PROTOCOL_VERSION (0x0015) with must-understand bit.
704        pl.push(Parameter::new(
705            MUST_UNDERSTAND_BIT | 0x0015,
706            vec![2, 5, 0, 0],
707        ));
708        assert!(pl.validate_must_understand(|pid| pid == 0x0015).is_ok());
709    }
710
711    #[test]
712    fn must_understand_unknown_pid_rejects() {
713        let mut pl = ParameterList::new();
714        // PID 0x4014 = PID_DOMAIN_TAG (Cyclone often sets it with the
715        // must-understand bit). We pretend we do not know 0x0014.
716        pl.push(Parameter::new(MUST_UNDERSTAND_BIT | 0x0014, vec![0; 4]));
717        let res = pl.validate_must_understand(|pid| pid == 0x0015);
718        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
719    }
720
721    #[test]
722    fn must_understand_unknown_optional_pid_skips() {
723        let mut pl = ParameterList::new();
724        // PID 0x0099 (no Must-Understand bit) — the receiver may skip.
725        pl.push(Parameter::new(0x0099, vec![0; 4]));
726        assert!(pl.validate_must_understand(|pid| pid == 0x0015).is_ok());
727    }
728
729    #[test]
730    fn must_understand_vendor_pid_with_must_understand_rejects() {
731        let mut pl = ParameterList::new();
732        // 0xC042 = vendor PID 0x0042 with the must-understand bit.
733        // We do not know 0x0042. validate_must_understand with a strict
734        // closure rejects.
735        pl.push(Parameter::new(0xC042, vec![0; 4]));
736        let res = pl.validate_must_understand(|_| false);
737        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
738    }
739
740    #[test]
741    fn validate_must_understand_in_data_pipeline_known_pid_passes() {
742        let mut pl = ParameterList::new();
743        pl.push(Parameter::new(
744            MUST_UNDERSTAND_BIT | pid::KEY_HASH,
745            vec![0; 16],
746        ));
747        assert!(pl.validate_must_understand_in_data_pipeline().is_ok());
748    }
749
750    #[test]
751    fn validate_must_understand_in_data_pipeline_unknown_pid_rejects() {
752        let mut pl = ParameterList::new();
753        // 0x3500 is not a standard PID.
754        pl.push(Parameter::new(MUST_UNDERSTAND_BIT | 0x3500, vec![0; 4]));
755        let r = pl.validate_must_understand_in_data_pipeline();
756        assert!(matches!(r, Err(WireError::ValueOutOfRange { .. })));
757    }
758
759    #[test]
760    fn validate_must_understand_in_data_pipeline_vendor_specific_pid_passes() {
761        let mut pl = ParameterList::new();
762        // Vendor-specific PID (bit 15 set) with MU bit — spec
763        // §9.6.2 allows ignoring.
764        pl.push(Parameter::new(
765            MUST_UNDERSTAND_BIT | VENDOR_SPECIFIC_BIT | 0x0050,
766            vec![0xCA, 0xFE, 0xBA, 0xBE],
767        ));
768        assert!(pl.validate_must_understand_in_data_pipeline().is_ok());
769    }
770
771    #[test]
772    fn validate_must_understand_in_data_pipeline_optional_unknown_pid_passes() {
773        let mut pl = ParameterList::new();
774        // Unknown PID WITHOUT the must-understand bit — may be ignored,
775        // no reject.
776        pl.push(Parameter::new(0x3500, vec![0; 4]));
777        assert!(pl.validate_must_understand_in_data_pipeline().is_ok());
778    }
779
780    #[test]
781    fn is_standard_pid_recognises_dds_security_pids() {
782        // Sanity check: DDS-Security 1.2 PIDs count as standard.
783        assert!(is_standard_pid(pid::ENDPOINT_SECURITY_INFO));
784        assert!(is_standard_pid(pid::IDENTITY_TOKEN));
785        assert!(is_standard_pid(pid::PERMISSIONS_TOKEN));
786    }
787
788    #[test]
789    fn is_standard_pid_unknown_pid_returns_false() {
790        // PID 0x3500 is not part of the standard set.
791        assert!(!is_standard_pid(0x3500));
792        assert!(!is_standard_pid(0x9999));
793    }
794
795    #[test]
796    fn must_understand_empty_list_passes() {
797        let pl = ParameterList::new();
798        assert!(pl.validate_must_understand(|_| false).is_ok());
799    }
800
801    #[test]
802    fn rpc_pid_constants_match_spec() {
803        // DDS-RPC 1.0 §7.8.2 — PIDs must have exactly these values,
804        // otherwise Cyclone RPC interop breaks.
805        assert_eq!(pid::SERVICE_INSTANCE_NAME, 0x0080);
806        assert_eq!(pid::RELATED_ENTITY_GUID, 0x0081);
807        assert_eq!(pid::TOPIC_ALIASES, 0x0082);
808        assert_eq!(pid::RELATED_SAMPLE_IDENTITY, 0x0083);
809    }
810
811    #[test]
812    fn rpc_pids_roundtrip_in_parameter_list() {
813        let mut pl = ParameterList::new();
814        pl.push(Parameter::new(pid::SERVICE_INSTANCE_NAME, vec![1, 2, 3, 4]));
815        pl.push(Parameter::new(pid::RELATED_ENTITY_GUID, vec![0xAB; 16]));
816        pl.push(Parameter::new(pid::TOPIC_ALIASES, vec![0xCD; 8]));
817        pl.push(Parameter::new(pid::RELATED_SAMPLE_IDENTITY, vec![0xEF; 24]));
818        let bytes = pl.to_bytes(true);
819        let decoded = ParameterList::from_bytes(&bytes, true).unwrap();
820        assert_eq!(decoded, pl);
821    }
822
823    // ---- PID_IGNORE (XTypes 1.3 §7.4.1.2.1) ----
824
825    #[test]
826    fn pid_ignore_skipped_in_pl_cdr_decode() {
827        // PID_IGNORE 0x3F03 with a 4-byte payload, then PROTOCOL_VERSION
828        // (0x0015) with a 4-byte payload, then the sentinel. The decoder
829        // MUST skip the PID_IGNORE item and return only PROTOCOL_VERSION.
830        let bytes = vec![
831            0x03, 0x3F, 0x04, 0x00, // PID_IGNORE, length=4
832            0xAA, 0xBB, 0xCC, 0xDD, // body (irrelevant)
833            0x15, 0x00, 0x04, 0x00, // PID_PROTOCOL_VERSION, length=4
834            2, 5, 0, 0, // value
835            0x01, 0x00, 0x00, 0x00, // Sentinel
836        ];
837        let pl = ParameterList::from_bytes(&bytes, true).unwrap();
838        assert_eq!(pl.parameters.len(), 1);
839        assert_eq!(pl.parameters[0].id, pid::PROTOCOL_VERSION);
840        assert_eq!(pl.parameters[0].value, vec![2, 5, 0, 0]);
841    }
842
843    #[test]
844    fn pid_ignore_zero_length_is_valid() {
845        // PID_IGNORE with length=0 — also allowed (pure padding marker).
846        let bytes = vec![
847            0x03, 0x3F, 0x00, 0x00, // PID_IGNORE, length=0
848            0x15, 0x00, 0x04, 0x00, // PID_PROTOCOL_VERSION
849            2, 5, 0, 0, 0x01, 0x00, 0x00, 0x00, // Sentinel
850        ];
851        let pl = ParameterList::from_bytes(&bytes, true).unwrap();
852        assert_eq!(pl.parameters.len(), 1);
853        assert_eq!(pl.parameters[0].id, pid::PROTOCOL_VERSION);
854    }
855
856    #[test]
857    fn pid_ignore_truncated_body_rejected() {
858        // PID_IGNORE with length=8, but only 4 bytes of body follow.
859        let bytes = vec![
860            0x03, 0x3F, 0x08, 0x00, // PID_IGNORE, length=8
861            0xAA, 0xBB, 0xCC, 0xDD, // nur 4 byte Body
862            0x01, 0x00, 0x00, 0x00, // would be the sentinel, but is within the announced 8
863        ];
864        let res = ParameterList::from_bytes(&bytes, true);
865        assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
866    }
867
868    #[test]
869    fn pid_ignore_be_decoded() {
870        // BE endianness: id 3F 03, length 00 04, body, then the sentinel.
871        let bytes = vec![
872            0x3F, 0x03, 0x00, 0x04, // PID_IGNORE BE
873            0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x15, 0x00, 0x04, // PROTOCOL_VERSION BE
874            2, 5, 0, 0, 0x00, 0x01, 0x00, 0x00, // Sentinel BE
875        ];
876        let pl = ParameterList::from_bytes(&bytes, false).unwrap();
877        assert_eq!(pl.parameters.len(), 1);
878        assert_eq!(pl.parameters[0].id, pid::PROTOCOL_VERSION);
879    }
880
881    #[test]
882    fn pid_ignore_ignored_even_when_count_would_exceed_cap() {
883        // The MAX_PARAMETERS cap does NOT count PID_IGNORE, because a
884        // silent skip creates no entry. 8192 PID_IGNOREs in a row would
885        // be a DoS risk without this path; with it they are harmless.
886        let mut bytes: Vec<u8> = Vec::new();
887        for _ in 0..50 {
888            bytes.extend_from_slice(&[0x03, 0x3F, 0x00, 0x00]);
889        }
890        bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
891        let pl = ParameterList::from_bytes(&bytes, true).unwrap();
892        assert_eq!(pl.parameters.len(), 0);
893    }
894
895    #[test]
896    fn roundtrip_be_endianness() {
897        let mut pl = ParameterList::new();
898        pl.push(Parameter::new(pid::PROTOCOL_VERSION, vec![2, 5, 0, 0]));
899        let bytes = pl.to_bytes(false);
900        // BE: id 00 15, length 00 04, value, then sentinel 00 01 00 00
901        assert_eq!(&bytes[..4], &[0, 0x15, 0, 4]);
902        let decoded = ParameterList::from_bytes(&bytes, false).unwrap();
903        assert_eq!(decoded, pl);
904    }
905}