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 fuer SPDP/SEDP Builtin-Topic-Daten. Jeder
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//! kein Value.
20//!
21//! Encoding ist immer mit der Submessage-Endianness; dieses Modul
22//! arbeitet auf rohen Bytes mit explizitem `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 + Tabelle 9.13).
30///
31/// Die 12 QoS-Policy-PIDs aus DDS 1.4 §2.2.3 werden aus
32/// [`zerodds_qos::Pid`] re-exportiert (Single-Source-of-Truth).
33/// rtps-spezifische PIDs (Locators, GUIDs, Security-Tokens, …) bleiben
34/// hier deklariert, da sie ausserhalb des QoS-Policy-Subsets sind.
35pub mod pid {
36 use zerodds_qos::Pid as QosPid;
37
38 // ---- Re-exports aus zerodds_qos::Pid (12 Policy-PIDs) ----
39 /// Sentinel — Terminator der ParameterList. (Re-export aus `zerodds_qos::Pid::SENTINEL`.)
40 pub const SENTINEL: u16 = QosPid::SENTINEL;
41 /// Reliability QoS. (Re-export aus `zerodds_qos::Pid::RELIABILITY`.)
42 pub const RELIABILITY: u16 = QosPid::RELIABILITY;
43 /// Durability QoS. (Re-export aus `zerodds_qos::Pid::DURABILITY`.)
44 pub const DURABILITY: u16 = QosPid::DURABILITY;
45 /// Ownership QoS. (Re-export aus `zerodds_qos::Pid::OWNERSHIP`.)
46 pub const OWNERSHIP: u16 = QosPid::OWNERSHIP;
47 /// Ownership-Strength QoS. (Re-export aus `zerodds_qos::Pid::OWNERSHIP_STRENGTH`.)
48 pub const OWNERSHIP_STRENGTH: u16 = QosPid::OWNERSHIP_STRENGTH;
49 /// Liveliness QoS. (Re-export aus `zerodds_qos::Pid::LIVELINESS`.)
50 pub const LIVELINESS: u16 = QosPid::LIVELINESS;
51 /// Deadline QoS. (Re-export aus `zerodds_qos::Pid::DEADLINE`.)
52 pub const DEADLINE: u16 = QosPid::DEADLINE;
53 /// Lifespan QoS. (Re-export aus `zerodds_qos::Pid::LIFESPAN`.)
54 pub const LIFESPAN: u16 = QosPid::LIFESPAN;
55 /// Partition QoS. (Re-export aus `zerodds_qos::Pid::PARTITION`.)
56 pub const PARTITION: u16 = QosPid::PARTITION;
57 /// UserData QoS. (Re-export aus `zerodds_qos::Pid::USER_DATA`.)
58 pub const USER_DATA: u16 = QosPid::USER_DATA;
59 /// GroupData QoS. (Re-export aus `zerodds_qos::Pid::GROUP_DATA`.)
60 pub const GROUP_DATA: u16 = QosPid::GROUP_DATA;
61 /// TopicData QoS. (Re-export aus `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) — fuer User-Daten.
81 pub const DEFAULT_UNICAST_LOCATOR: u16 = 0x0031;
82 /// Metatraffic-Unicast-Locator (24 byte) — wohin Peers SEDP-Unicast
83 /// schicken. Unverzichtbar fuer Cyclone-Interop.
84 pub const METATRAFFIC_UNICAST_LOCATOR: u16 = 0x0032;
85 /// Metatraffic-Multicast-Locator (24 byte) — wohin Peers SPDP/SEDP
86 /// Multicast schicken.
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 /// Participant-Guid (16 byte).
93 pub const PARTICIPANT_GUID: u16 = 0x0050;
94 /// Endpoint-Guid (16 byte) — fuer Publication/Subscription-Discovery.
95 pub const ENDPOINT_GUID: u16 = 0x005a;
96 /// Property-List (Spec OMG DDS-Security 1.1 §7.2.1). Sequence von
97 /// (name, value)-String-Paaren plus einer leeren oder gefuellten
98 /// BinaryPropertySeq. Traeger fuer Security-Plugin-Klassen,
99 /// Permissions-Tokens und ZeroDDS-Heterogeneous-Security-Caps
100 /// (WP 4H-b).
101 pub const PROPERTY_LIST: u16 = 0x0059;
102 /// Endpoint-Security-Info (Spec OMG DDS-Security 1.1 §7.4.1.5).
103 /// 2x u32 Masken: `endpoint_security_attributes` +
104 /// `plugin_endpoint_security_attributes`. Traeger fuer Endpoint-
105 /// Level-Protection-Flags (WP 4H-c).
106 pub const ENDPOINT_SECURITY_INFO: u16 = 0x1004;
107 /// Participant-Security-Info (Spec DDS-Security 1.2 §7.4.1.6
108 /// Tab.18+19). 2x u32 Masken auf Participant-Level —
109 /// `participant_security_attributes` + `plugin_participant_security_
110 /// attributes`. Steuert RTPS-Submessage / Discovery / Liveliness-
111 /// Protection-Flags fuer den ganzen Participant.
112 pub const PARTICIPANT_SECURITY_INFO: u16 = 0x1005;
113 /// PID_IDENTITY_TOKEN (DDS-Security 1.2 §7.4.1.4 Tab.16). Wert ist
114 /// ein CDR-encoded `DataHolder` (`class_id="DDS:Auth:PKI-DH:1.2"` +
115 /// Properties `dds.cert.sn`, `dds.cert.algo`, `dds.ca.sn`,
116 /// `dds.ca.algo`). Erlaubt Discovery-Routing und Cert-Chain-
117 /// Bind ohne Voll-Cert im SPDP-Announce. Pflicht ab DDS-Security
118 /// 1.2 — Cyclone DDS / FastDDS verlassen sich auf diesen PID.
119 pub const IDENTITY_TOKEN: u16 = 0x1001;
120 /// PID_PERMISSIONS_TOKEN (DDS-Security 1.2 §7.4.1.5 Tab.17).
121 /// Wert ist CDR-encoded `DataHolder`
122 /// (`class_id="DDS:Access:Permissions:1.2"` + Properties
123 /// `dds.perm_ca.sn`, `dds.perm_ca.algo`).
124 pub const PERMISSIONS_TOKEN: u16 = 0x1002;
125 /// PID_IDENTITY_STATUS_TOKEN (DDS-Security 1.2 §7.4.1.6, §10.3.2
126 /// Tab.53). Wert ist CDR-encoded `DataHolder`. Traeger fuer OCSP-
127 /// Live-Status (`AuthenticationListener.on_revoke_identity` etc.).
128 pub const IDENTITY_STATUS_TOKEN: u16 = 0x1006;
129 /// PID_PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO
130 /// (DDS-Security 1.2 §7.3.11 + §7.5.1.4). 16 byte: 2 ×
131 /// `AlgorithmRequirements` (trust_chain + message_auth). Spec-
132 /// Default: RSASSA-PSS + ECDSA-P256.
133 pub const PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO: u16 = 0x1010;
134 /// PID_PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO
135 /// (DDS-Security 1.2 §7.3.12 + §7.5.1.4). 8 byte:
136 /// `AlgorithmRequirements` fuer DH/ECDH. Spec-Default:
137 /// DHE-MODP-2048 + ECDHE-CEUM-P256.
138 pub const PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO: u16 = 0x1011;
139 /// PID_PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO
140 /// (DDS-Security 1.2 §7.3.13 + §7.5.1.4). 16 byte: 4 × u32
141 /// (supported + 3 required-Masks). Spec-Default:
142 /// AES128 | AES256 supported, AES128 fuer alle Endpoint-Klassen
143 /// required.
144 pub const PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO: u16 = 0x1012;
145 /// PID_ENDPOINT_SYMMETRIC_CIPHER_ALGORITHM_INFO (DDS-Security 1.2
146 /// §7.3.15 + §7.5.1.5). 4 byte: required_mask. Pro DataWriter/
147 /// DataReader im Pub/SubscriptionBuiltinTopicData.
148 pub const ENDPOINT_SYMMETRIC_CIPHER_ALGORITHM_INFO: u16 = 0x1013;
149 /// Builtin-Endpoint-Set (4 byte u32 Bitmask).
150 pub const BUILTIN_ENDPOINT_SET: u16 = 0x0058;
151 /// Data-Representation (sequence<int16>) — XCDR1/XCDR2-Negotiation
152 /// (XTypes §7.6.3.2.2).
153 pub const DATA_REPRESENTATION: u16 = 0x0073;
154 /// Type-Information (TypeInformation payload) — XTypes §7.6.3.2.2.
155 pub const TYPE_INFORMATION: u16 = 0x0075;
156 /// Type-Consistency-Enforcement (4 byte kind + flags) — XTypes
157 /// §7.6.3.7.
158 pub const TYPE_CONSISTENCY_ENFORCEMENT: u16 = 0x0074;
159 /// PID_KEY_HASH (DDSI-RTPS 2.5 §9.6.4.8 + XTypes 1.3 §7.6.8): 16-Byte
160 /// Instance-Identifier in Inline-QoS einer DATA/DATA_FRAG. Reader und
161 /// Persistence-Service korrelieren Samples derselben Instanz ueber
162 /// diesen Hash. Berechnung: PLAIN_CDR2-BE des @key-Holders, bei
163 /// max_size <= 16 zero-padded, sonst MD5(stream).
164 pub const KEY_HASH: u16 = 0x0070;
165 /// PID_STATUS_INFO (DDSI-RTPS 2.5 §9.6.3.9): 4 byte Statusword;
166 /// Bit 0 = DISPOSED, Bit 1 = UNREGISTERED, Bit 2 = FILTERED. Wird
167 /// als Inline-QoS gesendet, wenn der Sample-Lifecycle das verlangt
168 /// (DataWriter::dispose / unregister oder ContentFilter-Match=False).
169 pub const STATUS_INFO: u16 = 0x0071;
170 /// PID_SHM_LOCATOR (ZeroDDS Vendor-PID 0x8001, zerodds-flatdata-1.0 §3.1).
171 /// Wert: u32 hostname_hash + u32 uid + u32 slot_count + u32 slot_size +
172 /// CDR-String segment_path. Vom Writer im Discovery-Sample, Reader auf
173 /// demselben Host (uid + hostname_hash match) attached an SHM-Segment.
174 /// Vendor-PID OHNE MUST_UNDERSTAND-Bit — fremde Vendoren ignorieren.
175 pub const SHM_LOCATOR: u16 = 0x8001;
176 /// PID_ZERODDS_TYPE_ID (ZeroDDS Vendor-PID 0x8002).
177 /// Wert: CDR-encoded `zerodds_types::TypeIdentifier` (XTypes §7.3.4.2),
178 /// Little-Endian (Submessage-Endianness). Trägt die TypeIdentifier-
179 /// Diskrimination der Topic-Type fuer XTypes-aware Reader-Writer-
180 /// Matching (XTypes §7.6.3.7 + DDS 1.4 §2.2.3 TypeConsistencyEnforcement).
181 /// Vendor-PID OHNE MUST_UNDERSTAND-Bit — fremde Vendoren ignorieren
182 /// und der Reader-Match faellt zurueck auf reinen `type_name`-Vergleich
183 /// (DDS 1.4 §2.2.3 Default-Path).
184 pub const ZERODDS_TYPE_ID: u16 = 0x8002;
185 /// PID_VENDOR_TRACE_CONTEXT (zerodds-monitor-1.0 §4): Inline-QoS-PID
186 /// fuer W3C-Trace-Context-Propagation. Wert: 2 CDR-Strings
187 /// (`traceparent` + `tracestate`). Steht im Standard-PID-Range, weil
188 /// Cross-Vendor-Adoption gewuenscht ist; Empfaenger ohne PID-Kenntnis
189 /// ignorieren transparent (kein MUST_UNDERSTAND-Bit). Encoder/Decoder
190 /// im Spec-konsumierenden `zerodds-monitor::TraceContextPid`.
191 pub const VENDOR_TRACE_CONTEXT: u16 = 0x0D00;
192 /// PID_COHERENT_SET (DDSI-RTPS 2.5 §9.6.4.2): 8 byte SequenceNumber
193 /// = sequence_number des ersten Sample im Coherent-Set. Alle DATA/
194 /// DATA_FRAG eines Sets tragen diesen PID in Inline-QoS. Set-Ende
195 /// signalisiert eine DATA mit PID_COHERENT_SET=neue_sn oder ohne PID.
196 /// Implementiert WP 2.9 (C2.9 Coherent-Sets).
197 pub const COHERENT_SET: u16 = 0x0056;
198 /// PID_GROUP_COHERENT_SET (DDSI-RTPS 2.5 §9.6.4.3): 8 byte
199 /// SequenceNumber = group_sequence_number des ersten Sample im
200 /// Group-Coherent-Set (PRESENTATION.access_scope = GROUP).
201 pub const GROUP_COHERENT_SET: u16 = 0x0063;
202 /// PID_GROUP_SEQ_NUM (DDSI-RTPS 2.5 §9.6.4.4): 8 byte SequenceNumber
203 /// = group sequence number des Sample. Pflicht-Tag fuer Publisher
204 /// mit access_scope=GROUP.
205 pub const GROUP_SEQ_NUM: u16 = 0x0064;
206
207 // ----------------------------------------------------------------
208 // DDS-RPC 1.0 Discovery-PIDs (formal/16-12-04 §7.8.2 + §7.6.2). Werden
209 // beim SEDP-Announce einer RPC-Endpoint-Pair-Haelfte gesetzt und in der
210 // Inline-QoS einer Reply-DATA (`PID_RELATED_SAMPLE_IDENTITY`) genutzt.
211 // ----------------------------------------------------------------
212
213 /// PID_SERVICE_INSTANCE_NAME (DDS-RPC 1.0 §7.8.2). CDR-String =
214 /// logischer Service-Instance-Name. Erlaubt mehrere Instanzen
215 /// desselben Service-Typs auf einem Participant.
216 pub const SERVICE_INSTANCE_NAME: u16 = 0x0080;
217 /// PID_RELATED_ENTITY_GUID (DDS-RPC 1.0 §7.8.2). 16 byte = GUID des
218 /// Pendant-Endpoints (Request-Writer ↔ Reply-Reader bzw.
219 /// Request-Reader ↔ Reply-Writer). Bindet die zwei Topics zu einem
220 /// logischen RPC-Endpoint-Pair.
221 pub const RELATED_ENTITY_GUID: u16 = 0x0081;
222 /// PID_TOPIC_ALIASES (DDS-RPC 1.0 §7.8.2). `sequence<string>` =
223 /// alternative Topic-Namen fuer Routing/Compat. Reihenfolge ist
224 /// signifikant (erstes Element = bevorzugter Alias).
225 pub const TOPIC_ALIASES: u16 = 0x0082;
226 /// PID_RELATED_SAMPLE_IDENTITY (DDS-RPC 1.0 §7.8.2). Inline-QoS-PID
227 /// auf einer Reply-DATA-Submessage. 24 byte XCDR2-`SampleIdentity` =
228 /// `request_id` der korrelierten Request, damit der Requester den
229 /// Reply der zugehoerigen Anfrage zuordnen kann.
230 pub const RELATED_SAMPLE_IDENTITY: u16 = 0x0083;
231
232 /// PID_IGNORE (XTypes 1.3 §7.4.1.2.1). Padding-/Filler-PID. Receiver
233 /// MUSS Wert ueberlesen und nicht in die ParameterList aufnehmen
234 /// (Spec: "Used to ignore parameters which can be safely ignored").
235 /// Wird von Encodern als Padding zwischen variablen-laengen Parametern
236 /// verwendet, ohne den Decoder zu stoeren.
237 pub const IGNORE: u16 = 0x3F03;
238 /// PID_DIRECTED_WRITE (DDSI-RTPS 2.5 §8.7.7 / §9.6.2.2.5). Inline-QoS
239 /// auf einer DATA/DATA_FRAG, die exakt einen Ziel-Reader (per GUID,
240 /// 16 byte) adressiert. Andere Reader, die das Sample empfangen,
241 /// MUESSEN es verwerfen. Ermoeglicht Punkt-zu-Punkt-Pfade ueber
242 /// einen Multicast-Writer (z.B. Auth-Handshake).
243 pub const DIRECTED_WRITE: u16 = 0x0057;
244 /// PID_TYPE_MAX_SIZE_SERIALIZED (Spec §9.6.4.7). 4 byte u32 — die
245 /// max-Wire-Groesse eines Samples-Payloads in CDR. Wird fuer
246 /// Subscriber genutzt, um vorab zu pruefen, ob die Payload in
247 /// `max_dataMaxSize` paesst (DoS-Schutz).
248 pub const TYPE_MAX_SIZE_SERIALIZED: u16 = 0x0060;
249 /// PID_ORIGINAL_WRITER_INFO (Spec §8.7.9). 24 byte: GUID +
250 /// SequenceNumber des urspruenglichen Writers. Wird vom
251 /// Persistence-Service als Inline-QoS gesetzt, wenn er ein
252 /// gespeichertes Sample im Auftrag eines anderen Writers
253 /// weiterleitet (Late-Joiner-Replay).
254 pub const ORIGINAL_WRITER_INFO: u16 = 0x0061;
255 /// PID_WRITER_GROUP_INFO (Spec §8.7.5 + §9.6.2.2.6). Group-Sequence-
256 /// Nummer des Writers innerhalb eines Publisher-Group-Coherent-
257 /// Sets. Wird in HEARTBEAT.GroupInfo + InlineQoS getragen.
258 pub const WRITER_GROUP_INFO: u16 = 0x0062;
259}
260
261/// `true` wenn `masked_pid` (ohne Must-Understand- und Vendor-Bits)
262/// ein im DDSI-RTPS 2.5 + DDS-Security 1.2 Spec-Set bekannter PID ist.
263/// Wird von [`ParameterList::validate_must_understand_in_data_pipeline`]
264/// genutzt, um Must-Understand-Reject-Logik (Spec §9.4.2.11.2) zu
265/// implementieren.
266#[must_use]
267pub fn is_standard_pid(masked_pid: u16) -> bool {
268 use pid::*;
269 matches!(
270 masked_pid,
271 SENTINEL
272 | PARTICIPANT_LEASE_DURATION
273 | TOPIC_NAME
274 | TYPE_NAME
275 | PROTOCOL_VERSION
276 | VENDOR_ID
277 | RELIABILITY
278 | DURABILITY
279 | OWNERSHIP
280 | OWNERSHIP_STRENGTH
281 | LIVELINESS
282 | DEADLINE
283 | LIFESPAN
284 | PARTITION
285 | USER_DATA
286 | GROUP_DATA
287 | TOPIC_DATA
288 | CONTENT_FILTER_PROPERTY
289 | DEFAULT_UNICAST_LOCATOR
290 | METATRAFFIC_UNICAST_LOCATOR
291 | METATRAFFIC_MULTICAST_LOCATOR
292 | DOMAIN_ID
293 | DEFAULT_MULTICAST_LOCATOR
294 | PARTICIPANT_GUID
295 | ENDPOINT_GUID
296 | PROPERTY_LIST
297 | ENDPOINT_SECURITY_INFO
298 | PARTICIPANT_SECURITY_INFO
299 | IDENTITY_TOKEN
300 | PERMISSIONS_TOKEN
301 | IDENTITY_STATUS_TOKEN
302 | PARTICIPANT_SECURITY_DIGITAL_SIGNATURE_ALGORITHM_INFO
303 | PARTICIPANT_SECURITY_KEY_ESTABLISHMENT_ALGORITHM_INFO
304 | PARTICIPANT_SECURITY_SYMMETRIC_CIPHER_ALGORITHM_INFO
305 | ENDPOINT_SYMMETRIC_CIPHER_ALGORITHM_INFO
306 | BUILTIN_ENDPOINT_SET
307 | DATA_REPRESENTATION
308 | TYPE_INFORMATION
309 | TYPE_CONSISTENCY_ENFORCEMENT
310 | KEY_HASH
311 | STATUS_INFO
312 | COHERENT_SET
313 | GROUP_COHERENT_SET
314 | GROUP_SEQ_NUM
315 | SERVICE_INSTANCE_NAME
316 | RELATED_ENTITY_GUID
317 | TOPIC_ALIASES
318 | RELATED_SAMPLE_IDENTITY
319 | IGNORE
320 | DIRECTED_WRITE
321 | TYPE_MAX_SIZE_SERIALIZED
322 | ORIGINAL_WRITER_INFO
323 | WRITER_GROUP_INFO
324 )
325}
326
327/// Ein einzelner Parameter (Tag + Bytes-Wert).
328#[derive(Debug, Clone, PartialEq, Eq)]
329pub struct Parameter {
330 /// Parameter-ID (siehe [`pid`]).
331 pub id: u16,
332 /// Roher Value (ohne Padding-Bytes; Encoder fuegt Padding bis
333 /// zur 4-Byte-Boundary ein).
334 pub value: Vec<u8>,
335}
336
337impl Parameter {
338 /// Konstruktor.
339 #[must_use]
340 pub fn new(id: u16, value: Vec<u8>) -> Self {
341 Self { id, value }
342 }
343
344 /// Spec §9.4.2.11.2 — setzt das Must-Understand-Bit (`0x4000`)
345 /// auf der PID. Sender-Side: jeder Parameter, dessen Verstaendnis
346 /// fuer den Receiver kritisch ist (z.B. `PID_KEY_HASH` bei
347 /// custom keys), muss das Bit gesetzt haben.
348 #[must_use]
349 pub fn with_must_understand(mut self) -> Self {
350 self.id |= MUST_UNDERSTAND_BIT;
351 self
352 }
353
354 /// `true` wenn das Must-Understand-Bit gesetzt ist.
355 #[must_use]
356 pub fn has_must_understand(&self) -> bool {
357 (self.id & MUST_UNDERSTAND_BIT) != 0
358 }
359}
360
361/// ParameterList = Sequenz von Parametern + Sentinel-Terminator.
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub struct ParameterList {
364 /// Liste der Parameter (ohne Sentinel — der wird automatisch
365 /// beim Encode angefuegt).
366 pub parameters: Vec<Parameter>,
367}
368
369impl ParameterList {
370 /// Leere Liste.
371 #[must_use]
372 pub fn new() -> Self {
373 Self {
374 parameters: Vec::new(),
375 }
376 }
377
378 /// Parameter anhaengen.
379 pub fn push(&mut self, param: Parameter) {
380 self.parameters.push(param);
381 }
382
383 /// Erste Parameter mit `id` finden.
384 #[must_use]
385 pub fn find(&self, id: u16) -> Option<&Parameter> {
386 self.parameters.iter().find(|p| p.id == id)
387 }
388
389 /// Validiert die ParameterList gegen die Must-Understand-Regel
390 /// (DDSI-RTPS 2.5 §9.4.2.11.2).
391 ///
392 /// Spec-Verhalten:
393 /// > "If the receiver does not understand a parameter and the
394 /// > must_understand bit (0x4000) is set, the entire RTPS message
395 /// > carrying this ParameterList MUST be discarded."
396 ///
397 /// `is_known` ist ein Klassifikator: liefert `true` fuer alle PIDs,
398 /// die der Receiver versteht (ohne Must-Understand- und Vendor-
399 /// Bits, also masked PID).
400 ///
401 /// # Errors
402 /// `ValueOutOfRange` mit Marker-Message bei Verletzung — der
403 /// Caller MUSS die Ganz-Message verwerfen.
404 pub fn validate_must_understand<F>(&self, is_known: F) -> Result<(), WireError>
405 where
406 F: Fn(u16) -> bool,
407 {
408 for p in &self.parameters {
409 let must_understand = (p.id & MUST_UNDERSTAND_BIT) != 0;
410 if must_understand {
411 let masked = p.id & !(MUST_UNDERSTAND_BIT | VENDOR_SPECIFIC_BIT);
412 if !is_known(masked) {
413 return Err(WireError::ValueOutOfRange {
414 message: "ParameterList contains unknown must-understand PID",
415 });
416 }
417 }
418 }
419 Ok(())
420 }
421
422 /// Convenience-Wrapper um [`Self::validate_must_understand`] mit
423 /// dem standard-konformen [`is_standard_pid`]-Klassifikator.
424 /// Wird im Receiver-Pipeline-Hot-Path gerufen
425 /// (`crates/rtps/src/datagram.rs::decode_datagram`), sodass die
426 /// Spec-§9.4.2.11.2-Reject-Regel automatisch greift.
427 ///
428 /// Vendor-Specific-PIDs (oberstes Bit gesetzt) sind explizit
429 /// erlaubt — der Vendor-Bit-Pfad ueberlebt die Pruefung in
430 /// `validate_must_understand`, weil das `masked` den Vendor-Bit
431 /// strippt aber `is_standard_pid` ihn nicht zurueckaddiert; der
432 /// Caller behandelt Vendor-PIDs als "kann ignoriert werden".
433 ///
434 /// # Errors
435 /// `ValueOutOfRange` bei Verletzung — Caller verwirft die
436 /// Message.
437 pub fn validate_must_understand_in_data_pipeline(&self) -> Result<(), WireError> {
438 for p in &self.parameters {
439 let must_understand = (p.id & MUST_UNDERSTAND_BIT) != 0;
440 if must_understand {
441 // Vendor-Specific-PIDs ueberspringen — Vendor entscheidet
442 // selbst, der Standard-Receiver darf sie auch mit
443 // Must-Understand-Bit ignorieren (Spec §9.6.2).
444 if (p.id & VENDOR_SPECIFIC_BIT) != 0 {
445 continue;
446 }
447 let masked = p.id & !(MUST_UNDERSTAND_BIT | VENDOR_SPECIFIC_BIT);
448 if !is_standard_pid(masked) {
449 return Err(WireError::ValueOutOfRange {
450 message: "ParameterList contains unknown must-understand PID",
451 });
452 }
453 }
454 }
455 Ok(())
456 }
457
458 /// Encoded zu Bytes mit der gegebenen Endianness. Padding zu
459 /// 4-Byte-Boundary wird pro Value automatisch eingefuegt; der
460 /// Sentinel wird angefuegt.
461 #[must_use]
462 pub fn to_bytes(&self, little_endian: bool) -> Vec<u8> {
463 let mut out = Vec::new();
464 for p in &self.parameters {
465 let padded = padded_to_4(p.value.len());
466 let len_field = padded as u16;
467 write_u16(&mut out, p.id, little_endian);
468 write_u16(&mut out, len_field, little_endian);
469 out.extend_from_slice(&p.value);
470 out.resize(out.len() + (padded - p.value.len()), 0);
471 }
472 // Sentinel: id=0x0001, length=0, no value.
473 write_u16(&mut out, pid::SENTINEL, little_endian);
474 write_u16(&mut out, 0, little_endian);
475 out
476 }
477
478 /// Decoded eine ParameterList aus Bytes. Stoppt am Sentinel.
479 ///
480 /// # Errors
481 /// `UnexpectedEof` bei truncated Eingabe; `ValueOutOfRange` wenn
482 /// Length nicht 4-Byte-aligned ist; `ValueOutOfRange` wenn die
483 /// Parameter-Zahl [`MAX_PARAMETERS`] uebersteigt (DoS-Cap).
484 pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
485 let mut parameters = Vec::new();
486 let mut pos = 0usize;
487 loop {
488 if bytes.len() < pos + 4 {
489 return Err(WireError::UnexpectedEof {
490 needed: 4,
491 offset: pos,
492 });
493 }
494 let id = read_u16(&bytes[pos..pos + 2], little_endian);
495 let length = read_u16(&bytes[pos + 2..pos + 4], little_endian) as usize;
496 pos += 4;
497 if id == pid::SENTINEL {
498 return Ok(Self { parameters });
499 }
500 if length % 4 != 0 {
501 return Err(WireError::ValueOutOfRange {
502 message: "ParameterList length not 4-byte aligned",
503 });
504 }
505 if bytes.len() < pos + length {
506 return Err(WireError::UnexpectedEof {
507 needed: length,
508 offset: pos,
509 });
510 }
511 // PID_IGNORE: Spec §7.4.1.2.1 — silently skip ohne Aufnahme
512 // in `parameters`. Length-Feld + Body trotzdem konsumieren.
513 if id == pid::IGNORE {
514 pos += length;
515 continue;
516 }
517 if parameters.len() >= MAX_PARAMETERS {
518 return Err(WireError::ValueOutOfRange {
519 message: "ParameterList exceeds MAX_PARAMETERS cap",
520 });
521 }
522 parameters.push(Parameter {
523 id,
524 value: bytes[pos..pos + length].to_vec(),
525 });
526 pos += length;
527 }
528 }
529}
530
531/// DoS-Cap fuer Parameter-Anzahl in einer ParameterList (SEDP/SPDP-
532/// Amplification-Schutz). 4 096 passt für alle Payloads
533/// (realistisch <100 pro Message); boese Peers koennen u16::MAX=65_535
534/// Mal `{pid=XXXX, length=0}` ankuendigen und ohne Cap Stunden-lange
535/// Iteration auslosen.
536pub const MAX_PARAMETERS: usize = 4_096;
537
538/// Spec §9.4.2.11.2 — Must-Understand-Bit der Parameter-ID. Ist es
539/// gesetzt und kennt der Receiver den PID nicht, MUSS die ganze
540/// Message verworfen werden.
541pub const MUST_UNDERSTAND_BIT: u16 = 0x4000;
542
543/// Spec §9.4.2.11.2 — Vendor-spezifische PIDs ab `0x8000`.
544pub const VENDOR_SPECIFIC_BIT: u16 = 0x8000;
545
546impl Default for ParameterList {
547 fn default() -> Self {
548 Self::new()
549 }
550}
551
552// ---- Bit-Helpers ----
553
554fn padded_to_4(len: usize) -> usize {
555 (len + 3) & !3
556}
557
558fn write_u16(out: &mut Vec<u8>, v: u16, le: bool) {
559 let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
560 out.extend_from_slice(&bytes);
561}
562
563fn read_u16(bytes: &[u8], le: bool) -> u16 {
564 let mut buf = [0u8; 2];
565 buf.copy_from_slice(&bytes[..2]);
566 if le {
567 u16::from_le_bytes(buf)
568 } else {
569 u16::from_be_bytes(buf)
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 #![allow(clippy::expect_used, clippy::unwrap_used)]
576 use super::*;
577 use alloc::vec;
578
579 #[test]
580 fn padded_to_4_examples() {
581 assert_eq!(padded_to_4(0), 0);
582 assert_eq!(padded_to_4(1), 4);
583 assert_eq!(padded_to_4(3), 4);
584 assert_eq!(padded_to_4(4), 4);
585 assert_eq!(padded_to_4(5), 8);
586 assert_eq!(padded_to_4(16), 16);
587 }
588
589 #[test]
590 fn empty_parameter_list_is_just_sentinel() {
591 let pl = ParameterList::new();
592 let bytes = pl.to_bytes(true);
593 // Sentinel LE: 01 00 00 00 (id=0x0001, length=0)
594 assert_eq!(bytes, vec![0x01, 0x00, 0x00, 0x00]);
595 }
596
597 #[test]
598 fn single_parameter_encodes_id_length_value() {
599 let mut pl = ParameterList::new();
600 pl.push(Parameter::new(0x0015, vec![0x02, 0x05, 0x00, 0x00]));
601 let bytes = pl.to_bytes(true);
602 // id=0x0015 LE = 15 00, length=4 LE = 04 00, value = 02 05 00 00,
603 // sentinel = 01 00 00 00
604 assert_eq!(
605 bytes,
606 vec![
607 0x15, 0x00, 0x04, 0x00, 0x02, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00
608 ]
609 );
610 }
611
612 #[test]
613 fn parameter_value_is_padded_to_4_bytes() {
614 let mut pl = ParameterList::new();
615 // 2 byte value → 2 byte padding
616 pl.push(Parameter::new(0x0015, vec![0x02, 0x05]));
617 let bytes = pl.to_bytes(true);
618 // length-Feld = 4 (padded), value = 02 05 00 00, dann sentinel
619 assert_eq!(bytes[2], 4);
620 assert_eq!(&bytes[4..8], &[0x02, 0x05, 0, 0]);
621 }
622
623 #[test]
624 fn roundtrip_single_parameter() {
625 let mut pl = ParameterList::new();
626 pl.push(Parameter::new(
627 0x0050,
628 vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
629 ));
630 let bytes = pl.to_bytes(true);
631 let decoded = ParameterList::from_bytes(&bytes, true).unwrap();
632 assert_eq!(decoded, pl);
633 }
634
635 #[test]
636 fn roundtrip_multiple_parameters() {
637 let mut pl = ParameterList::new();
638 pl.push(Parameter::new(pid::PROTOCOL_VERSION, vec![2, 5, 0, 0]));
639 pl.push(Parameter::new(pid::VENDOR_ID, vec![0x01, 0xF0, 0, 0]));
640 pl.push(Parameter::new(pid::PARTICIPANT_GUID, vec![0xAA; 16]));
641 let bytes = pl.to_bytes(true);
642 let decoded = ParameterList::from_bytes(&bytes, true).unwrap();
643 assert_eq!(decoded, pl);
644 }
645
646 #[test]
647 fn find_returns_first_parameter_with_id() {
648 let mut pl = ParameterList::new();
649 pl.push(Parameter::new(pid::VENDOR_ID, vec![0x01, 0xF0, 0, 0]));
650 pl.push(Parameter::new(pid::PROTOCOL_VERSION, vec![2, 5, 0, 0]));
651 let p = pl.find(pid::VENDOR_ID).unwrap();
652 assert_eq!(p.value, vec![0x01, 0xF0, 0, 0]);
653 }
654
655 #[test]
656 fn find_returns_none_for_missing_id() {
657 let pl = ParameterList::new();
658 assert!(pl.find(pid::VENDOR_ID).is_none());
659 }
660
661 #[test]
662 fn decode_rejects_non_aligned_length() {
663 // id=0x0015, length=3 (nicht aligned), value=3 bytes
664 let bytes = vec![0x15, 0x00, 0x03, 0x00, 1, 2, 3, 0x01, 0x00, 0x00, 0x00];
665 let res = ParameterList::from_bytes(&bytes, true);
666 assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
667 }
668
669 #[test]
670 fn decode_rejects_truncated_value() {
671 let bytes = vec![0x15, 0x00, 0x08, 0x00, 1, 2, 3]; // length=8, nur 3 byte da
672 let res = ParameterList::from_bytes(&bytes, true);
673 assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
674 }
675
676 #[test]
677 fn decode_rejects_missing_sentinel() {
678 // Nur ein Parameter, dann EOF — kein Sentinel.
679 let bytes = vec![0x15, 0x00, 0x04, 0x00, 1, 2, 3, 4];
680 let res = ParameterList::from_bytes(&bytes, true);
681 assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
682 }
683
684 #[test]
685 fn must_understand_known_pid_passes() {
686 let mut pl = ParameterList::new();
687 // PID 0x4015 = PROTOCOL_VERSION (0x0015) mit Must-Understand-Bit.
688 pl.push(Parameter::new(
689 MUST_UNDERSTAND_BIT | 0x0015,
690 vec![2, 5, 0, 0],
691 ));
692 assert!(pl.validate_must_understand(|pid| pid == 0x0015).is_ok());
693 }
694
695 #[test]
696 fn must_understand_unknown_pid_rejects() {
697 let mut pl = ParameterList::new();
698 // PID 0x4014 = PID_DOMAIN_TAG (Cyclone setzt das oft mit
699 // Must-Understand-Bit). Wir tun so, als kennten wir 0x0014 nicht.
700 pl.push(Parameter::new(MUST_UNDERSTAND_BIT | 0x0014, vec![0; 4]));
701 let res = pl.validate_must_understand(|pid| pid == 0x0015);
702 assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
703 }
704
705 #[test]
706 fn must_understand_unknown_optional_pid_skips() {
707 let mut pl = ParameterList::new();
708 // PID 0x0099 (kein Must-Understand-Bit) — Receiver darf skippen.
709 pl.push(Parameter::new(0x0099, vec![0; 4]));
710 assert!(pl.validate_must_understand(|pid| pid == 0x0015).is_ok());
711 }
712
713 #[test]
714 fn must_understand_vendor_pid_with_must_understand_rejects() {
715 let mut pl = ParameterList::new();
716 // 0xC042 = vendor-PID 0x0042 mit Must-Understand-Bit.
717 // Wir kennen 0x0042 nicht. validate_must_understand mit
718 // strikter Closure rejected.
719 pl.push(Parameter::new(0xC042, vec![0; 4]));
720 let res = pl.validate_must_understand(|_| false);
721 assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
722 }
723
724 #[test]
725 fn validate_must_understand_in_data_pipeline_known_pid_passes() {
726 let mut pl = ParameterList::new();
727 pl.push(Parameter::new(
728 MUST_UNDERSTAND_BIT | pid::KEY_HASH,
729 vec![0; 16],
730 ));
731 assert!(pl.validate_must_understand_in_data_pipeline().is_ok());
732 }
733
734 #[test]
735 fn validate_must_understand_in_data_pipeline_unknown_pid_rejects() {
736 let mut pl = ParameterList::new();
737 // 0x3500 ist kein Standard-PID.
738 pl.push(Parameter::new(MUST_UNDERSTAND_BIT | 0x3500, vec![0; 4]));
739 let r = pl.validate_must_understand_in_data_pipeline();
740 assert!(matches!(r, Err(WireError::ValueOutOfRange { .. })));
741 }
742
743 #[test]
744 fn validate_must_understand_in_data_pipeline_vendor_specific_pid_passes() {
745 let mut pl = ParameterList::new();
746 // Vendor-spezifischer PID (Bit 15 gesetzt) mit MU-Bit — Spec
747 // §9.6.2 erlaubt ignorieren.
748 pl.push(Parameter::new(
749 MUST_UNDERSTAND_BIT | VENDOR_SPECIFIC_BIT | 0x0050,
750 vec![0xCA, 0xFE, 0xBA, 0xBE],
751 ));
752 assert!(pl.validate_must_understand_in_data_pipeline().is_ok());
753 }
754
755 #[test]
756 fn validate_must_understand_in_data_pipeline_optional_unknown_pid_passes() {
757 let mut pl = ParameterList::new();
758 // Unbekannter PID OHNE Must-Understand-Bit — darf ignoriert
759 // werden, kein Reject.
760 pl.push(Parameter::new(0x3500, vec![0; 4]));
761 assert!(pl.validate_must_understand_in_data_pipeline().is_ok());
762 }
763
764 #[test]
765 fn is_standard_pid_recognises_dds_security_pids() {
766 // Sanity-Check: DDS-Security 1.2 PIDs zaehlen als Standard.
767 assert!(is_standard_pid(pid::ENDPOINT_SECURITY_INFO));
768 assert!(is_standard_pid(pid::IDENTITY_TOKEN));
769 assert!(is_standard_pid(pid::PERMISSIONS_TOKEN));
770 }
771
772 #[test]
773 fn is_standard_pid_unknown_pid_returns_false() {
774 // PID 0x3500 ist nicht Teil des Standard-Sets.
775 assert!(!is_standard_pid(0x3500));
776 assert!(!is_standard_pid(0x9999));
777 }
778
779 #[test]
780 fn must_understand_empty_list_passes() {
781 let pl = ParameterList::new();
782 assert!(pl.validate_must_understand(|_| false).is_ok());
783 }
784
785 #[test]
786 fn rpc_pid_constants_match_spec() {
787 // DDS-RPC 1.0 §7.8.2 — PIDs muessen exakt diese Werte haben,
788 // sonst bricht Cyclone-RPC-Interop.
789 assert_eq!(pid::SERVICE_INSTANCE_NAME, 0x0080);
790 assert_eq!(pid::RELATED_ENTITY_GUID, 0x0081);
791 assert_eq!(pid::TOPIC_ALIASES, 0x0082);
792 assert_eq!(pid::RELATED_SAMPLE_IDENTITY, 0x0083);
793 }
794
795 #[test]
796 fn rpc_pids_roundtrip_in_parameter_list() {
797 let mut pl = ParameterList::new();
798 pl.push(Parameter::new(pid::SERVICE_INSTANCE_NAME, vec![1, 2, 3, 4]));
799 pl.push(Parameter::new(pid::RELATED_ENTITY_GUID, vec![0xAB; 16]));
800 pl.push(Parameter::new(pid::TOPIC_ALIASES, vec![0xCD; 8]));
801 pl.push(Parameter::new(pid::RELATED_SAMPLE_IDENTITY, vec![0xEF; 24]));
802 let bytes = pl.to_bytes(true);
803 let decoded = ParameterList::from_bytes(&bytes, true).unwrap();
804 assert_eq!(decoded, pl);
805 }
806
807 // ---- PID_IGNORE (XTypes 1.3 §7.4.1.2.1) ----
808
809 #[test]
810 fn pid_ignore_skipped_in_pl_cdr_decode() {
811 // PID_IGNORE 0x3F03 mit 4-byte payload, dann PROTOCOL_VERSION
812 // (0x0015) mit 4-byte payload, dann Sentinel. Decoder MUSS das
813 // PID_IGNORE-Item ueberlesen und nur PROTOCOL_VERSION zurueckgeben.
814 let bytes = vec![
815 0x03, 0x3F, 0x04, 0x00, // PID_IGNORE, length=4
816 0xAA, 0xBB, 0xCC, 0xDD, // body (irrelevant)
817 0x15, 0x00, 0x04, 0x00, // PID_PROTOCOL_VERSION, length=4
818 2, 5, 0, 0, // value
819 0x01, 0x00, 0x00, 0x00, // Sentinel
820 ];
821 let pl = ParameterList::from_bytes(&bytes, true).unwrap();
822 assert_eq!(pl.parameters.len(), 1);
823 assert_eq!(pl.parameters[0].id, pid::PROTOCOL_VERSION);
824 assert_eq!(pl.parameters[0].value, vec![2, 5, 0, 0]);
825 }
826
827 #[test]
828 fn pid_ignore_zero_length_is_valid() {
829 // PID_IGNORE mit length=0 — auch zulaessig (reines Padding-Marker).
830 let bytes = vec![
831 0x03, 0x3F, 0x00, 0x00, // PID_IGNORE, length=0
832 0x15, 0x00, 0x04, 0x00, // PID_PROTOCOL_VERSION
833 2, 5, 0, 0, 0x01, 0x00, 0x00, 0x00, // Sentinel
834 ];
835 let pl = ParameterList::from_bytes(&bytes, true).unwrap();
836 assert_eq!(pl.parameters.len(), 1);
837 assert_eq!(pl.parameters[0].id, pid::PROTOCOL_VERSION);
838 }
839
840 #[test]
841 fn pid_ignore_truncated_body_rejected() {
842 // PID_IGNORE mit length=8, aber nur 4 byte Body folgen.
843 let bytes = vec![
844 0x03, 0x3F, 0x08, 0x00, // PID_IGNORE, length=8
845 0xAA, 0xBB, 0xCC, 0xDD, // nur 4 byte Body
846 0x01, 0x00, 0x00,
847 0x00, // wuerde Sentinel sein, ist aber innerhalb der angekuendigten 8
848 ];
849 let res = ParameterList::from_bytes(&bytes, true);
850 assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
851 }
852
853 #[test]
854 fn pid_ignore_be_decoded() {
855 // BE-Endianness: id 3F 03, length 00 04, body, dann Sentinel.
856 let bytes = vec![
857 0x3F, 0x03, 0x00, 0x04, // PID_IGNORE BE
858 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x15, 0x00, 0x04, // PROTOCOL_VERSION BE
859 2, 5, 0, 0, 0x00, 0x01, 0x00, 0x00, // Sentinel BE
860 ];
861 let pl = ParameterList::from_bytes(&bytes, false).unwrap();
862 assert_eq!(pl.parameters.len(), 1);
863 assert_eq!(pl.parameters[0].id, pid::PROTOCOL_VERSION);
864 }
865
866 #[test]
867 fn pid_ignore_ignored_even_when_count_would_exceed_cap() {
868 // MAX_PARAMETERS-Cap zaehlt PID_IGNORE NICHT mit, weil silent-skip
869 // keinen Eintrag erzeugt. 8192 PID_IGNOREs am Stueck waeren ohne
870 // diesen Pfad ein DoS-Risiko, mit dem Pfad sind sie harmlos.
871 let mut bytes: Vec<u8> = Vec::new();
872 for _ in 0..50 {
873 bytes.extend_from_slice(&[0x03, 0x3F, 0x00, 0x00]);
874 }
875 bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
876 let pl = ParameterList::from_bytes(&bytes, true).unwrap();
877 assert_eq!(pl.parameters.len(), 0);
878 }
879
880 #[test]
881 fn roundtrip_be_endianness() {
882 let mut pl = ParameterList::new();
883 pl.push(Parameter::new(pid::PROTOCOL_VERSION, vec![2, 5, 0, 0]));
884 let bytes = pl.to_bytes(false);
885 // BE: id 00 15, length 00 04, value, then sentinel 00 01 00 00
886 assert_eq!(&bytes[..4], &[0, 0x15, 0, 4]);
887 let decoded = ParameterList::from_bytes(&bytes, false).unwrap();
888 assert_eq!(decoded, pl);
889 }
890}