Skip to main content

zerodds_cdr/
key_hash.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! KeyHash-Berechnung fuer DDS-Topics (XTypes 1.3 §7.6.8 + DDSI-RTPS
4//! 2.5 §9.6.4.8).
5//!
6//! Der **PID_KEY_HASH** (0x0070, 16 Byte) im Inline-QoS einer DATA/
7//! DATA_FRAG-Submessage adressiert die **Instanz** eines Sample.
8//! Ein Reader/Persistence-Service kann anhand dieses Hashes
9//! Samples derselben Instanz korrelieren, ohne den Payload deserialisieren
10//! zu muessen.
11//!
12//! # Algorithmus (XTypes 1.3 §7.6.8 Steps 1-5)
13//!
14//! 1. **Step 1** — Konstruktion eines `FooKeyHolder`-Aequivalents: nur
15//!    die `@key`-Member von `Foo`, sortiert nach `member_id` aufsteigend.
16//! 2. **Step 2** — Werte mit den `@key`-Werten der Instanz fuellen.
17//! 3. **Step 3** — Nested aggregate Members rekursiv expandieren (jeder
18//!    Sub-`@key`-Member ebenfalls in den KeyHolder).
19//! 4. **Step 4** — Serialisierung mit **PLAIN_CDR2 / Big-Endian**,
20//!    alignment 4, **ohne** Encapsulation-Header und **ohne** Member-
21//!    Header.
22//! 5. **Step 5** — Hash:
23//!    * Wenn die **maximale** serialisierte Groesse des KeyHolders
24//!      <= 16 Byte: KeyHash = Bytes von Step 4, mit Null-Bytes
25//!      auf 16 Byte gepadded.
26//!    * Sonst: KeyHash = MD5(Bytes von Step 4)[0..16].
27//!
28//! Die Entscheidung ist **statisch pro Topic-Type** — sie haengt von der
29//! Maximal-Groesse aller @key-Member ab, NICHT von der konkreten
30//! Instanz-Groesse. Ein Topic mit `@key string<8>` hat max=12 (4 byte
31//! length + 8 byte content) → zero-pad. Ein Topic mit `@key string`
32//! (ohne max-bound) hat keine fixe max-Groesse → MD5.
33//!
34//! # WP 1.B Scope
35//!
36//! Dieses Modul liefert die low-level [`compute_key_hash`]-Funktion.
37//! Der DCPS-Sample-Encode-Pfad wiring (PID_KEY_HASH in Inline-QoS)
38//! folgt in der gleichen WP, im DcpsRuntime-Encoder.
39
40extern crate alloc;
41
42use alloc::vec::Vec;
43use zerodds_foundation::md5;
44
45/// Wire-Laenge eines KeyHash (Spec: 16 Byte).
46pub const KEY_HASH_LEN: usize = 16;
47
48/// Berechnet den 16-Byte KeyHash einer Instanz aus dem **PLAIN_CDR2-BE**
49/// serialisierten KeyHolder-Byte-Stream.
50///
51/// `plain_cdr2_be_bytes` MUSS bereits in PLAIN_CDR2-BE-Form vorliegen
52/// (Caller stellt das sicher — typisch via `DdsType::encode_key_holder`).
53/// `key_holder_max_size` ist die **statisch bekannte** maximale Groesse
54/// des KeyHolder-Streams in Bytes; entscheidet zwischen Zero-Pad und
55/// MD5-Pfad.
56///
57/// # Spec-Referenz
58/// XTypes 1.3 §7.6.8.4 (Steps 5.1, 5.2).
59#[must_use]
60pub fn compute_key_hash(
61    plain_cdr2_be_bytes: &[u8],
62    key_holder_max_size: usize,
63) -> [u8; KEY_HASH_LEN] {
64    if key_holder_max_size <= KEY_HASH_LEN {
65        // Step 5.1: zero-pad auf 16 byte.
66        let mut out = [0u8; KEY_HASH_LEN];
67        let n = core::cmp::min(plain_cdr2_be_bytes.len(), KEY_HASH_LEN);
68        out[..n].copy_from_slice(&plain_cdr2_be_bytes[..n]);
69        out
70    } else {
71        // Step 5.2: MD5 erste 16 byte.
72        md5(plain_cdr2_be_bytes)
73    }
74}
75
76/// Convenience-Builder fuer einen PLAIN_CDR2-BE-KeyHolder-Stream.
77///
78/// PLAIN_CDR2 unterscheidet sich von XCDR1 in Alignment-Regel: maximales
79/// Alignment ist **4 Byte** (XTypes 1.3 §7.4.2.5.4) — `i64`/`f64` wird
80/// nicht 8-aligned. Dieser Builder kapselt die Alignment-Logik und
81/// vermeidet Drift mit dem zerodds-cdr-Buffer-Writer (der XCDR2-LE-default ist).
82///
83/// Member werden in der **Reihenfolge der `member_id` aufsteigend**
84/// hinzugefuegt (Spec-Pflicht §7.6.8.3.1.b).
85#[derive(Debug, Default)]
86pub struct PlainCdr2BeKeyHolder {
87    bytes: Vec<u8>,
88}
89
90impl PlainCdr2BeKeyHolder {
91    /// Leerer Builder.
92    #[must_use]
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Aktuelle Stream-Laenge (typisch == max_size, wenn alle Member fest).
98    #[must_use]
99    pub fn len(&self) -> usize {
100        self.bytes.len()
101    }
102
103    /// `true` wenn keine Bytes geschrieben.
104    #[must_use]
105    pub fn is_empty(&self) -> bool {
106        self.bytes.is_empty()
107    }
108
109    /// Gibt die fertigen Bytes zurueck.
110    #[must_use]
111    pub fn into_bytes(self) -> Vec<u8> {
112        self.bytes
113    }
114
115    /// Zugriff auf die internen Bytes ohne Konsumieren.
116    #[must_use]
117    pub fn as_bytes(&self) -> &[u8] {
118        &self.bytes
119    }
120
121    /// Padded auf `align`-Byte-Grenze (1, 2, 4 — max 4 nach §7.4.2.5.4).
122    fn pad_to(&mut self, align: usize) {
123        let pad = (align - (self.bytes.len() % align)) % align;
124        for _ in 0..pad {
125            self.bytes.push(0);
126        }
127    }
128
129    /// Schreibt einen u8.
130    pub fn write_u8(&mut self, v: u8) {
131        self.bytes.push(v);
132    }
133
134    /// Schreibt einen i8.
135    pub fn write_i8(&mut self, v: i8) {
136        self.bytes.push(v as u8);
137    }
138
139    /// Schreibt einen u16 in BE.
140    pub fn write_u16(&mut self, v: u16) {
141        self.pad_to(2);
142        self.bytes.extend_from_slice(&v.to_be_bytes());
143    }
144
145    /// Schreibt einen i16 in BE.
146    pub fn write_i16(&mut self, v: i16) {
147        self.pad_to(2);
148        self.bytes.extend_from_slice(&v.to_be_bytes());
149    }
150
151    /// Schreibt einen u32 in BE.
152    pub fn write_u32(&mut self, v: u32) {
153        self.pad_to(4);
154        self.bytes.extend_from_slice(&v.to_be_bytes());
155    }
156
157    /// Schreibt einen i32 in BE.
158    pub fn write_i32(&mut self, v: i32) {
159        self.pad_to(4);
160        self.bytes.extend_from_slice(&v.to_be_bytes());
161    }
162
163    /// Schreibt einen u64 in BE — alignment **4**, NICHT 8 (PLAIN_CDR2).
164    pub fn write_u64(&mut self, v: u64) {
165        self.pad_to(4);
166        self.bytes.extend_from_slice(&v.to_be_bytes());
167    }
168
169    /// Schreibt einen i64 in BE — alignment 4.
170    pub fn write_i64(&mut self, v: i64) {
171        self.pad_to(4);
172        self.bytes.extend_from_slice(&v.to_be_bytes());
173    }
174
175    /// Schreibt einen f32 in BE.
176    pub fn write_f32(&mut self, v: f32) {
177        self.pad_to(4);
178        self.bytes.extend_from_slice(&v.to_be_bytes());
179    }
180
181    /// Schreibt einen f64 in BE — alignment 4.
182    pub fn write_f64(&mut self, v: f64) {
183        self.pad_to(4);
184        self.bytes.extend_from_slice(&v.to_be_bytes());
185    }
186
187    /// Schreibt eine bounded String in CDR-Form (4-byte length BE +
188    /// utf8-Bytes inkl. terminating NUL). Caller stellt sicher, dass
189    /// die Laenge die statische Bound nicht ueberschreitet (sonst
190    /// kollidiert das mit `key_holder_max_size`).
191    pub fn write_string(&mut self, s: &str) {
192        self.pad_to(4);
193        let len = u32::try_from(s.len() + 1).unwrap_or(u32::MAX);
194        self.bytes.extend_from_slice(&len.to_be_bytes());
195        self.bytes.extend_from_slice(s.as_bytes());
196        self.bytes.push(0); // NUL-Terminator
197    }
198
199    /// Schreibt einen rohen byte-Slice (z.B. fuer Octet-Arrays).
200    pub fn write_bytes(&mut self, b: &[u8]) {
201        self.bytes.extend_from_slice(b);
202    }
203}
204
205/// Berechnet den KeyHash aus einer **unsortierten** Liste von
206/// `(member_id, key_value_bytes)`-Paaren. Die Funktion sortiert die
207/// Eintraege nach `member_id` aufsteigend (Spec-Pflicht §7.6.8.3.1.b)
208/// und konkateniert die `key_value_bytes` in dieser Reihenfolge — der
209/// Caller liefert pro Member bereits die PLAIN_CDR2-BE-Bytes des
210/// Werts, dieser Helper kapselt nur die Sortier-Pflicht.
211///
212/// Das ist die Lecke, gegen die §7.4.5 normativ ist: zwei Encoder-
213/// Implementierungen, die ihre @key-Member in unterschiedlicher
214/// Reihenfolge aufzaehlen, MUESSEN trotzdem denselben KeyHash
215/// produzieren. Der Helper erzwingt das, sodass das nicht der
216/// Disziplin des Caller-Codes ueberlassen bleibt.
217///
218/// `key_holder_max_size` wie in [`compute_key_hash`].
219#[must_use]
220pub fn keyhash_cdr2_be(
221    members: &[(u32, alloc::vec::Vec<u8>)],
222    key_holder_max_size: usize,
223) -> [u8; KEY_HASH_LEN] {
224    let mut sorted: alloc::vec::Vec<&(u32, alloc::vec::Vec<u8>)> = members.iter().collect();
225    sorted.sort_by_key(|(id, _)| *id);
226    let mut concat: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
227    for (_, bytes) in &sorted {
228        concat.extend_from_slice(bytes);
229    }
230    compute_key_hash(&concat, key_holder_max_size)
231}
232
233#[cfg(test)]
234#[allow(clippy::expect_used, clippy::unwrap_used)]
235mod tests {
236    use super::*;
237    use alloc::vec;
238
239    // ---- compute_key_hash: Step 5.1 zero-pad ----
240
241    #[test]
242    fn small_keyholder_zero_padded_to_16() {
243        // Single u32 = 4 byte → max 4 ≤ 16 → zero-pad.
244        let bytes = 0x1234_5678u32.to_be_bytes();
245        let h = compute_key_hash(&bytes, 4);
246        assert_eq!(h[0..4], [0x12, 0x34, 0x56, 0x78]);
247        assert_eq!(h[4..16], [0u8; 12]);
248    }
249
250    #[test]
251    fn exactly_16_byte_keyholder_no_md5() {
252        let mut bytes = [0u8; 16];
253        for (i, b) in bytes.iter_mut().enumerate() {
254            *b = i as u8;
255        }
256        let h = compute_key_hash(&bytes, 16);
257        assert_eq!(h, bytes);
258    }
259
260    #[test]
261    fn input_shorter_than_max_zero_padded() {
262        // max=16 statisch, aber nur 5 byte tatsaechlich
263        let h = compute_key_hash(&[1, 2, 3, 4, 5], 16);
264        assert_eq!(&h[..5], &[1, 2, 3, 4, 5]);
265        assert_eq!(&h[5..], &[0u8; 11]);
266    }
267
268    // ---- compute_key_hash: Step 5.2 MD5 ----
269
270    #[test]
271    fn large_keyholder_md5_hashed() {
272        // max=20 → MD5-Pfad
273        let bytes = [0u8; 20];
274        let h = compute_key_hash(&bytes, 20);
275        // MD5(20 zero bytes) bekannt: 0x441018525208457705bf09a8ee3c1093
276        assert_eq!(
277            h,
278            [
279                0x44, 0x10, 0x18, 0x52, 0x52, 0x08, 0x45, 0x77, 0x05, 0xbf, 0x09, 0xa8, 0xee, 0x3c,
280                0x10, 0x93
281            ]
282        );
283    }
284
285    #[test]
286    fn md5_known_vector_empty_string() {
287        // MD5("") = d41d8cd98f00b204e9800998ecf8427e
288        let h = compute_key_hash(&[], 17);
289        assert_eq!(
290            h,
291            [
292                0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
293                0x42, 0x7e
294            ]
295        );
296    }
297
298    #[test]
299    fn md5_known_vector_abc() {
300        let h = compute_key_hash(b"abc", 17);
301        // MD5("abc") = 900150983cd24fb0d6963f7d28e17f72
302        assert_eq!(
303            h,
304            [
305                0x90, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0, 0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1,
306                0x7f, 0x72
307            ]
308        );
309    }
310
311    // ---- PlainCdr2BeKeyHolder ----
312
313    #[test]
314    fn keyholder_writes_u32_be() {
315        let mut h = PlainCdr2BeKeyHolder::new();
316        h.write_u32(0x1234_5678);
317        assert_eq!(h.into_bytes(), vec![0x12, 0x34, 0x56, 0x78]);
318    }
319
320    #[test]
321    fn keyholder_pads_to_4_for_u32_after_u8() {
322        let mut h = PlainCdr2BeKeyHolder::new();
323        h.write_u8(0xAA);
324        h.write_u32(0x1234_5678);
325        // u8 (1) + 3 pad + u32 (4) = 8 byte
326        assert_eq!(h.into_bytes(), vec![0xAA, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]);
327    }
328
329    #[test]
330    fn keyholder_u64_aligned_to_4_not_8() {
331        let mut h = PlainCdr2BeKeyHolder::new();
332        h.write_u8(1);
333        h.write_u64(0x1122_3344_5566_7788);
334        // PLAIN_CDR2: u64 ist 4-aligned, nicht 8 → 1 pad nach u8 = 3
335        // bytes pad, dann 8 byte u64
336        assert_eq!(h.len(), 1 + 3 + 8);
337    }
338
339    #[test]
340    fn keyholder_string_length_prefixed_with_nul() {
341        let mut h = PlainCdr2BeKeyHolder::new();
342        h.write_string("hi");
343        // length=3 BE (0x00000003) + "hi\0"
344        assert_eq!(h.into_bytes(), vec![0x00, 0x00, 0x00, 0x03, b'h', b'i', 0]);
345    }
346
347    #[test]
348    fn keyholder_struct_member_order() {
349        // Beispiel-KeyHolder mit @key id:u32 + @key topic:string<8>
350        let mut h = PlainCdr2BeKeyHolder::new();
351        h.write_u32(42); // id (member_id 0)
352        h.write_string("foo"); // topic (member_id 1)
353        let bytes = h.into_bytes();
354        // u32 4 + length-be(4) + "foo\0" (4) = 12 byte
355        assert_eq!(bytes.len(), 12);
356        assert_eq!(&bytes[0..4], &[0, 0, 0, 42]);
357        assert_eq!(&bytes[4..8], &[0, 0, 0, 4]);
358        assert_eq!(&bytes[8..12], &[b'f', b'o', b'o', 0]);
359    }
360
361    #[test]
362    fn keyholder_full_keyhash_pipeline_zero_pad() {
363        // Topic mit @key u32 → max 4 byte → zero-pad
364        let mut h = PlainCdr2BeKeyHolder::new();
365        h.write_u32(0x1122_3344);
366        let h_bytes = h.into_bytes();
367        let key = compute_key_hash(&h_bytes, 4);
368        assert_eq!(&key[..4], &[0x11, 0x22, 0x33, 0x44]);
369        assert_eq!(&key[4..], &[0u8; 12]);
370    }
371
372    #[test]
373    fn keyholder_full_keyhash_pipeline_md5() {
374        // Topic mit @key string (unbounded) → MD5
375        let mut h = PlainCdr2BeKeyHolder::new();
376        h.write_string("hello-world-this-is-a-long-key-name-exceeding-16");
377        let h_bytes = h.into_bytes();
378        // max_size > 16 → MD5
379        let key = compute_key_hash(&h_bytes, usize::MAX);
380        // 16 byte deterministischer Hash, ungleich allen-null
381        assert_ne!(key, [0u8; 16]);
382    }
383
384    // ---- §7.4.5 / §7.6.8.3.1.b — KeyHash CDR2-BE Member-Sort by memberId ----
385
386    #[test]
387    fn keyhash_cdr2_be_member_order_independent() {
388        // Drei @key Member mit IDs 7, 3, 5. Encoder-Caller A liefert in
389        // [3, 5, 7]; Encoder-Caller B liefert in [7, 3, 5]. Spec-Sort
390        // erzwingt KeyHash-Identitaet.
391        let m_3 = (3u32, vec![0xAAu8, 0xBB, 0xCC, 0xDD]);
392        let m_5 = (5u32, vec![0x11u8, 0x22, 0x33, 0x44]);
393        let m_7 = (7u32, vec![0xDEu8, 0xAD, 0xBE, 0xEF]);
394
395        let order_a = vec![m_3.clone(), m_5.clone(), m_7.clone()];
396        let order_b = vec![m_7.clone(), m_3.clone(), m_5.clone()];
397        let order_c = vec![m_5.clone(), m_7.clone(), m_3.clone()];
398
399        let h_a = keyhash_cdr2_be(&order_a, 12);
400        let h_b = keyhash_cdr2_be(&order_b, 12);
401        let h_c = keyhash_cdr2_be(&order_c, 12);
402        assert_eq!(h_a, h_b);
403        assert_eq!(h_a, h_c);
404        // sortierte Bytes: 3.value || 5.value || 7.value, dann zero-pad bis 16.
405        let expected = {
406            let mut v = Vec::new();
407            v.extend_from_slice(&m_3.1);
408            v.extend_from_slice(&m_5.1);
409            v.extend_from_slice(&m_7.1);
410            v.resize(16, 0);
411            v
412        };
413        assert_eq!(h_a.as_slice(), expected.as_slice());
414    }
415
416    #[test]
417    fn keyhash_cdr2_be_md5_path_also_order_independent() {
418        // 4 Member mit Strings → max_size > 16 → MD5-Pfad.
419        let m_1 = (1u32, b"alpha-content-1".to_vec());
420        let m_2 = (2u32, b"beta-content-22".to_vec());
421        let m_3 = (3u32, b"gamma-content-333".to_vec());
422        let m_4 = (4u32, b"delta-content-4444".to_vec());
423
424        let order_a = vec![m_1.clone(), m_2.clone(), m_3.clone(), m_4.clone()];
425        let order_b = vec![m_4.clone(), m_3.clone(), m_2.clone(), m_1.clone()];
426        let h_a = keyhash_cdr2_be(&order_a, 1024);
427        let h_b = keyhash_cdr2_be(&order_b, 1024);
428        assert_eq!(h_a, h_b);
429        assert_ne!(h_a, [0u8; 16]);
430    }
431
432    #[test]
433    fn keyhash_cdr2_be_single_member_matches_compute_key_hash() {
434        let m = (42u32, vec![0xAB, 0xCD, 0xEF, 0x12]);
435        let h = keyhash_cdr2_be(&[m.clone()], 4);
436        let expected = compute_key_hash(&m.1, 4);
437        assert_eq!(h, expected);
438    }
439
440    #[test]
441    fn keyhash_cdr2_be_empty_member_set_yields_zero_padding() {
442        let h = keyhash_cdr2_be(&[], 4);
443        assert_eq!(h, [0u8; 16]);
444    }
445
446    #[test]
447    fn keyhash_cdr2_be_member_id_zero_sorts_first() {
448        // member_id=0 ist gueltig (Spec §7.2.2.4.1.4) und muss vor
449        // hoeheren IDs serialisiert werden.
450        let m_0 = (0u32, vec![0xFFu8, 0xEE, 0xDD, 0xCC]);
451        let m_99 = (99u32, vec![0x00u8, 0x11, 0x22, 0x33]);
452
453        let h_natural = keyhash_cdr2_be(&[m_0.clone(), m_99.clone()], 8);
454        let h_swapped = keyhash_cdr2_be(&[m_99.clone(), m_0.clone()], 8);
455        assert_eq!(h_natural, h_swapped);
456        // Sortiert: m_0 zuerst → Bytes [0xFF, 0xEE, 0xDD, 0xCC, 0x00, 0x11, 0x22, 0x33].
457        let mut expected = [0u8; 16];
458        expected[..4].copy_from_slice(&m_0.1);
459        expected[4..8].copy_from_slice(&m_99.1);
460        assert_eq!(h_natural, expected);
461    }
462
463    // ---- Mutation-Killer fuer alle PlainCdr2BeKeyHolder-Methoden ----
464    // Direkter Roundtrip jeder write_*-Methode + as_bytes/is_empty/len.
465
466    #[test]
467    fn keyholder_is_empty_initially_and_after_write_not_empty() {
468        let mut h = PlainCdr2BeKeyHolder::new();
469        assert!(h.is_empty());
470        assert_eq!(h.len(), 0);
471        h.write_u8(0x01);
472        assert!(!h.is_empty());
473        assert_eq!(h.len(), 1);
474    }
475
476    #[test]
477    fn keyholder_as_bytes_returns_written_content() {
478        let mut h = PlainCdr2BeKeyHolder::new();
479        h.write_u8(0xDE);
480        h.write_u8(0xAD);
481        h.write_u8(0xBE);
482        h.write_u8(0xEF);
483        assert_eq!(h.as_bytes(), &[0xDE, 0xAD, 0xBE, 0xEF]);
484        // as_bytes ist nicht-konsumierend
485        assert_eq!(h.as_bytes(), &[0xDE, 0xAD, 0xBE, 0xEF]);
486    }
487
488    #[test]
489    fn keyholder_write_i8_negative_two_complement() {
490        let mut h = PlainCdr2BeKeyHolder::new();
491        h.write_i8(-1);
492        h.write_i8(-128);
493        h.write_i8(127);
494        assert_eq!(h.into_bytes(), vec![0xFF, 0x80, 0x7F]);
495    }
496
497    #[test]
498    fn keyholder_write_u16_be_with_alignment() {
499        let mut h = PlainCdr2BeKeyHolder::new();
500        h.write_u8(0xAA); // 1 byte
501        h.write_u16(0x1234); // pad to 2: 1 pad-byte, then BE
502        assert_eq!(h.into_bytes(), vec![0xAA, 0x00, 0x12, 0x34]);
503    }
504
505    #[test]
506    fn keyholder_write_i16_be_with_alignment() {
507        let mut h = PlainCdr2BeKeyHolder::new();
508        h.write_u8(0xAA);
509        h.write_i16(-1);
510        // i16 -1 BE = 0xFFFF
511        assert_eq!(h.into_bytes(), vec![0xAA, 0x00, 0xFF, 0xFF]);
512    }
513
514    #[test]
515    fn keyholder_write_i32_be_with_alignment() {
516        let mut h = PlainCdr2BeKeyHolder::new();
517        h.write_u8(0xAA);
518        h.write_i32(-1);
519        // pad to 4 from 1 = 3 pad-bytes; i32 -1 BE = FFFFFFFF
520        assert_eq!(h.into_bytes(), vec![0xAA, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF]);
521    }
522
523    #[test]
524    fn keyholder_write_i64_be_with_4byte_alignment() {
525        // PLAIN_CDR2 alignment fuer 64-bit ist 4, nicht 8 (§7.4.2)
526        let mut h = PlainCdr2BeKeyHolder::new();
527        h.write_u8(0xAA);
528        h.write_i64(-1);
529        // pad to 4: 3 pad. i64 -1 BE = FFFFFFFFFFFFFFFF
530        let mut expected = vec![0xAA, 0, 0, 0];
531        expected.extend_from_slice(&[0xFF; 8]);
532        assert_eq!(h.into_bytes(), expected);
533    }
534
535    #[test]
536    fn keyholder_write_f32_be_known_pattern() {
537        let mut h = PlainCdr2BeKeyHolder::new();
538        h.write_f32(1.0_f32);
539        // 1.0_f32 BE = 0x3F800000
540        assert_eq!(h.into_bytes(), vec![0x3F, 0x80, 0x00, 0x00]);
541    }
542
543    #[test]
544    fn keyholder_write_f64_be_known_pattern() {
545        let mut h = PlainCdr2BeKeyHolder::new();
546        h.write_f64(1.0_f64);
547        // 1.0_f64 BE = 0x3FF0000000000000
548        assert_eq!(
549            h.into_bytes(),
550            vec![0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
551        );
552    }
553
554    #[test]
555    fn keyholder_write_bytes_appends_raw() {
556        let mut h = PlainCdr2BeKeyHolder::new();
557        h.write_u8(0x01);
558        h.write_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]);
559        h.write_bytes(&[]);
560        h.write_bytes(&[0xFF]);
561        assert_eq!(h.into_bytes(), vec![0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF]);
562    }
563}