Skip to main content

zerodds_rtps/
wire_types.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! RTPS wire base types (DDSI-RTPS 2.5 §8.3.5, §8.3.5.1).
4//!
5//! These types are the atoms of the RTPS wire format: GUID components,
6//! sequence numbers, locators. They are all pure byte structures with
7//! a fixed layout (no XCDR alignment, no endianness tagging on the type —
8//! the endianness of a submessage stream slice comes from the
9//! submessage-header E flag).
10//!
11//! # Convention
12//!
13//! - `read_from_le` / `read_from_be`: decoder with explicit endianness.
14//! - `write_to_le` / `write_to_be`: encoder, symmetric.
15//! - `WIRE_SIZE`: constant with the fixed byte count on the wire.
16
17use crate::error::WireError;
18
19// ============================================================================
20// ProtocolVersion (§8.3.5.5)
21// ============================================================================
22
23/// `ProtocolVersion`: major + minor of the RTPS protocol. Currently 2.5.
24///
25/// `PartialOrd`/`Ord` compare lexicographically — `(major, minor)`
26/// tuple order — which matches the spec version ordering
27/// (2.4 < 2.5).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub struct ProtocolVersion {
30    /// Major version.
31    pub major: u8,
32    /// Minor version.
33    pub minor: u8,
34}
35
36impl ProtocolVersion {
37    /// Wire size: 2 bytes.
38    pub const WIRE_SIZE: usize = 2;
39
40    /// RTPS 1.0 — historical (Spec §8.3.5.5).
41    pub const V1_0: Self = Self { major: 1, minor: 0 };
42    /// RTPS 1.1 — historical.
43    pub const V1_1: Self = Self { major: 1, minor: 1 };
44    /// RTPS 2.0 — historical.
45    pub const V2_0: Self = Self { major: 2, minor: 0 };
46    /// RTPS 2.1 — historical.
47    pub const V2_1: Self = Self { major: 2, minor: 1 };
48    /// RTPS 2.2 — historical.
49    pub const V2_2: Self = Self { major: 2, minor: 2 };
50    /// RTPS 2.3 — historical.
51    pub const V2_3: Self = Self { major: 2, minor: 3 };
52    /// RTPS 2.4 — Cyclone DDS default before the update to 2.5.
53    pub const V2_4: Self = Self { major: 2, minor: 4 };
54    /// RTPS 2.5 (default for ZeroDDS).
55    pub const V2_5: Self = Self { major: 2, minor: 5 };
56
57    /// `PROTOCOLVERSION` — spec alias for the most recent supported
58    /// value (RTPS 2.5).
59    pub const CURRENT: Self = Self::V2_5;
60
61    /// Bytes [major, minor].
62    #[must_use]
63    pub fn to_bytes(self) -> [u8; 2] {
64        [self.major, self.minor]
65    }
66
67    /// Reads 2 bytes.
68    #[must_use]
69    pub fn from_bytes(bytes: [u8; 2]) -> Self {
70        Self {
71            major: bytes[0],
72            minor: bytes[1],
73        }
74    }
75}
76
77impl Default for ProtocolVersion {
78    fn default() -> Self {
79        Self::V2_5
80    }
81}
82
83// ============================================================================
84// VendorId (§8.3.5.6)
85// ============================================================================
86
87/// `VendorId`: 2-byte vendor identifier. ZeroDDS uses `0x01F0` as an
88/// interim value from the OMG developer range, until an official
89/// VendorId is requested.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub struct VendorId(pub [u8; 2]);
92
93impl VendorId {
94    /// Wire size: 2 bytes.
95    pub const WIRE_SIZE: usize = 2;
96
97    /// Sentinel "unknown" (0x00, 0x00) — only for tests/stub.
98    pub const UNKNOWN: Self = Self([0, 0]);
99
100    /// ZeroDDS interim VendorId from the OMG developer range.
101    pub const ZERODDS: Self = Self([0x01, 0xF0]);
102
103    /// OCI OpenDDS (OMG-assigned VendorId).
104    pub const OPENDDS: Self = Self([0x01, 0x03]);
105
106    /// eProsima Fast DDS (OMG-assigned VendorId).
107    pub const FASTDDS: Self = Self([0x01, 0x0F]);
108
109    /// Eclipse Cyclone DDS (OMG-assigned VendorId).
110    pub const CYCLONE: Self = Self([0x01, 0x10]);
111
112    /// Bytes unchanged.
113    #[must_use]
114    pub fn to_bytes(self) -> [u8; 2] {
115        self.0
116    }
117
118    /// Bytes unchanged.
119    #[must_use]
120    pub fn from_bytes(bytes: [u8; 2]) -> Self {
121        Self(bytes)
122    }
123}
124
125// ============================================================================
126// GuidPrefix (§8.3.5.1)
127// ============================================================================
128
129/// `GuidPrefix`: 12-byte prefix of a GUID. Identifies a
130/// participant; stays the same for all endpoints of the participant.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
132pub struct GuidPrefix(pub [u8; 12]);
133
134impl GuidPrefix {
135    /// Wire size: 12 bytes.
136    pub const WIRE_SIZE: usize = 12;
137
138    /// Sentinel "unknown".
139    pub const UNKNOWN: Self = Self([0; 12]);
140
141    /// Bytes unchanged.
142    #[must_use]
143    pub fn to_bytes(self) -> [u8; 12] {
144        self.0
145    }
146
147    /// Bytes unchanged.
148    #[must_use]
149    pub fn from_bytes(bytes: [u8; 12]) -> Self {
150        Self(bytes)
151    }
152
153    /// ZeroDDS convention (Spec `zerodds-zero-copy-1.0` §6 wave 4):
154    /// the first 4 bytes of the GuidPrefix carry a deterministic
155    /// host identifier (hash of the `gethostname` output). Two
156    /// participants with an identical host-id prefix run on the
157    /// same machine and can set up a same-host zero-copy path.
158    ///
159    /// The RTPS 2.5 spec §9.3.1.5 allows vendor-specific
160    /// structuring of the first 8 bytes (vendor-specific);
161    /// only the comparison semantics of the full 12 bytes is normative.
162    #[must_use]
163    pub fn host_id(self) -> [u8; 4] {
164        [self.0[0], self.0[1], self.0[2], self.0[3]]
165    }
166
167    /// Returns `true` if both participants carry the same host-id
168    /// prefix. See [`Self::host_id`].
169    #[must_use]
170    pub fn is_same_host(self, other: Self) -> bool {
171        self.host_id() == other.host_id()
172    }
173}
174
175// ============================================================================
176// EntityId (§8.3.5.2 + Table 9.1)
177// ============================================================================
178
179/// `EntityKind`: classification of an endpoint. Spec Table 9.1.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
181#[repr(u8)]
182#[allow(missing_docs)]
183pub enum EntityKind {
184    Unknown = 0x00,
185    UserWriterNoKey = 0x03,
186    UserWriterWithKey = 0x02,
187    UserReaderNoKey = 0x04,
188    UserReaderWithKey = 0x07,
189    BuiltinWriterNoKey = 0xC3,
190    BuiltinWriterWithKey = 0xC2,
191    BuiltinReaderNoKey = 0xC4,
192    BuiltinReaderWithKey = 0xC7,
193    Participant = 0xC1,
194}
195
196impl EntityKind {
197    /// Converts a byte into an `EntityKind`. Unknown bytes
198    /// are mapped to `Unknown` — this mirrors spec tolerance behaviour.
199    #[must_use]
200    pub fn from_byte(b: u8) -> Self {
201        match b {
202            0x03 => Self::UserWriterNoKey,
203            0x02 => Self::UserWriterWithKey,
204            0x04 => Self::UserReaderNoKey,
205            0x07 => Self::UserReaderWithKey,
206            0xC3 => Self::BuiltinWriterNoKey,
207            0xC2 => Self::BuiltinWriterWithKey,
208            0xC4 => Self::BuiltinReaderNoKey,
209            0xC7 => Self::BuiltinReaderWithKey,
210            0xC1 => Self::Participant,
211            _ => Self::Unknown,
212        }
213    }
214}
215
216/// `EntityId`: 4-byte endpoint identifier within a participant.
217/// Layout: 3 bytes `entity_key` + 1 byte `entity_kind`.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
219pub struct EntityId {
220    /// First 3 bytes (key).
221    pub entity_key: [u8; 3],
222    /// Last byte (kind).
223    pub entity_kind: EntityKind,
224}
225
226impl EntityId {
227    /// Wire size: 4 bytes.
228    pub const WIRE_SIZE: usize = 4;
229
230    /// Sentinel.
231    pub const UNKNOWN: Self = Self {
232        entity_key: [0; 3],
233        entity_kind: EntityKind::Unknown,
234    };
235
236    /// Reserved participant EntityId (Spec §9.3.1.2).
237    pub const PARTICIPANT: Self = Self {
238        entity_key: [0, 0, 1],
239        entity_kind: EntityKind::Participant,
240    };
241
242    /// Constructs a user-writer endpoint with key.
243    #[must_use]
244    pub const fn user_writer_with_key(key: [u8; 3]) -> Self {
245        Self {
246            entity_key: key,
247            entity_kind: EntityKind::UserWriterWithKey,
248        }
249    }
250
251    /// Constructs a user-reader endpoint with key.
252    #[must_use]
253    pub const fn user_reader_with_key(key: [u8; 3]) -> Self {
254        Self {
255            entity_key: key,
256            entity_kind: EntityKind::UserReaderWithKey,
257        }
258    }
259
260    /// Constructs a user-writer endpoint without key (NoKey).
261    /// Spec §9.3.1.2 Table 9.1: entity_kind=0x03 for NoKey writers.
262    /// Important for cross-vendor interop: if the IDL type has no @key,
263    /// the writer must be published as NoKey, otherwise a
264    /// remote reader rejects the DATA submessage (writer/reader kind mismatch).
265    #[must_use]
266    pub const fn user_writer_no_key(key: [u8; 3]) -> Self {
267        Self {
268            entity_key: key,
269            entity_kind: EntityKind::UserWriterNoKey,
270        }
271    }
272
273    /// Constructs a user-reader endpoint without key (NoKey).
274    /// Spec §9.3.1.2 Table 9.1: entity_kind=0x04 for NoKey readers.
275    #[must_use]
276    pub const fn user_reader_no_key(key: [u8; 3]) -> Self {
277        Self {
278            entity_key: key,
279            entity_kind: EntityKind::UserReaderNoKey,
280        }
281    }
282
283    /// SPDP Builtin Participant Writer (Spec §9.3.1.5 Table 9.4).
284    /// Multicast beacon sender in the discovery path.
285    pub const SPDP_BUILTIN_PARTICIPANT_WRITER: Self = Self {
286        entity_key: [0, 0x01, 0x00],
287        entity_kind: EntityKind::BuiltinWriterWithKey,
288    };
289
290    /// SPDP Builtin Participant Reader.
291    pub const SPDP_BUILTIN_PARTICIPANT_READER: Self = Self {
292        entity_key: [0, 0x01, 0x00],
293        entity_kind: EntityKind::BuiltinReaderWithKey,
294    };
295
296    /// SEDP Subscriptions Writer .
297    pub const SEDP_BUILTIN_SUBSCRIPTIONS_WRITER: Self = Self {
298        entity_key: [0, 0x00, 0x04],
299        entity_kind: EntityKind::BuiltinWriterWithKey,
300    };
301
302    /// SEDP Subscriptions Reader .
303    pub const SEDP_BUILTIN_SUBSCRIPTIONS_READER: Self = Self {
304        entity_key: [0, 0x00, 0x04],
305        entity_kind: EntityKind::BuiltinReaderWithKey,
306    };
307
308    /// SEDP Publications Writer .
309    pub const SEDP_BUILTIN_PUBLICATIONS_WRITER: Self = Self {
310        entity_key: [0, 0x00, 0x03],
311        entity_kind: EntityKind::BuiltinWriterWithKey,
312    };
313
314    /// SEDP Publications Reader .
315    pub const SEDP_BUILTIN_PUBLICATIONS_READER: Self = Self {
316        entity_key: [0, 0x00, 0x03],
317        entity_kind: EntityKind::BuiltinReaderWithKey,
318    };
319
320    /// `BUILTIN_PARTICIPANT_MESSAGE_WRITER` — Writer Liveliness
321    /// Protocol (WLP). Sends `ParticipantMessageData` over the
322    /// topic `DCPSParticipantMessage` (DDSI-RTPS 2.5 §8.4.13,
323    /// §9.3.1.5 Tab. 9.4 — EntityKey `[00, 02, 00]`,
324    /// EntityKind `BuiltinWriterWithKey = 0xC2`).
325    pub const BUILTIN_PARTICIPANT_MESSAGE_WRITER: Self = Self {
326        entity_key: [0, 0x02, 0x00],
327        entity_kind: EntityKind::BuiltinWriterWithKey,
328    };
329
330    /// `BUILTIN_PARTICIPANT_MESSAGE_READER` — counterpart to the
331    /// WLP writer (DDSI-RTPS 2.5 §8.4.13, §9.3.1.5 Tab. 9.4).
332    pub const BUILTIN_PARTICIPANT_MESSAGE_READER: Self = Self {
333        entity_key: [0, 0x02, 0x00],
334        entity_kind: EntityKind::BuiltinReaderWithKey,
335    };
336
337    // TypeLookup Service (XTypes §7.6.3.3.4): RPC, no key.
338    // ENTITYKIND_BUILTIN_WRITER_NO_KEY = 0xC3, _READER_NO_KEY = 0xC4.
339    /// TypeLookup Service Request Writer.
340    pub const TL_SVC_REQ_WRITER: Self = Self {
341        entity_key: [0, 0x03, 0x00],
342        entity_kind: EntityKind::BuiltinWriterNoKey,
343    };
344    /// TypeLookup Service Request Reader.
345    pub const TL_SVC_REQ_READER: Self = Self {
346        entity_key: [0, 0x03, 0x00],
347        entity_kind: EntityKind::BuiltinReaderNoKey,
348    };
349    /// TypeLookup Service Reply Writer.
350    pub const TL_SVC_REPLY_WRITER: Self = Self {
351        entity_key: [0, 0x03, 0x01],
352        entity_kind: EntityKind::BuiltinWriterNoKey,
353    };
354    /// TypeLookup Service Reply Reader.
355    pub const TL_SVC_REPLY_READER: Self = Self {
356        entity_key: [0, 0x03, 0x01],
357        entity_kind: EntityKind::BuiltinReaderNoKey,
358    };
359
360    // ----------------------------------------------------------------
361    // DDS-Security 1.2 §7.4.7.1 Tab.7 — 12 secure builtin EntityIds
362    // (C3.8). EntityKey layout per spec; kind = WithKey except for the
363    // stateless topics (NoKey, since those topics are keyless).
364    // ----------------------------------------------------------------
365
366    /// `SEDP_BUILTIN_PUBLICATIONS_SECURE_WRITER` — secure SEDP
367    /// publications writer (bit 16, §9.3.1.6 Tab.13).
368    pub const SEDP_BUILTIN_PUBLICATIONS_SECURE_WRITER: Self = Self {
369        entity_key: [0xff, 0x00, 0x03],
370        entity_kind: EntityKind::BuiltinWriterWithKey,
371    };
372    /// `SEDP_BUILTIN_PUBLICATIONS_SECURE_READER` — Bit 17.
373    pub const SEDP_BUILTIN_PUBLICATIONS_SECURE_READER: Self = Self {
374        entity_key: [0xff, 0x00, 0x03],
375        entity_kind: EntityKind::BuiltinReaderWithKey,
376    };
377    /// `SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_WRITER` — Bit 18.
378    pub const SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_WRITER: Self = Self {
379        entity_key: [0xff, 0x00, 0x04],
380        entity_kind: EntityKind::BuiltinWriterWithKey,
381    };
382    /// `SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_READER` — Bit 19.
383    pub const SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_READER: Self = Self {
384        entity_key: [0xff, 0x00, 0x04],
385        entity_kind: EntityKind::BuiltinReaderWithKey,
386    };
387    /// `BUILTIN_PARTICIPANT_MESSAGE_SECURE_WRITER` — secure WLP writer
388    /// (bit 20, §7.4.7.1).
389    pub const BUILTIN_PARTICIPANT_MESSAGE_SECURE_WRITER: Self = Self {
390        entity_key: [0xff, 0x02, 0x00],
391        entity_kind: EntityKind::BuiltinWriterWithKey,
392    };
393    /// `BUILTIN_PARTICIPANT_MESSAGE_SECURE_READER` — Bit 21.
394    pub const BUILTIN_PARTICIPANT_MESSAGE_SECURE_READER: Self = Self {
395        entity_key: [0xff, 0x02, 0x00],
396        entity_kind: EntityKind::BuiltinReaderWithKey,
397    };
398    /// `BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER` — auth-handshake
399    /// topic writer (bit 22, §7.4.7.1, §10.3.4 auth stateless wire).
400    /// NoKey, since the stateless topic is keyless.
401    pub const BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER: Self = Self {
402        entity_key: [0x00, 0x02, 0x01],
403        entity_kind: EntityKind::BuiltinWriterNoKey,
404    };
405    /// `BUILTIN_PARTICIPANT_STATELESS_MESSAGE_READER` — Bit 23.
406    pub const BUILTIN_PARTICIPANT_STATELESS_MESSAGE_READER: Self = Self {
407        entity_key: [0x00, 0x02, 0x01],
408        entity_kind: EntityKind::BuiltinReaderNoKey,
409    };
410    /// `BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER` — crypto
411    /// key-exchange topic writer (bit 24, §7.4.7.1, §10.5.4
412    /// VolatileMessageSecure wire).
413    pub const BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER: Self = Self {
414        entity_key: [0xff, 0x02, 0x02],
415        // Kind byte 0xc3 (NoKey value) — Spec §7.4.7.1 Tab.7 + cyclone wire
416        // (ff0202c3); NOT WithKey/0xc2, otherwise the crypto channel does
417        // not match cross-vendor.
418        entity_kind: EntityKind::BuiltinWriterNoKey,
419    };
420    /// `BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER` — Bit 25.
421    pub const BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER: Self = Self {
422        entity_key: [0xff, 0x02, 0x02],
423        // Kind byte 0xc4 (NoKey value) — cyclone wire (ff0202c4).
424        entity_kind: EntityKind::BuiltinReaderNoKey,
425    };
426    /// `SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_WRITER` — secure
427    /// SPDP writer for the DCPSParticipantsSecure topic (bit 26).
428    pub const SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_WRITER: Self = Self {
429        entity_key: [0xff, 0x01, 0x01],
430        entity_kind: EntityKind::BuiltinWriterWithKey,
431    };
432    /// `SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_READER` — Bit 27.
433    pub const SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_READER: Self = Self {
434        entity_key: [0xff, 0x01, 0x01],
435        entity_kind: EntityKind::BuiltinReaderWithKey,
436    };
437
438    /// True if this is one of the 12 secure builtin EntityIds from
439    /// DDS-Security 1.2 §7.4.7.1 Tab.7.
440    #[must_use]
441    pub const fn is_secure_builtin(self) -> bool {
442        matches!(
443            self,
444            Self::SEDP_BUILTIN_PUBLICATIONS_SECURE_WRITER
445                | Self::SEDP_BUILTIN_PUBLICATIONS_SECURE_READER
446                | Self::SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_WRITER
447                | Self::SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_READER
448                | Self::BUILTIN_PARTICIPANT_MESSAGE_SECURE_WRITER
449                | Self::BUILTIN_PARTICIPANT_MESSAGE_SECURE_READER
450                | Self::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER
451                | Self::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_READER
452                | Self::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER
453                | Self::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER
454                | Self::SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_WRITER
455                | Self::SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_READER
456        )
457    }
458
459    /// Bytes [key0, key1, key2, kind].
460    #[must_use]
461    pub fn to_bytes(self) -> [u8; 4] {
462        [
463            self.entity_key[0],
464            self.entity_key[1],
465            self.entity_key[2],
466            self.entity_kind as u8,
467        ]
468    }
469
470    /// Reads 4 bytes.
471    #[must_use]
472    pub fn from_bytes(bytes: [u8; 4]) -> Self {
473        Self {
474            entity_key: [bytes[0], bytes[1], bytes[2]],
475            entity_kind: EntityKind::from_byte(bytes[3]),
476        }
477    }
478}
479
480// ============================================================================
481// Guid (§8.3.5.3)
482// ============================================================================
483
484/// `Guid`: GuidPrefix + EntityId = 16 bytes. Globally unique identifier
485/// of an endpoint.
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
487pub struct Guid {
488    /// Participant prefix.
489    pub prefix: GuidPrefix,
490    /// Endpoint identifier within the participant.
491    pub entity_id: EntityId,
492}
493
494impl Guid {
495    /// Wire size: 16 bytes.
496    pub const WIRE_SIZE: usize = 16;
497
498    /// Sentinel.
499    pub const UNKNOWN: Self = Self {
500        prefix: GuidPrefix::UNKNOWN,
501        entity_id: EntityId::UNKNOWN,
502    };
503
504    /// Constructs a Guid from prefix + EntityId.
505    #[must_use]
506    pub const fn new(prefix: GuidPrefix, entity_id: EntityId) -> Self {
507        Self { prefix, entity_id }
508    }
509
510    /// Bytes (prefix + EntityId).
511    #[must_use]
512    pub fn to_bytes(self) -> [u8; 16] {
513        let mut out = [0u8; 16];
514        out[..12].copy_from_slice(&self.prefix.to_bytes());
515        out[12..].copy_from_slice(&self.entity_id.to_bytes());
516        out
517    }
518
519    /// Reads 16 bytes.
520    #[must_use]
521    pub fn from_bytes(bytes: [u8; 16]) -> Self {
522        let mut prefix_bytes = [0u8; 12];
523        prefix_bytes.copy_from_slice(&bytes[..12]);
524        let mut entity_bytes = [0u8; 4];
525        entity_bytes.copy_from_slice(&bytes[12..]);
526        Self {
527            prefix: GuidPrefix::from_bytes(prefix_bytes),
528            entity_id: EntityId::from_bytes(entity_bytes),
529        }
530    }
531}
532
533// ============================================================================
534// SequenceNumber (§8.3.5.4)
535// ============================================================================
536
537/// `SequenceNumber`: 64-bit signed, encoded as (high: i32, low: u32).
538/// Both fields are written with the active submessage endianness.
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
540pub struct SequenceNumber(pub i64);
541
542impl SequenceNumber {
543    /// Wire size: 8 bytes.
544    pub const WIRE_SIZE: usize = 8;
545
546    /// Sentinel "unknown" (high=-1, low=0) → -2^32.
547    pub const UNKNOWN: Self = Self(-(1_i64 << 32));
548
549    /// Splits the i64 into (high, low) per spec.
550    #[must_use]
551    pub fn split(self) -> (i32, u32) {
552        let value = self.0;
553        let high = (value >> 32) as i32;
554        let low = (value & 0xFFFF_FFFF) as u32;
555        (high, low)
556    }
557
558    /// Assembles from (high, low).
559    #[must_use]
560    pub fn from_high_low(high: i32, low: u32) -> Self {
561        let value = (i64::from(high) << 32) | i64::from(low);
562        Self(value)
563    }
564
565    /// Writes into 8 bytes with the given endianness (LE or BE).
566    #[must_use]
567    pub fn to_bytes_le(self) -> [u8; 8] {
568        let (high, low) = self.split();
569        let mut out = [0u8; 8];
570        out[..4].copy_from_slice(&high.to_le_bytes());
571        out[4..].copy_from_slice(&low.to_le_bytes());
572        out
573    }
574
575    /// BE variant.
576    #[must_use]
577    pub fn to_bytes_be(self) -> [u8; 8] {
578        let (high, low) = self.split();
579        let mut out = [0u8; 8];
580        out[..4].copy_from_slice(&high.to_be_bytes());
581        out[4..].copy_from_slice(&low.to_be_bytes());
582        out
583    }
584
585    /// LE decoder.
586    #[must_use]
587    pub fn from_bytes_le(bytes: [u8; 8]) -> Self {
588        let mut hi = [0u8; 4];
589        hi.copy_from_slice(&bytes[..4]);
590        let mut lo = [0u8; 4];
591        lo.copy_from_slice(&bytes[4..]);
592        Self::from_high_low(i32::from_le_bytes(hi), u32::from_le_bytes(lo))
593    }
594
595    /// BE decoder.
596    #[must_use]
597    pub fn from_bytes_be(bytes: [u8; 8]) -> Self {
598        let mut hi = [0u8; 4];
599        hi.copy_from_slice(&bytes[..4]);
600        let mut lo = [0u8; 4];
601        lo.copy_from_slice(&bytes[4..]);
602        Self::from_high_low(i32::from_be_bytes(hi), u32::from_be_bytes(lo))
603    }
604}
605
606// ============================================================================
607// Vendor extension slots (§8.3.2 UExtension4_t / WExtension8_t)
608// ============================================================================
609
610/// `UExtension4_t` — 4-byte vendor-specific extension slot.
611/// Spec §8.3.2: opaque 32-bit value, the vendor decides its meaning;
612/// the receiver propagates the value via the `extensions` field into the
613/// receiver state.
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
615pub struct UExtension4(pub [u8; 4]);
616
617impl UExtension4 {
618    /// Wire size: 4 bytes.
619    pub const WIRE_SIZE: usize = 4;
620
621    /// Constructor from u32 (big-endian).
622    #[must_use]
623    pub fn from_u32_be(v: u32) -> Self {
624        Self(v.to_be_bytes())
625    }
626
627    /// Returns the value as u32 (big-endian interpretation).
628    #[must_use]
629    pub fn to_u32_be(self) -> u32 {
630        u32::from_be_bytes(self.0)
631    }
632
633    /// Roundtrip identity.
634    #[must_use]
635    pub fn to_bytes(self) -> [u8; 4] {
636        self.0
637    }
638
639    /// Roundtrip identity.
640    #[must_use]
641    pub fn from_bytes(bytes: [u8; 4]) -> Self {
642        Self(bytes)
643    }
644}
645
646/// `WExtension8_t` — 8-byte vendor-specific extension slot.
647/// Spec §8.3.2: opaque 64-bit value (analogous to UExtension4_t for
648/// fields that need 8 bytes, e.g. for 64-bit counters).
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
650pub struct WExtension8(pub [u8; 8]);
651
652impl WExtension8 {
653    /// Wire size: 8 bytes.
654    pub const WIRE_SIZE: usize = 8;
655
656    /// Constructor from u64 (big-endian).
657    #[must_use]
658    pub fn from_u64_be(v: u64) -> Self {
659        Self(v.to_be_bytes())
660    }
661
662    /// Returns the value as u64 (big-endian interpretation).
663    #[must_use]
664    pub fn to_u64_be(self) -> u64 {
665        u64::from_be_bytes(self.0)
666    }
667
668    /// Roundtrip identity.
669    #[must_use]
670    pub fn to_bytes(self) -> [u8; 8] {
671        self.0
672    }
673
674    /// Roundtrip identity.
675    #[must_use]
676    pub fn from_bytes(bytes: [u8; 8]) -> Self {
677        Self(bytes)
678    }
679}
680
681// ============================================================================
682// FragmentNumber (§8.3.5.7)
683// ============================================================================
684
685/// `FragmentNumber`: 32-bit unsigned, 1-based (fragment #1 is the
686/// first fragment of a sample). `UNKNOWN` = 0 is used as a sentinel,
687/// but is not a valid fragment.
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
689pub struct FragmentNumber(pub u32);
690
691impl FragmentNumber {
692    /// Wire size: 4 bytes.
693    pub const WIRE_SIZE: usize = 4;
694
695    /// Sentinel "unknown" (= 0). Never a valid fragment.
696    pub const UNKNOWN: Self = Self(0);
697
698    /// LE bytes.
699    #[must_use]
700    pub fn to_bytes_le(self) -> [u8; 4] {
701        self.0.to_le_bytes()
702    }
703
704    /// BE bytes.
705    #[must_use]
706    pub fn to_bytes_be(self) -> [u8; 4] {
707        self.0.to_be_bytes()
708    }
709
710    /// LE decoder.
711    #[must_use]
712    pub fn from_bytes_le(bytes: [u8; 4]) -> Self {
713        Self(u32::from_le_bytes(bytes))
714    }
715
716    /// BE decoder.
717    #[must_use]
718    pub fn from_bytes_be(bytes: [u8; 4]) -> Self {
719        Self(u32::from_be_bytes(bytes))
720    }
721}
722
723// ============================================================================
724// Locator (§8.3.5.7)
725// ============================================================================
726
727/// `LocatorKind`: address family of a locator.
728#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
729#[repr(i32)]
730#[allow(missing_docs)]
731pub enum LocatorKind {
732    Invalid = -1,
733    Reserved = 0,
734    UdpV4 = 1,
735    UdpV6 = 2,
736    /// TCPv4 (DDS-TCP-PSM §4 `LOCATOR_KIND_TCPV4`).
737    Tcpv4 = 4,
738    /// TCPv6 (DDS-TCP-PSM §4 `LOCATOR_KIND_TCPV6`).
739    Tcpv6 = 8,
740    /// Shared memory (vendor range, MSB=1 per DDSI-RTPS §9.3.1.2 —
741    /// negative `i32` values are vendor-specific). `0x8100_0000` as the
742    /// ZeroDDS vendor token; Cyclone + Fast-DDS ignore unknown
743    /// kinds.
744    Shm = -2_130_706_432, // 0x8100_0000 as i32
745    /// Unix domain socket (vendor range, ZeroDDS extension for
746    /// containerized IPC when multicast is blocked or POSIX SHM
747    /// does not work cross-container). `0x8100_0001`. The 16-byte
748    /// address field carries an identifier that is resolved into a socket path
749    /// under a configurable base directory
750    /// (`/tmp/zerodds/uds/<hex>.sock`).
751    Uds = -2_130_706_431, // 0x8100_0001 as i32
752    /// Time-Sensitive-Networking L2 endpoint (vendor range, ZeroDDS
753    /// extension for DDS-TSN 1.0 Annex A — RTPS-over-Ethernet with
754    /// EtherType 0x88B5). `0x8100_0002`. The 16-byte address field
755    /// carries the destination MAC (bytes 0..6) + optional VLAN VID
756    /// (bytes 6..8, big-endian).
757    Tsn = -2_130_706_430, // 0x8100_0002 as i32
758}
759
760impl LocatorKind {
761    /// Returns the i32 wire value.
762    #[must_use]
763    pub fn as_i32(self) -> i32 {
764        self as i32
765    }
766
767    /// Converts i32 into a LocatorKind.
768    ///
769    /// # Errors
770    /// `WireError::InvalidLocatorKind` for an unknown value.
771    pub fn from_i32(v: i32) -> Result<Self, WireError> {
772        match v {
773            -1 => Ok(Self::Invalid),
774            0 => Ok(Self::Reserved),
775            1 => Ok(Self::UdpV4),
776            2 => Ok(Self::UdpV6),
777            4 => Ok(Self::Tcpv4),
778            8 => Ok(Self::Tcpv6),
779            -2_130_706_432 => Ok(Self::Shm),
780            -2_130_706_431 => Ok(Self::Uds),
781            -2_130_706_430 => Ok(Self::Tsn),
782            other => Err(WireError::InvalidLocatorKind { kind: other }),
783        }
784    }
785}
786
787/// SPDP default multicast address (Spec §9.6.1.4.1): `239.255.0.1`.
788pub const SPDP_DEFAULT_MULTICAST_ADDRESS: [u8; 4] = [239, 255, 0, 1];
789
790/// SPDP discovery port base (Spec §9.6.1.4.1, PB).
791pub const SPDP_PORT_BASE: u32 = 7400;
792
793/// Domain-specific port offset (Spec §9.6.1.4.1, DG).
794pub const SPDP_DOMAIN_GAIN: u32 = 250;
795
796/// Multicast discovery port offset (Spec §9.6.1.4.1, d0).
797pub const SPDP_DISCOVERY_MULTICAST_OFFSET: u32 = 0;
798
799/// Computes the SPDP multicast discovery port for a domain.
800/// Formula (Spec §9.6.1.4.1):
801///   port = PB + DG * domain_id + d0
802///        = 7400 + 250 * domain + 0
803#[must_use]
804pub fn spdp_multicast_port(domain_id: u32) -> u32 {
805    SPDP_PORT_BASE + SPDP_DOMAIN_GAIN * domain_id + SPDP_DISCOVERY_MULTICAST_OFFSET
806}
807
808/// `Locator`: 24-byte address (kind + port + 16-byte address).
809#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
810pub struct Locator {
811    /// Address family.
812    pub kind: LocatorKind,
813    /// UDP port.
814    pub port: u32,
815    /// 16-byte address. UDPv4 uses the last 4 bytes, 0 before that.
816    pub address: [u8; 16],
817}
818
819impl Locator {
820    /// Wire size: 24 bytes.
821    pub const WIRE_SIZE: usize = 24;
822
823    /// Spec §8.3.5.7 `LOCATOR_INVALID`.
824    pub const INVALID: Self = Self {
825        kind: LocatorKind::Invalid,
826        port: 0,
827        address: [0; 16],
828    };
829
830    /// Spec §8.3.5.7 `LOCATOR_KIND_RESERVED` template (kind=0,
831    /// port=0, address=0).
832    pub const RESERVED: Self = Self {
833        kind: LocatorKind::Reserved,
834        port: 0,
835        address: [0; 16],
836    };
837
838    /// Spec §8.3.5.7 — default UDPv4 locator (kind=1, port=0, addr=0).
839    pub const UDP_V4_ANY: Self = Self {
840        kind: LocatorKind::UdpV4,
841        port: 0,
842        address: [0; 16],
843    };
844
845    /// Spec §8.3.5.7 — default UDPv6 locator (kind=2, port=0, addr=0).
846    pub const UDP_V6_ANY: Self = Self {
847        kind: LocatorKind::UdpV6,
848        port: 0,
849        address: [0; 16],
850    };
851
852    /// ZeroDDS vendor-extension SHM locator template.
853    pub const SHM_ANY: Self = Self {
854        kind: LocatorKind::Shm,
855        port: 0,
856        address: [0; 16],
857    };
858
859    /// Spec §8.3.5.7 — `LOCATOR_PORT_INVALID` sentinel.
860    pub const PORT_INVALID: u32 = 0;
861
862    /// Spec §8.3.5.7 — `LOCATOR_ADDRESS_INVALID` sentinel.
863    pub const ADDRESS_INVALID: [u8; 16] = [0; 16];
864
865    /// Constructor for UDPv6 (16-byte address + port).
866    #[must_use]
867    pub fn udp_v6(addr: [u8; 16], port: u32) -> Self {
868        Self {
869            kind: LocatorKind::UdpV6,
870            port,
871            address: addr,
872        }
873    }
874
875    /// Constructor for UDPv4 (a.b.c.d:port).
876    #[must_use]
877    pub fn udp_v4(addr: [u8; 4], port: u32) -> Self {
878        Self::with_address(LocatorKind::UdpV4, addr, port)
879    }
880
881    /// Constructor for TCPv4 (DDS-TCP-PSM §4).
882    #[must_use]
883    pub fn tcp_v4(addr: [u8; 4], port: u32) -> Self {
884        Self::with_address(LocatorKind::Tcpv4, addr, port)
885    }
886
887    /// Constructor for TCPv6 (DDSI-RTPS 2.5 §9.4, LocatorKind=8).
888    /// 16-byte network-byte-order address.
889    #[must_use]
890    pub fn tcp_v6(addr: [u8; 16], port: u32) -> Self {
891        Self {
892            kind: LocatorKind::Tcpv6,
893            port,
894            address: addr,
895        }
896    }
897
898    /// Legacy alias for [`Self::tcp_v4`]. **Deprecated**: rename
899    /// call sites to `tcp_v4` (consistent with `udp_v4`).
900    #[must_use]
901    #[deprecated(note = "use Locator::tcp_v4 instead (naming consistency)")]
902    pub fn new_tcp_v4(addr: [u8; 4], port: u32) -> Self {
903        Self::tcp_v4(addr, port)
904    }
905
906    /// Constructor for a Unix domain socket endpoint with a 16-byte
907    /// ID. The port stays 0 (not meaningful for UDS). The transport
908    /// resolves the ID to `/<base_dir>/<hex>.sock`.
909    #[must_use]
910    pub fn uds(id: [u8; 16]) -> Self {
911        Self {
912            kind: LocatorKind::Uds,
913            port: 0,
914            address: id,
915        }
916    }
917
918    /// Constructor for a shared-memory segment with an ID (16-byte
919    /// token). The port field stays 0 (not meaningful for SHM).
920    #[must_use]
921    pub fn shm(id: [u8; 16]) -> Self {
922        Self {
923            kind: LocatorKind::Shm,
924            port: 0,
925            address: id,
926        }
927    }
928
929    /// Constructor for a TSN L2 endpoint (DDS-TSN 1.0 Annex A).
930    /// `mac` is the 6-byte destination MAC, `vlan_vid` the IEEE 802.1Q VID
931    /// (0 = untagged). Layout in the 16-byte address field: bytes 0..6 MAC,
932    /// bytes 6..8 VID (big-endian), rest 0. The port stays 0.
933    #[must_use]
934    pub fn tsn(mac: [u8; 6], vlan_vid: u16) -> Self {
935        let mut address = [0u8; 16];
936        address[..6].copy_from_slice(&mac);
937        address[6..8].copy_from_slice(&vlan_vid.to_be_bytes());
938        Self {
939            kind: LocatorKind::Tsn,
940            port: 0,
941            address,
942        }
943    }
944
945    /// Shared IPv4 constructor. Places the IPv4 bytes in the last
946    /// 4 bytes of the 16-byte `address` field (mapped IPv4-in-IPv6 layout).
947    #[must_use]
948    fn with_address(kind: LocatorKind, addr: [u8; 4], port: u32) -> Self {
949        let mut address = [0u8; 16];
950        address[12..].copy_from_slice(&addr);
951        Self {
952            kind,
953            port,
954            address,
955        }
956    }
957
958    /// Extracts the IPv4 address (only meaningful for kind == UdpV4).
959    #[must_use]
960    pub fn ipv4(self) -> [u8; 4] {
961        let mut out = [0u8; 4];
962        out.copy_from_slice(&self.address[12..]);
963        out
964    }
965
966    /// LE encoder.
967    #[must_use]
968    pub fn to_bytes_le(self) -> [u8; 24] {
969        let mut out = [0u8; 24];
970        out[..4].copy_from_slice(&self.kind.as_i32().to_le_bytes());
971        out[4..8].copy_from_slice(&self.port.to_le_bytes());
972        out[8..].copy_from_slice(&self.address);
973        out
974    }
975
976    /// BE encoder (for PL_CDR_BE, e.g. handshake `c.pdata`).
977    #[must_use]
978    pub fn to_bytes_be(self) -> [u8; 24] {
979        let mut out = [0u8; 24];
980        out[..4].copy_from_slice(&self.kind.as_i32().to_be_bytes());
981        out[4..8].copy_from_slice(&self.port.to_be_bytes());
982        out[8..].copy_from_slice(&self.address);
983        out
984    }
985
986    /// LE decoder.
987    ///
988    /// # Errors
989    /// `WireError::InvalidLocatorKind` on an unknown kind.
990    pub fn from_bytes_le(bytes: [u8; 24]) -> Result<Self, WireError> {
991        let mut kind_bytes = [0u8; 4];
992        kind_bytes.copy_from_slice(&bytes[..4]);
993        let kind = LocatorKind::from_i32(i32::from_le_bytes(kind_bytes))?;
994        let mut port_bytes = [0u8; 4];
995        port_bytes.copy_from_slice(&bytes[4..8]);
996        let port = u32::from_le_bytes(port_bytes);
997        let mut address = [0u8; 16];
998        address.copy_from_slice(&bytes[8..]);
999        Ok(Self {
1000            kind,
1001            port,
1002            address,
1003        })
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1010    use super::*;
1011
1012    // ---- ProtocolVersion ----
1013    #[test]
1014    fn protocol_version_default_is_2_5() {
1015        assert_eq!(ProtocolVersion::default(), ProtocolVersion::V2_5);
1016    }
1017
1018    #[test]
1019    fn protocol_version_roundtrip() {
1020        let v = ProtocolVersion::V2_5;
1021        assert_eq!(ProtocolVersion::from_bytes(v.to_bytes()), v);
1022    }
1023
1024    // ---- VendorId ----
1025    #[test]
1026    fn vendor_id_zerodds_constant() {
1027        assert_eq!(VendorId::ZERODDS.0, [0x01, 0xF0]);
1028    }
1029
1030    #[test]
1031    fn vendor_id_roundtrip() {
1032        let v = VendorId([0xAB, 0xCD]);
1033        assert_eq!(VendorId::from_bytes(v.to_bytes()), v);
1034    }
1035
1036    // ---- GuidPrefix ----
1037    #[test]
1038    fn guid_prefix_unknown_is_zero() {
1039        assert_eq!(GuidPrefix::UNKNOWN.0, [0u8; 12]);
1040    }
1041
1042    #[test]
1043    fn guid_prefix_roundtrip() {
1044        let bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
1045        let p = GuidPrefix::from_bytes(bytes);
1046        assert_eq!(p.to_bytes(), bytes);
1047    }
1048
1049    #[test]
1050    fn guid_prefix_host_id_is_first_four_bytes() {
1051        let p = GuidPrefix::from_bytes([0xAB, 0xCD, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0]);
1052        assert_eq!(p.host_id(), [0xAB, 0xCD, 0x12, 0x34]);
1053    }
1054
1055    #[test]
1056    fn guid_prefix_same_host_matches_on_first_four_bytes() {
1057        let a = GuidPrefix::from_bytes([1, 2, 3, 4, 9, 9, 9, 9, 9, 9, 9, 9]);
1058        let b = GuidPrefix::from_bytes([1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0]);
1059        let c = GuidPrefix::from_bytes([1, 2, 3, 5, 9, 9, 9, 9, 9, 9, 9, 9]);
1060        assert!(a.is_same_host(b));
1061        assert!(!a.is_same_host(c));
1062    }
1063
1064    // ---- EntityId ----
1065    #[test]
1066    fn entity_id_user_writer_with_key_layout() {
1067        let id = EntityId::user_writer_with_key([0xAA, 0xBB, 0xCC]);
1068        assert_eq!(id.to_bytes(), [0xAA, 0xBB, 0xCC, 0x02]);
1069    }
1070
1071    #[test]
1072    fn entity_id_user_reader_with_key_layout() {
1073        let id = EntityId::user_reader_with_key([0x11, 0x22, 0x33]);
1074        assert_eq!(id.to_bytes(), [0x11, 0x22, 0x33, 0x07]);
1075    }
1076
1077    #[test]
1078    fn entity_id_participant_constant() {
1079        assert_eq!(EntityId::PARTICIPANT.to_bytes(), [0, 0, 1, 0xC1]);
1080    }
1081
1082    #[test]
1083    fn entity_id_unknown_kind_byte_maps_to_unknown() {
1084        let id = EntityId::from_bytes([1, 2, 3, 0xEE]);
1085        assert_eq!(id.entity_kind, EntityKind::Unknown);
1086    }
1087
1088    #[test]
1089    fn entity_id_roundtrip() {
1090        let id = EntityId::user_writer_with_key([1, 2, 3]);
1091        assert_eq!(EntityId::from_bytes(id.to_bytes()), id);
1092    }
1093
1094    // ---- Secure builtin EntityIds (DDS-Security 1.2 §7.4.7.1, C3.8) ----
1095
1096    #[test]
1097    fn secure_publications_writer_layout() {
1098        // Spec: key=[0xff, 0x00, 0x03], kind=BuiltinWriterWithKey=0xC2.
1099        let id = EntityId::SEDP_BUILTIN_PUBLICATIONS_SECURE_WRITER;
1100        assert_eq!(id.to_bytes(), [0xff, 0x00, 0x03, 0xC2]);
1101    }
1102
1103    #[test]
1104    fn stateless_writer_is_no_key_kind() {
1105        // Stateless topic is keyless → BuiltinWriterNoKey=0xC3.
1106        let id = EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER;
1107        assert_eq!(id.entity_kind, EntityKind::BuiltinWriterNoKey);
1108        assert_eq!(id.to_bytes(), [0x00, 0x02, 0x01, 0xC3]);
1109    }
1110
1111    #[test]
1112    fn volatile_secure_writer_layout() {
1113        // Spec §7.4.7.1 Tab.7 + cyclone wire: 0xff0202c3 (NoKey kind byte),
1114        // NOT 0xc2 — otherwise the crypto-token channel does not match cross-vendor.
1115        let id = EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER;
1116        assert_eq!(id.to_bytes(), [0xff, 0x02, 0x02, 0xC3]);
1117    }
1118
1119    #[test]
1120    fn spdp_secure_reader_layout() {
1121        let id = EntityId::SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_READER;
1122        assert_eq!(id.to_bytes(), [0xff, 0x01, 0x01, 0xC7]);
1123    }
1124
1125    #[test]
1126    fn all_12_secure_entityids_roundtrip() {
1127        let ids = [
1128            EntityId::SEDP_BUILTIN_PUBLICATIONS_SECURE_WRITER,
1129            EntityId::SEDP_BUILTIN_PUBLICATIONS_SECURE_READER,
1130            EntityId::SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_WRITER,
1131            EntityId::SEDP_BUILTIN_SUBSCRIPTIONS_SECURE_READER,
1132            EntityId::BUILTIN_PARTICIPANT_MESSAGE_SECURE_WRITER,
1133            EntityId::BUILTIN_PARTICIPANT_MESSAGE_SECURE_READER,
1134            EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER,
1135            EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_READER,
1136            EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
1137            EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
1138            EntityId::SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_WRITER,
1139            EntityId::SPDP_RELIABLE_BUILTIN_PARTICIPANTS_SECURE_READER,
1140        ];
1141        assert_eq!(ids.len(), 12);
1142        for id in ids {
1143            assert!(id.is_secure_builtin(), "{id:?}");
1144            assert_eq!(EntityId::from_bytes(id.to_bytes()), id);
1145        }
1146    }
1147
1148    #[test]
1149    fn standard_builtin_is_not_secure_builtin() {
1150        for id in [
1151            EntityId::SPDP_BUILTIN_PARTICIPANT_WRITER,
1152            EntityId::SEDP_BUILTIN_PUBLICATIONS_WRITER,
1153            EntityId::BUILTIN_PARTICIPANT_MESSAGE_WRITER,
1154            EntityId::PARTICIPANT,
1155            EntityId::user_writer_with_key([1, 2, 3]),
1156        ] {
1157            assert!(!id.is_secure_builtin(), "{id:?} should not be secure");
1158        }
1159    }
1160
1161    // ---- Guid ----
1162    #[test]
1163    fn guid_layout_is_prefix_then_entity_id() {
1164        let g = Guid::new(
1165            GuidPrefix::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
1166            EntityId::user_writer_with_key([0xAA, 0xBB, 0xCC]),
1167        );
1168        let bytes = g.to_bytes();
1169        assert_eq!(&bytes[..12], &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
1170        assert_eq!(&bytes[12..], &[0xAA, 0xBB, 0xCC, 0x02]);
1171    }
1172
1173    #[test]
1174    fn guid_roundtrip() {
1175        let g = Guid::new(GuidPrefix::from_bytes([42; 12]), EntityId::PARTICIPANT);
1176        assert_eq!(Guid::from_bytes(g.to_bytes()), g);
1177    }
1178
1179    // ---- SequenceNumber ----
1180    #[test]
1181    fn sequence_number_split_zero() {
1182        let (h, l) = SequenceNumber(0).split();
1183        assert_eq!((h, l), (0, 0));
1184    }
1185
1186    #[test]
1187    fn sequence_number_split_one() {
1188        let (h, l) = SequenceNumber(1).split();
1189        assert_eq!((h, l), (0, 1));
1190    }
1191
1192    #[test]
1193    fn sequence_number_split_high_low() {
1194        let sn = SequenceNumber::from_high_low(2, 5);
1195        assert_eq!(sn.0, (2_i64 << 32) | 5);
1196    }
1197
1198    #[test]
1199    fn sequence_number_roundtrip_le() {
1200        let sn = SequenceNumber(0x0102_0304_0506_0708);
1201        assert_eq!(SequenceNumber::from_bytes_le(sn.to_bytes_le()), sn);
1202    }
1203
1204    #[test]
1205    fn sequence_number_roundtrip_be() {
1206        let sn = SequenceNumber(0x0102_0304_0506_0708);
1207        assert_eq!(SequenceNumber::from_bytes_be(sn.to_bytes_be()), sn);
1208    }
1209
1210    #[test]
1211    fn sequence_number_unknown_high_minus_one_low_zero() {
1212        let (h, l) = SequenceNumber::UNKNOWN.split();
1213        assert_eq!((h, l), (-1, 0));
1214    }
1215
1216    // ---- FragmentNumber ----
1217    #[test]
1218    fn fragment_number_roundtrip_le_be() {
1219        let fns = [
1220            FragmentNumber(0),
1221            FragmentNumber(1),
1222            FragmentNumber(0x1234_5678),
1223            FragmentNumber(u32::MAX),
1224        ];
1225        for fnum in fns {
1226            assert_eq!(FragmentNumber::from_bytes_le(fnum.to_bytes_le()), fnum);
1227            assert_eq!(FragmentNumber::from_bytes_be(fnum.to_bytes_be()), fnum);
1228        }
1229    }
1230
1231    #[test]
1232    fn fragment_number_unknown_is_zero() {
1233        assert_eq!(FragmentNumber::UNKNOWN.0, 0);
1234    }
1235
1236    #[test]
1237    fn fragment_number_wire_size_is_four() {
1238        assert_eq!(FragmentNumber::WIRE_SIZE, 4);
1239    }
1240
1241    // ---- Locator ----
1242    #[test]
1243    fn locator_kind_roundtrip() {
1244        for kind in [
1245            LocatorKind::Invalid,
1246            LocatorKind::Reserved,
1247            LocatorKind::UdpV4,
1248            LocatorKind::UdpV6,
1249            LocatorKind::Tcpv4,
1250            LocatorKind::Tcpv6,
1251            LocatorKind::Shm,
1252            LocatorKind::Uds,
1253        ] {
1254            assert_eq!(LocatorKind::from_i32(kind.as_i32()).unwrap(), kind);
1255        }
1256    }
1257
1258    #[test]
1259    fn locator_uds_layout() {
1260        let id = [
1261            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1262            0x0F, 0x10,
1263        ];
1264        let l = Locator::uds(id);
1265        assert_eq!(l.kind, LocatorKind::Uds);
1266        assert_eq!(l.port, 0);
1267        assert_eq!(l.address, id);
1268    }
1269
1270    #[test]
1271    fn locator_kind_rejects_unknown() {
1272        let res = LocatorKind::from_i32(99);
1273        assert!(matches!(
1274            res,
1275            Err(WireError::InvalidLocatorKind { kind: 99 })
1276        ));
1277    }
1278
1279    #[test]
1280    fn locator_udp_v4_layout() {
1281        let l = Locator::udp_v4([192, 168, 1, 100], 7400);
1282        assert_eq!(l.kind, LocatorKind::UdpV4);
1283        assert_eq!(l.port, 7400);
1284        assert_eq!(&l.address[..12], &[0u8; 12]);
1285        assert_eq!(&l.address[12..], &[192, 168, 1, 100]);
1286        assert_eq!(l.ipv4(), [192, 168, 1, 100]);
1287    }
1288
1289    #[test]
1290    fn locator_roundtrip_le() {
1291        let l = Locator::udp_v4([10, 0, 0, 1], 7400);
1292        let bytes = l.to_bytes_le();
1293        assert_eq!(Locator::from_bytes_le(bytes).unwrap(), l);
1294    }
1295
1296    #[test]
1297    fn locator_invalid_kind_decoded() {
1298        let mut bytes = [0u8; 24];
1299        bytes[..4].copy_from_slice(&99_i32.to_le_bytes());
1300        let res = Locator::from_bytes_le(bytes);
1301        assert!(matches!(
1302            res,
1303            Err(WireError::InvalidLocatorKind { kind: 99 })
1304        ));
1305    }
1306
1307    // ---- §8.3.5.7 Locator constants ----
1308
1309    #[test]
1310    fn locator_invalid_constant_matches_spec() {
1311        assert_eq!(Locator::INVALID.kind, LocatorKind::Invalid);
1312        assert_eq!(Locator::INVALID.port, Locator::PORT_INVALID);
1313        assert_eq!(Locator::INVALID.address, Locator::ADDRESS_INVALID);
1314    }
1315
1316    #[test]
1317    fn locator_reserved_constant_kind_is_zero() {
1318        assert_eq!(Locator::RESERVED.kind.as_i32(), 0);
1319    }
1320
1321    #[test]
1322    fn locator_udp_v4_any_kind_is_one() {
1323        assert_eq!(Locator::UDP_V4_ANY.kind.as_i32(), 1);
1324        assert_eq!(Locator::UDP_V4_ANY.port, 0);
1325    }
1326
1327    #[test]
1328    fn locator_udp_v6_any_kind_is_two() {
1329        assert_eq!(Locator::UDP_V6_ANY.kind.as_i32(), 2);
1330    }
1331
1332    #[test]
1333    fn locator_shm_any_uses_vendor_kind() {
1334        assert!(Locator::SHM_ANY.kind.as_i32() < 0); // vendor range
1335    }
1336
1337    #[test]
1338    fn locator_tsn_carries_mac_and_vlan() {
1339        let mac = [0x02, 0x00, 0x00, 0x11, 0x22, 0x33];
1340        let loc = Locator::tsn(mac, 42);
1341        assert_eq!(loc.kind, LocatorKind::Tsn);
1342        assert_eq!(&loc.address[..6], &mac);
1343        assert_eq!(u16::from_be_bytes([loc.address[6], loc.address[7]]), 42);
1344        // Vendor range + i32 roundtrip.
1345        assert!(loc.kind.as_i32() < 0);
1346        assert_eq!(
1347            LocatorKind::from_i32(loc.kind.as_i32()).unwrap(),
1348            LocatorKind::Tsn
1349        );
1350    }
1351
1352    #[test]
1353    fn locator_udp_v6_constructor_keeps_full_address() {
1354        let addr: [u8; 16] = [
1355            0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01,
1356        ];
1357        let l = Locator::udp_v6(addr, 1234);
1358        assert_eq!(l.kind, LocatorKind::UdpV6);
1359        assert_eq!(l.address, addr);
1360        assert_eq!(l.port, 1234);
1361    }
1362
1363    #[test]
1364    fn locator_udp_v6_roundtrip_le() {
1365        let l = Locator::udp_v6([1; 16], 9000);
1366        assert_eq!(Locator::from_bytes_le(l.to_bytes_le()).unwrap(), l);
1367    }
1368
1369    // ---- §8.3.5.5 ProtocolVersion aliases ----
1370
1371    #[test]
1372    fn protocol_version_aliases_match_spec() {
1373        assert_eq!(
1374            ProtocolVersion::V1_0,
1375            ProtocolVersion { major: 1, minor: 0 }
1376        );
1377        assert_eq!(
1378            ProtocolVersion::V1_1,
1379            ProtocolVersion { major: 1, minor: 1 }
1380        );
1381        assert_eq!(
1382            ProtocolVersion::V2_0,
1383            ProtocolVersion { major: 2, minor: 0 }
1384        );
1385        assert_eq!(
1386            ProtocolVersion::V2_1,
1387            ProtocolVersion { major: 2, minor: 1 }
1388        );
1389        assert_eq!(
1390            ProtocolVersion::V2_2,
1391            ProtocolVersion { major: 2, minor: 2 }
1392        );
1393        assert_eq!(
1394            ProtocolVersion::V2_3,
1395            ProtocolVersion { major: 2, minor: 3 }
1396        );
1397        assert_eq!(
1398            ProtocolVersion::V2_4,
1399            ProtocolVersion { major: 2, minor: 4 }
1400        );
1401        assert_eq!(
1402            ProtocolVersion::V2_5,
1403            ProtocolVersion { major: 2, minor: 5 }
1404        );
1405        assert_eq!(ProtocolVersion::CURRENT, ProtocolVersion::V2_5);
1406    }
1407
1408    #[test]
1409    fn protocol_version_all_aliases_roundtrip() {
1410        for v in [
1411            ProtocolVersion::V1_0,
1412            ProtocolVersion::V1_1,
1413            ProtocolVersion::V2_0,
1414            ProtocolVersion::V2_1,
1415            ProtocolVersion::V2_2,
1416            ProtocolVersion::V2_3,
1417            ProtocolVersion::V2_4,
1418            ProtocolVersion::V2_5,
1419        ] {
1420            assert_eq!(ProtocolVersion::from_bytes(v.to_bytes()), v);
1421        }
1422    }
1423
1424    // ---- §8.3.2 UExtension4_t ----
1425
1426    #[test]
1427    fn uextension4_roundtrip() {
1428        let u = UExtension4([0xDE, 0xAD, 0xBE, 0xEF]);
1429        assert_eq!(UExtension4::from_bytes(u.to_bytes()), u);
1430    }
1431
1432    #[test]
1433    fn uextension4_u32_be_roundtrip() {
1434        let u = UExtension4::from_u32_be(0x1122_3344);
1435        assert_eq!(u.to_u32_be(), 0x1122_3344);
1436        assert_eq!(u.0, [0x11, 0x22, 0x33, 0x44]);
1437    }
1438
1439    #[test]
1440    fn uextension4_default_is_zero() {
1441        assert_eq!(UExtension4::default().0, [0u8; 4]);
1442        assert_eq!(UExtension4::WIRE_SIZE, 4);
1443    }
1444
1445    // ---- §8.3.2 WExtension8_t ----
1446
1447    #[test]
1448    fn wextension8_roundtrip() {
1449        let w = WExtension8([1, 2, 3, 4, 5, 6, 7, 8]);
1450        assert_eq!(WExtension8::from_bytes(w.to_bytes()), w);
1451    }
1452
1453    #[test]
1454    fn wextension8_u64_be_roundtrip() {
1455        let w = WExtension8::from_u64_be(0x1122_3344_5566_7788);
1456        assert_eq!(w.to_u64_be(), 0x1122_3344_5566_7788);
1457        assert_eq!(w.0, [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
1458    }
1459
1460    #[test]
1461    fn wextension8_default_is_zero() {
1462        assert_eq!(WExtension8::default().0, [0u8; 8]);
1463        assert_eq!(WExtension8::WIRE_SIZE, 8);
1464    }
1465
1466    // ---- SPDP builtin EntityIds (Spec §9.3.1.5 Table 9.4) ----
1467
1468    #[test]
1469    fn spdp_builtin_participant_writer_layout() {
1470        // Spec: 0x000100C2
1471        let bytes = EntityId::SPDP_BUILTIN_PARTICIPANT_WRITER.to_bytes();
1472        assert_eq!(bytes, [0x00, 0x01, 0x00, 0xC2]);
1473    }
1474
1475    #[test]
1476    fn spdp_builtin_participant_reader_layout() {
1477        // Spec: 0x000100C7
1478        let bytes = EntityId::SPDP_BUILTIN_PARTICIPANT_READER.to_bytes();
1479        assert_eq!(bytes, [0x00, 0x01, 0x00, 0xC7]);
1480    }
1481
1482    #[test]
1483    fn sedp_publications_writer_layout() {
1484        // Spec: 0x000003C2
1485        let bytes = EntityId::SEDP_BUILTIN_PUBLICATIONS_WRITER.to_bytes();
1486        assert_eq!(bytes, [0x00, 0x00, 0x03, 0xC2]);
1487    }
1488
1489    #[test]
1490    fn sedp_subscriptions_reader_layout() {
1491        // Spec: 0x000004C7
1492        let bytes = EntityId::SEDP_BUILTIN_SUBSCRIPTIONS_READER.to_bytes();
1493        assert_eq!(bytes, [0x00, 0x00, 0x04, 0xC7]);
1494    }
1495
1496    // ---- SPDP multicast address + port computation ----
1497
1498    #[test]
1499    fn spdp_default_multicast_is_239_255_0_1() {
1500        assert_eq!(SPDP_DEFAULT_MULTICAST_ADDRESS, [239, 255, 0, 1]);
1501    }
1502
1503    #[test]
1504    fn spdp_multicast_port_domain_0_is_7400() {
1505        assert_eq!(spdp_multicast_port(0), 7400);
1506    }
1507
1508    #[test]
1509    fn spdp_multicast_port_domain_1_is_7650() {
1510        assert_eq!(spdp_multicast_port(1), 7650);
1511    }
1512
1513    #[test]
1514    fn spdp_multicast_port_domain_5_is_8650() {
1515        assert_eq!(spdp_multicast_port(5), 8650);
1516    }
1517
1518    #[test]
1519    fn volatile_message_secure_entity_ids_match_spec_and_cyclone() {
1520        // DDS-Security §7.4.7.1 Tab.7 + cyclone-wire-verified
1521        // (trace: new_writer ...:ff0202c3 / new_reader ...:ff0202c4 for
1522        // DCPSParticipantVolatileMessageSecure). The kind bytes are c3/c4
1523        // (NoKey bytes), NOT c2/c7 — otherwise the crypto-token channel does
1524        // not match cross-vendor.
1525        assert_eq!(
1526            EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER.to_bytes(),
1527            [0xff, 0x02, 0x02, 0xc3],
1528            "VolatileMessageSecure writer must be 0xff0202c3"
1529        );
1530        assert_eq!(
1531            EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER.to_bytes(),
1532            [0xff, 0x02, 0x02, 0xc4],
1533            "VolatileMessageSecure reader must be 0xff0202c4"
1534        );
1535    }
1536}