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/// <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}