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 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 &lt;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}