Skip to main content

zerodds_rpc/
common_types.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! DDS-RPC Common-Types — Spec §7.5.1.1.1.
5//!
6//! Diese Datei stellt die Wire-Strukturen bereit, die jede RPC-Anfrage
7//! bzw. -Antwort begleiten:
8//!
9//! ```text
10//! struct SampleIdentity {
11//!     octet[16] writer_guid;
12//!     unsigned long long sequence_number;
13//! };
14//! struct RequestHeader {
15//!     SampleIdentity request_id;
16//!     string instance_name;
17//! };
18//! enum RemoteExceptionCode_t {
19//!     REMOTE_EX_OK,
20//!     REMOTE_EX_UNSUPPORTED,
21//!     REMOTE_EX_INVALID_ARGUMENT,
22//!     REMOTE_EX_OUT_OF_RESOURCES,
23//!     REMOTE_EX_UNKNOWN_OPERATION,
24//!     REMOTE_EX_UNKNOWN_EXCEPTION,
25//!     REMOTE_EX_UNKNOWN_INTERFACE,
26//! };
27//! struct ReplyHeader {
28//!     SampleIdentity related_request_id;
29//!     RemoteExceptionCode_t remote_ex;
30//! };
31//! ```
32//!
33//! Encoding: **XCDR2** mit `Final`-Extensibility. Spec §7.5.1.1.1 gibt
34//! nur das Wire-Layout vor (kein DHEADER, kein EMHEADER); wir nehmen
35//! Final wegen niedriger Overhead-Kosten und weil RPC-Header fuer
36//! Forward-Compat-Erweiterungen nicht vorgesehen sind.
37//!
38//! Wire-Layout (Final-XCDR2, Little-Endian Beispiel):
39//!
40//! ```text
41//! SampleIdentity:
42//!   octet[16] writer_guid    -- 16 byte, kein Padding
43//!   align(8)
44//!   uint64    sequence_number
45//!
46//! RequestHeader:
47//!   SampleIdentity request_id   -- 24 byte
48//!   align(4)
49//!   uint32    instance_name_len -- inkl. NUL-Terminator
50//!   bytes     instance_name + NUL
51//!
52//! ReplyHeader:
53//!   SampleIdentity related_request_id
54//!   align(4)
55//!   uint32    remote_ex_kind
56//! ```
57
58extern crate alloc;
59
60use alloc::string::{String, ToString};
61use alloc::vec::Vec;
62
63use crate::error::{RpcError, RpcResult};
64
65/// Maximale akzeptierte Wire-Payload pro Header (DoS-Cap).
66pub const MAX_HEADER_BYTES: usize = 64 * 1024;
67
68/// Maximale Stringlaenge in Headern.
69pub const MAX_STRING_LEN: u32 = 8 * 1024;
70
71// ---------------------------------------------------------------------
72// SampleIdentity
73// ---------------------------------------------------------------------
74
75/// Spec §7.5.1.1.1 — Identifies a sample by writer GUID + sequence number.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
77pub struct SampleIdentity {
78    /// 16-byte GUID des absendenden Writers.
79    pub writer_guid: [u8; 16],
80    /// Sequence-Number des Samples (`uint64`, RTPS-konvention 1-basiert).
81    pub sequence_number: u64,
82}
83
84impl SampleIdentity {
85    /// Konstruktor.
86    #[must_use]
87    pub const fn new(writer_guid: [u8; 16], sequence_number: u64) -> Self {
88        Self {
89            writer_guid,
90            sequence_number,
91        }
92    }
93
94    /// Reservierter "unknown"-Wert (Spec §7.5.1.1.1 — alles 0).
95    pub const UNKNOWN: Self = Self {
96        writer_guid: [0u8; 16],
97        sequence_number: 0,
98    };
99
100    /// XCDR2-Little-Endian-Encoder.
101    #[must_use]
102    pub fn to_cdr_le(&self) -> Vec<u8> {
103        let mut out = Vec::with_capacity(24);
104        encode_sample_identity(&mut out, self, true);
105        out
106    }
107
108    /// XCDR2-Big-Endian-Encoder.
109    #[must_use]
110    pub fn to_cdr_be(&self) -> Vec<u8> {
111        let mut out = Vec::with_capacity(24);
112        encode_sample_identity(&mut out, self, false);
113        out
114    }
115
116    /// XCDR2-Decoder, Little-Endian.
117    ///
118    /// # Errors
119    /// `RpcError::Codec` bei zu kurzem Buffer.
120    pub fn from_cdr_le(bytes: &[u8]) -> RpcResult<Self> {
121        check_cap(bytes)?;
122        let mut cur = Cursor::new(bytes);
123        cur.read_sample_identity(true)
124    }
125
126    /// XCDR2-Decoder, Big-Endian.
127    ///
128    /// # Errors
129    /// `RpcError::Codec` bei zu kurzem Buffer.
130    pub fn from_cdr_be(bytes: &[u8]) -> RpcResult<Self> {
131        check_cap(bytes)?;
132        let mut cur = Cursor::new(bytes);
133        cur.read_sample_identity(false)
134    }
135}
136
137// ---------------------------------------------------------------------
138// RemoteExceptionCode_t
139// ---------------------------------------------------------------------
140
141/// Spec §7.5.1.1.1 Tab.7.55 — Server-side error code in `ReplyHeader`.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
143#[repr(u32)]
144pub enum RemoteExceptionCode {
145    /// Operation lief erfolgreich durch.
146    #[default]
147    Ok = 0,
148    /// Service unterstuetzt diese Operation nicht.
149    Unsupported = 1,
150    /// Argument war nicht spec-konform.
151    InvalidArgument = 2,
152    /// Server hat nicht genug Resources.
153    OutOfResources = 3,
154    /// Operation gibt es nicht im Service.
155    UnknownOperation = 4,
156    /// Operation hat eine User-Exception geworfen, die der Stub nicht
157    /// kennt.
158    UnknownException = 5,
159    /// Service-Interface ist dem Server unbekannt.
160    UnknownInterface = 6,
161}
162
163impl RemoteExceptionCode {
164    /// Konvertiert die Wire-Diskriminator-`u32` in das Enum.
165    ///
166    /// # Errors
167    /// `RpcError::UnknownExceptionCode` bei unbekanntem Diskriminator.
168    pub fn from_u32(v: u32) -> RpcResult<Self> {
169        match v {
170            0 => Ok(Self::Ok),
171            1 => Ok(Self::Unsupported),
172            2 => Ok(Self::InvalidArgument),
173            3 => Ok(Self::OutOfResources),
174            4 => Ok(Self::UnknownOperation),
175            5 => Ok(Self::UnknownException),
176            6 => Ok(Self::UnknownInterface),
177            other => Err(RpcError::UnknownExceptionCode(other)),
178        }
179    }
180
181    /// Liefert den Wire-Diskriminator.
182    #[must_use]
183    pub const fn as_u32(self) -> u32 {
184        self as u32
185    }
186}
187
188// ---------------------------------------------------------------------
189// RequestHeader
190// ---------------------------------------------------------------------
191
192/// Spec §7.5.1.1.1 — Pro Sample im Request-Topic prepended.
193#[derive(Debug, Clone, PartialEq, Eq, Default)]
194pub struct RequestHeader {
195    /// Eindeutige ID dieser Request.
196    pub request_id: SampleIdentity,
197    /// Optionaler Service-Instance-Name (leer wenn nicht gesetzt).
198    pub instance_name: String,
199}
200
201impl RequestHeader {
202    /// Konstruktor.
203    #[must_use]
204    pub fn new(request_id: SampleIdentity, instance_name: impl Into<String>) -> Self {
205        Self {
206            request_id,
207            instance_name: instance_name.into(),
208        }
209    }
210
211    /// XCDR2-Little-Endian-Encoder.
212    #[must_use]
213    pub fn to_cdr_le(&self) -> Vec<u8> {
214        encode_request_header(self, true)
215    }
216
217    /// XCDR2-Big-Endian-Encoder.
218    #[must_use]
219    pub fn to_cdr_be(&self) -> Vec<u8> {
220        encode_request_header(self, false)
221    }
222
223    /// XCDR2-Decoder, Little-Endian.
224    ///
225    /// # Errors
226    /// `RpcError::Codec` bei zu kurzem oder ungueltigem Buffer.
227    pub fn from_cdr_le(bytes: &[u8]) -> RpcResult<Self> {
228        check_cap(bytes)?;
229        let mut cur = Cursor::new(bytes);
230        let request_id = cur.read_sample_identity(true)?;
231        let instance_name = cur.read_string(true)?;
232        Ok(Self {
233            request_id,
234            instance_name,
235        })
236    }
237
238    /// XCDR2-Decoder, Big-Endian.
239    ///
240    /// # Errors
241    /// `RpcError::Codec` bei zu kurzem oder ungueltigem Buffer.
242    pub fn from_cdr_be(bytes: &[u8]) -> RpcResult<Self> {
243        check_cap(bytes)?;
244        let mut cur = Cursor::new(bytes);
245        let request_id = cur.read_sample_identity(false)?;
246        let instance_name = cur.read_string(false)?;
247        Ok(Self {
248            request_id,
249            instance_name,
250        })
251    }
252}
253
254// ---------------------------------------------------------------------
255// ReplyHeader
256// ---------------------------------------------------------------------
257
258/// Spec §7.5.1.1.1 — Pro Sample im Reply-Topic prepended.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
260pub struct ReplyHeader {
261    /// Verweist auf das `request_id` der zugehoerigen Request.
262    pub related_request_id: SampleIdentity,
263    /// Server-Side Result-Code.
264    pub remote_ex: RemoteExceptionCode,
265}
266
267impl ReplyHeader {
268    /// Konstruktor.
269    #[must_use]
270    pub const fn new(related_request_id: SampleIdentity, remote_ex: RemoteExceptionCode) -> Self {
271        Self {
272            related_request_id,
273            remote_ex,
274        }
275    }
276
277    /// XCDR2-Little-Endian-Encoder.
278    #[must_use]
279    pub fn to_cdr_le(&self) -> Vec<u8> {
280        encode_reply_header(self, true)
281    }
282
283    /// XCDR2-Big-Endian-Encoder.
284    #[must_use]
285    pub fn to_cdr_be(&self) -> Vec<u8> {
286        encode_reply_header(self, false)
287    }
288
289    /// XCDR2-Decoder, Little-Endian.
290    ///
291    /// # Errors
292    /// `RpcError::Codec` bei zu kurzem oder ungueltigem Buffer.
293    /// `RpcError::UnknownExceptionCode` bei unbekanntem Diskriminator.
294    pub fn from_cdr_le(bytes: &[u8]) -> RpcResult<Self> {
295        check_cap(bytes)?;
296        let mut cur = Cursor::new(bytes);
297        let related_request_id = cur.read_sample_identity(true)?;
298        let raw = cur.read_u32(true)?;
299        let remote_ex = RemoteExceptionCode::from_u32(raw)?;
300        Ok(Self {
301            related_request_id,
302            remote_ex,
303        })
304    }
305
306    /// XCDR2-Decoder, Big-Endian.
307    ///
308    /// # Errors
309    /// `RpcError::Codec` bei zu kurzem oder ungueltigem Buffer.
310    /// `RpcError::UnknownExceptionCode` bei unbekanntem Diskriminator.
311    pub fn from_cdr_be(bytes: &[u8]) -> RpcResult<Self> {
312        check_cap(bytes)?;
313        let mut cur = Cursor::new(bytes);
314        let related_request_id = cur.read_sample_identity(false)?;
315        let raw = cur.read_u32(false)?;
316        let remote_ex = RemoteExceptionCode::from_u32(raw)?;
317        Ok(Self {
318            related_request_id,
319            remote_ex,
320        })
321    }
322}
323
324// ---------------------------------------------------------------------
325// XCDR2-Codec (Final-Extensibility, primitive-only)
326// ---------------------------------------------------------------------
327//
328// XCDR2-Spezifika gegenueber XCDR1:
329//   * Alignment-Cap = 4 byte (statt 8). uint64 ist daher auf 8 alignment
330//     -> in XCDR2 auf 4 reduziert.
331//   * String: uint32 length inkl. trailing NUL + UTF-8-Bytes + NUL.
332//   * Final-Extensibility: kein DHEADER vor der Struktur.
333//
334// Wir implementieren das hand-rolled (analog crates/security/src/token.rs)
335// statt zerodds-cdr zu konsumieren — zerodds-cdr ist no_std und der Foundation-
336// Stub hier braucht nichts darueber Hinausgehendes.
337
338fn check_cap(bytes: &[u8]) -> RpcResult<()> {
339    if bytes.len() > MAX_HEADER_BYTES {
340        return Err(RpcError::PayloadTooLarge {
341            got: bytes.len(),
342            max: MAX_HEADER_BYTES,
343        });
344    }
345    Ok(())
346}
347
348fn align_to(out: &mut Vec<u8>, n: usize) {
349    let pad = (n - out.len() % n) % n;
350    for _ in 0..pad {
351        out.push(0);
352    }
353}
354
355fn encode_u32(out: &mut Vec<u8>, v: u32, le: bool) {
356    align_to(out, 4);
357    if le {
358        out.extend_from_slice(&v.to_le_bytes());
359    } else {
360        out.extend_from_slice(&v.to_be_bytes());
361    }
362}
363
364fn encode_u64_xcdr2(out: &mut Vec<u8>, v: u64, le: bool) {
365    // XCDR2: uint64 alignment-Cap = 4 byte.
366    align_to(out, 4);
367    if le {
368        out.extend_from_slice(&v.to_le_bytes());
369    } else {
370        out.extend_from_slice(&v.to_be_bytes());
371    }
372}
373
374fn encode_string(out: &mut Vec<u8>, s: &str, le: bool) {
375    let bytes = s.as_bytes();
376    let len = (bytes.len() + 1) as u32;
377    encode_u32(out, len, le);
378    out.extend_from_slice(bytes);
379    out.push(0);
380}
381
382fn encode_sample_identity(out: &mut Vec<u8>, id: &SampleIdentity, le: bool) {
383    // octet[16] hat alignment 1 — direkt einfuegen.
384    out.extend_from_slice(&id.writer_guid);
385    encode_u64_xcdr2(out, id.sequence_number, le);
386}
387
388fn encode_request_header(h: &RequestHeader, le: bool) -> Vec<u8> {
389    let mut out = Vec::with_capacity(64);
390    encode_sample_identity(&mut out, &h.request_id, le);
391    encode_string(&mut out, &h.instance_name, le);
392    out
393}
394
395fn encode_reply_header(h: &ReplyHeader, le: bool) -> Vec<u8> {
396    let mut out = Vec::with_capacity(32);
397    encode_sample_identity(&mut out, &h.related_request_id, le);
398    encode_u32(&mut out, h.remote_ex.as_u32(), le);
399    out
400}
401
402struct Cursor<'a> {
403    buf: &'a [u8],
404    pos: usize,
405}
406
407impl<'a> Cursor<'a> {
408    fn new(buf: &'a [u8]) -> Self {
409        Self { buf, pos: 0 }
410    }
411
412    fn align_to(&mut self, n: usize) {
413        let pad = (n - self.pos % n) % n;
414        self.pos = self.pos.saturating_add(pad);
415    }
416
417    fn ensure(&self, need: usize) -> RpcResult<()> {
418        if self.pos.saturating_add(need) > self.buf.len() {
419            return Err(RpcError::codec("truncated buffer"));
420        }
421        Ok(())
422    }
423
424    fn read_u32(&mut self, le: bool) -> RpcResult<u32> {
425        self.align_to(4);
426        self.ensure(4)?;
427        let raw = [
428            self.buf[self.pos],
429            self.buf[self.pos + 1],
430            self.buf[self.pos + 2],
431            self.buf[self.pos + 3],
432        ];
433        self.pos += 4;
434        Ok(if le {
435            u32::from_le_bytes(raw)
436        } else {
437            u32::from_be_bytes(raw)
438        })
439    }
440
441    fn read_u64_xcdr2(&mut self, le: bool) -> RpcResult<u64> {
442        // XCDR2 Alignment-Cap = 4 byte.
443        self.align_to(4);
444        self.ensure(8)?;
445        let mut raw = [0u8; 8];
446        raw.copy_from_slice(&self.buf[self.pos..self.pos + 8]);
447        self.pos += 8;
448        Ok(if le {
449            u64::from_le_bytes(raw)
450        } else {
451            u64::from_be_bytes(raw)
452        })
453    }
454
455    fn read_string(&mut self, le: bool) -> RpcResult<String> {
456        let len = self.read_u32(le)?;
457        if len > MAX_STRING_LEN {
458            return Err(RpcError::codec("string exceeds cap"));
459        }
460        if len == 0 {
461            return Err(RpcError::codec("zero-length string body"));
462        }
463        self.ensure(len as usize)?;
464        let body = &self.buf[self.pos..self.pos + len as usize];
465        self.pos += len as usize;
466        if body.last() != Some(&0) {
467            return Err(RpcError::codec("string missing trailing NUL"));
468        }
469        let rest = &body[..body.len() - 1];
470        let s = core::str::from_utf8(rest)
471            .map_err(|_| RpcError::codec("string not UTF-8"))?
472            .to_string();
473        Ok(s)
474    }
475
476    fn read_sample_identity(&mut self, le: bool) -> RpcResult<SampleIdentity> {
477        self.ensure(16)?;
478        let mut writer_guid = [0u8; 16];
479        writer_guid.copy_from_slice(&self.buf[self.pos..self.pos + 16]);
480        self.pos += 16;
481        let sequence_number = self.read_u64_xcdr2(le)?;
482        Ok(SampleIdentity {
483            writer_guid,
484            sequence_number,
485        })
486    }
487}
488
489#[cfg(test)]
490#[allow(clippy::unwrap_used, clippy::expect_used)]
491mod tests {
492    use super::*;
493
494    fn sample_id() -> SampleIdentity {
495        SampleIdentity::new(
496            [
497                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
498                0x0F, 0x10,
499            ],
500            0xDEAD_BEEF_CAFE_BABE,
501        )
502    }
503
504    #[test]
505    fn sample_identity_roundtrip_le() {
506        let id = sample_id();
507        let bytes = id.to_cdr_le();
508        assert_eq!(bytes.len(), 24);
509        let back = SampleIdentity::from_cdr_le(&bytes).unwrap();
510        assert_eq!(id, back);
511    }
512
513    #[test]
514    fn sample_identity_roundtrip_be() {
515        let id = sample_id();
516        let bytes = id.to_cdr_be();
517        let back = SampleIdentity::from_cdr_be(&bytes).unwrap();
518        assert_eq!(id, back);
519    }
520
521    #[test]
522    fn sample_identity_le_be_streams_differ() {
523        let id = sample_id();
524        let le = id.to_cdr_le();
525        let be = id.to_cdr_be();
526        assert_ne!(le, be);
527    }
528
529    #[test]
530    fn sample_identity_unknown_constant_is_zero() {
531        let id = SampleIdentity::UNKNOWN;
532        assert_eq!(id.writer_guid, [0u8; 16]);
533        assert_eq!(id.sequence_number, 0);
534    }
535
536    #[test]
537    fn sample_identity_truncated_buffer_is_error() {
538        let bytes = vec![0u8; 23];
539        let err = SampleIdentity::from_cdr_le(&bytes).unwrap_err();
540        assert!(matches!(err, RpcError::Codec(_)));
541    }
542
543    #[test]
544    fn request_header_roundtrip_le() {
545        let h = RequestHeader::new(sample_id(), "calc-instance-1");
546        let bytes = h.to_cdr_le();
547        let back = RequestHeader::from_cdr_le(&bytes).unwrap();
548        assert_eq!(h, back);
549    }
550
551    #[test]
552    fn request_header_roundtrip_be() {
553        let h = RequestHeader::new(sample_id(), "calc-instance-1");
554        let bytes = h.to_cdr_be();
555        let back = RequestHeader::from_cdr_be(&bytes).unwrap();
556        assert_eq!(h, back);
557    }
558
559    #[test]
560    fn request_header_empty_instance_name_roundtrip() {
561        let h = RequestHeader::new(sample_id(), "");
562        let bytes = h.to_cdr_le();
563        let back = RequestHeader::from_cdr_le(&bytes).unwrap();
564        assert_eq!(h, back);
565        assert!(back.instance_name.is_empty());
566    }
567
568    #[test]
569    fn request_header_string_missing_nul_rejected() {
570        // Forge: id (24 byte) + len=1 + body byte != 0
571        let mut bytes = sample_id().to_cdr_le();
572        bytes.extend_from_slice(&1u32.to_le_bytes());
573        bytes.push(b'A');
574        let err = RequestHeader::from_cdr_le(&bytes).unwrap_err();
575        assert!(matches!(err, RpcError::Codec(_)));
576    }
577
578    #[test]
579    fn request_header_zero_length_string_rejected() {
580        let mut bytes = sample_id().to_cdr_le();
581        bytes.extend_from_slice(&0u32.to_le_bytes());
582        let err = RequestHeader::from_cdr_le(&bytes).unwrap_err();
583        assert!(matches!(err, RpcError::Codec(_)));
584    }
585
586    #[test]
587    fn request_header_invalid_utf8_rejected() {
588        let mut bytes = sample_id().to_cdr_le();
589        // len=3 (incl NUL): bytes 0xFF 0xFE 0x00
590        bytes.extend_from_slice(&3u32.to_le_bytes());
591        bytes.extend_from_slice(&[0xFF, 0xFE, 0x00]);
592        let err = RequestHeader::from_cdr_le(&bytes).unwrap_err();
593        assert!(matches!(err, RpcError::Codec(_)));
594    }
595
596    #[test]
597    fn reply_header_roundtrip_all_codes() {
598        for code in [
599            RemoteExceptionCode::Ok,
600            RemoteExceptionCode::Unsupported,
601            RemoteExceptionCode::InvalidArgument,
602            RemoteExceptionCode::OutOfResources,
603            RemoteExceptionCode::UnknownOperation,
604            RemoteExceptionCode::UnknownException,
605            RemoteExceptionCode::UnknownInterface,
606        ] {
607            let h = ReplyHeader::new(sample_id(), code);
608            let le = h.to_cdr_le();
609            let be = h.to_cdr_be();
610            assert_eq!(h, ReplyHeader::from_cdr_le(&le).unwrap());
611            assert_eq!(h, ReplyHeader::from_cdr_be(&be).unwrap());
612        }
613    }
614
615    #[test]
616    fn reply_header_unknown_discriminator_is_error() {
617        let mut bytes = sample_id().to_cdr_le();
618        bytes.extend_from_slice(&999u32.to_le_bytes());
619        let err = ReplyHeader::from_cdr_le(&bytes).unwrap_err();
620        assert_eq!(err, RpcError::UnknownExceptionCode(999));
621    }
622
623    #[test]
624    fn remote_exception_code_as_u32_round_trips() {
625        for v in 0u32..=6 {
626            let code = RemoteExceptionCode::from_u32(v).unwrap();
627            assert_eq!(code.as_u32(), v);
628        }
629    }
630
631    #[test]
632    fn remote_exception_code_default_is_ok() {
633        assert_eq!(RemoteExceptionCode::default(), RemoteExceptionCode::Ok);
634    }
635
636    #[test]
637    fn dos_cap_rejects_oversized_buffer() {
638        let big = vec![0u8; MAX_HEADER_BYTES + 1];
639        let err = RequestHeader::from_cdr_le(&big).unwrap_err();
640        assert!(matches!(
641            err,
642            RpcError::PayloadTooLarge {
643                got: _,
644                max: MAX_HEADER_BYTES
645            }
646        ));
647    }
648
649    #[test]
650    fn xcdr2_layout_sample_identity_le_is_24_bytes_no_padding() {
651        // GUID 16 byte + uint64 8 byte (XCDR2 cap=4, also keine
652        // zusaetzlichen 4 byte Padding).
653        let id = SampleIdentity::new([0xAB; 16], 0x0102_0304_0506_0708);
654        let bytes = id.to_cdr_le();
655        assert_eq!(bytes.len(), 24);
656        // Letzten 8 byte = LE-Encoding des u64.
657        assert_eq!(&bytes[16..24], &0x0102_0304_0506_0708u64.to_le_bytes());
658    }
659
660    #[test]
661    fn xcdr2_layout_string_includes_nul() {
662        // class_id="A" (1 char + NUL = len=2) ueber RequestHeader.
663        let h = RequestHeader::new(SampleIdentity::UNKNOWN, "A");
664        let bytes = h.to_cdr_le();
665        // Layout: 16 byte GUID + 8 byte uint64 + uint32 len=2 + 'A' + NUL
666        // = 24 + 4 + 2 = 30 byte.
667        assert_eq!(bytes.len(), 30);
668        assert_eq!(&bytes[24..28], &2u32.to_le_bytes());
669        assert_eq!(bytes[28], b'A');
670        assert_eq!(bytes[29], 0);
671    }
672}