Skip to main content

zerodds_rtps/
header_extension.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! HeaderExtension Submessage (DDSI-RTPS 2.5 §8.3.3.2 / §9.4.5.2 /
4//! §9.4.2.15).
5//!
6//! Die HeaderExtension (HE) ist eine in DDSI-RTPS 2.5 neu eingefuehrte
7//! Submessage-Klasse, die einer RTPS-Nachricht optionale Header-
8//! Felder anhaengt:
9//!
10//! - **`messageLength`**: u32 — Gesamtlaenge der RTPS-Message ab dem
11//!   ersten Submessage-Header. Erlaubt Reader-Pre-Allocation und ist
12//!   Pflicht-Hint fuer non-UDP-Transporte ohne implizite Datagram-
13//!   Grenze.
14//! - **`rtpsSendTimestamp`**: 8 Byte (Time_t) — Sender-Wallclock zum
15//!   Zeitpunkt des `send()`-Calls. Speist `clockSkewDetected` im
16//!   Receiver-State (§8.3.7.4).
17//! - **`uextension4`**: 4 Byte vendor-spezifisch.
18//! - **`wextension8`**: 8 Byte vendor-spezifisch (Spec §8.3.3.2).
19//! - **`messageChecksum`**: CRC-32C (4 Byte), CRC-64-XZ (8 Byte) oder
20//!   MD5-128 (16 Byte) ueber den Bereich `[RtpsHeader || alle
21//!   Submessages || HE-Submessage-Header || HE-Body bis vor dem
22//!   Checksum-Feld]`. C-Flag (2 Bit) selektiert die Variante.
23//! - **`parameters`**: ParameterList (TLV) — bisher nur fuer
24//!   Vendor-Erweiterungen reserviert; `must_understand` darin loest
25//!   ganzheitlichen Message-Reject aus (§9.4.2.11.2).
26//!
27//! # SubmessageId und Flag-Layout (Auftrags-Spezifikation)
28//!
29//! - `SubmessageId = 0x80` — HE-Marker. Ausserhalb der "klassischen"
30//!   Submessage-Range (≤ 0x7F) und damit forwards-kompatibel zu Pre-2.5-
31//!   Implementierungen, die `octets_to_next_header` zum Skippen nutzen.
32//!
33//! Flag-Bits im Submessage-Header:
34//!
35//! | Bit | Symbol | Bedeutung |
36//! |-----|--------|-----------|
37//! | 0 | E | Endianness (1 = LE) |
38//! | 1 | L | `messageLength` vorhanden |
39//! | 2 | W | `rtpsSendTimestamp` vorhanden |
40//! | 3 | U | `uextension4` vorhanden |
41//! | 4 | V | `wextension8` vorhanden |
42//! | 5+6 | C | Checksum-Variante (0=keine, 1=CRC-32C, 2=CRC-64, 3=MD5) |
43//! | 7 | P | `parameters` (ParameterList) vorhanden |
44//!
45//! # Receiver-State-Update (§8.3.7.4)
46//!
47//! Bei Empfang einer HE aktualisiert der Receiver:
48//!
49//! - `Receiver.messageLength` aus dem L-Feld (falls vorhanden) →
50//!   Cross-Check gegen tatsaechliche Datagram-Restlaenge. Mismatch =
51//!   `WireError::ValueOutOfRange`.
52//! - `Receiver.haveTimestamp = true`, `Receiver.timestamp = …` aus dem
53//!   T-Feld (falls vorhanden).
54//! - `Receiver.messageChecksum = …` aus dem C-Feld (falls Variante ≠ 0).
55//! - `Receiver.parameters = …` aus dem P-Feld (falls vorhanden).
56//! - `clockSkewDetected` Heuristik: wenn `|Receiver.timestamp - now|` >
57//!   Grenzwert. (Diese Heuristik verbleibt im Receiver-Code; das
58//!   Decode-Modul liefert nur die Eingabe.)
59//!
60//! # DoS-Cap
61//!
62//! `MAX_HE_LENGTH = 16 KiB`: HE-Body darf nicht laenger als 16 KiB
63//! sein (deckt typische ParameterList + alle Optional-Felder).
64//! Verhindert Amplification ueber boesartig grosse `parameters`.
65
66extern crate alloc;
67use alloc::vec::Vec;
68
69use crate::error::WireError;
70use crate::parameter_list::ParameterList;
71
72/// SubmessageId fuer HeaderExtension (DDSI-RTPS 2.5).
73pub const SUBMESSAGE_ID_HEADER_EXTENSION: u8 = 0x80;
74
75/// Hard-Cap fuer HE-Body-Laenge (DoS-Schutz).
76pub const MAX_HE_LENGTH: usize = 16 * 1024;
77
78// ----- Flag-Bits -----
79
80/// E-Flag (Bit 0) — Endianness des Bodies (1 = LE).
81pub const HE_FLAG_E: u8 = 1 << 0;
82/// L-Flag (Bit 1) — `messageLength` vorhanden.
83pub const HE_FLAG_L: u8 = 1 << 1;
84/// W-Flag (Bit 2) — `rtpsSendTimestamp` vorhanden.
85pub const HE_FLAG_W: u8 = 1 << 2;
86/// U-Flag (Bit 3) — `uextension4` vorhanden.
87pub const HE_FLAG_U: u8 = 1 << 3;
88/// V-Flag (Bit 4) — `wextension8` vorhanden.
89pub const HE_FLAG_V: u8 = 1 << 4;
90/// Lower-Bit der C-Maske (Bit 5).
91pub const HE_FLAG_C0: u8 = 1 << 5;
92/// Higher-Bit der C-Maske (Bit 6).
93pub const HE_FLAG_C1: u8 = 1 << 6;
94/// Maske der beiden Checksum-Selector-Bits (Bits 5+6).
95pub const HE_FLAG_C_MASK: u8 = HE_FLAG_C0 | HE_FLAG_C1;
96/// P-Flag (Bit 7) — `parameters` (ParameterList) vorhanden.
97pub const HE_FLAG_P: u8 = 1 << 7;
98
99// =====================================================================
100// Wire-Typen
101// =====================================================================
102
103/// Spec §9.4.2.15.2 — Auswahl der `messageChecksum`-Variante. Wert
104/// liegt in den C-Bits (5+6) des Flag-Bytes.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum ChecksumKind {
107    /// `C = 00` — keine Checksum.
108    None,
109    /// `C = 01` — CRC-32C (RFC 4960 App. B), 4 Byte.
110    Crc32c,
111    /// `C = 10` — CRC-64-XZ (ECMA-182, XZ utils), 8 Byte.
112    Crc64,
113    /// `C = 11` — MD5-128 (RFC 1321), 16 Byte.
114    Md5,
115}
116
117impl ChecksumKind {
118    /// Wire-Size in Bytes (0/4/8/16).
119    #[must_use]
120    pub fn wire_size(self) -> usize {
121        match self {
122            Self::None => 0,
123            Self::Crc32c => 4,
124            Self::Crc64 => 8,
125            Self::Md5 => 16,
126        }
127    }
128
129    /// Aus den C-Bits eines Flag-Bytes extrahieren.
130    #[must_use]
131    pub fn from_flags(flags: u8) -> Self {
132        // Maskierung garantiert Werte 0..=3.
133        match (flags & HE_FLAG_C_MASK) >> 5 {
134            1 => Self::Crc32c,
135            2 => Self::Crc64,
136            3 => Self::Md5,
137            // 0 oder unmoegliche oberen Bits: keine Checksum.
138            _ => Self::None,
139        }
140    }
141
142    /// In die C-Bits eines Flag-Bytes encodieren.
143    #[must_use]
144    pub fn to_flag_bits(self) -> u8 {
145        let raw = match self {
146            Self::None => 0u8,
147            Self::Crc32c => 1,
148            Self::Crc64 => 2,
149            Self::Md5 => 3,
150        };
151        (raw << 5) & HE_FLAG_C_MASK
152    }
153}
154
155/// Inhalt der `messageChecksum` (variabel nach [`ChecksumKind`]).
156#[derive(Debug, Clone, PartialEq, Eq, Default)]
157pub enum ChecksumValue {
158    /// Kein Checksum-Feld geschrieben.
159    #[default]
160    None,
161    /// CRC-32C u32.
162    Crc32c(u32),
163    /// CRC-64-XZ u64.
164    Crc64(u64),
165    /// MD5-128 16-Byte-Hash.
166    Md5([u8; 16]),
167}
168
169impl ChecksumValue {
170    /// Zugehoeriger [`ChecksumKind`].
171    #[must_use]
172    pub fn kind(&self) -> ChecksumKind {
173        match self {
174            Self::None => ChecksumKind::None,
175            Self::Crc32c(_) => ChecksumKind::Crc32c,
176            Self::Crc64(_) => ChecksumKind::Crc64,
177            Self::Md5(_) => ChecksumKind::Md5,
178        }
179    }
180
181    /// Berechnet die `messageChecksum` ueber `payload` mit dem
182    /// gewuenschten Algorithmus (DDSI-RTPS 2.5 §9.4.2.15.2).
183    ///
184    /// `payload` ist der Bereich, ueber den die Checksum berechnet
185    /// wird. Spec-konform sind das die Submessages **nach** der
186    /// HeaderExtension-Submessage; der Caller stellt das durch
187    /// passenden Slice-Cut sicher.
188    ///
189    /// Implementiert via [`zerodds_foundation::crc32c`],
190    /// [`zerodds_foundation::crc64_xz`] und [`zerodds_foundation::md5`] —
191    /// pure-Rust no_std-Hashes ohne externe Crypto-Crate-
192    /// Abhaengigkeit (Pillar 9 Zero-Dependency).
193    #[must_use]
194    pub fn compute(kind: ChecksumKind, payload: &[u8]) -> Self {
195        match kind {
196            ChecksumKind::None => Self::None,
197            ChecksumKind::Crc32c => Self::Crc32c(zerodds_foundation::crc32c(payload)),
198            ChecksumKind::Crc64 => Self::Crc64(zerodds_foundation::crc64_xz(payload)),
199            ChecksumKind::Md5 => Self::Md5(zerodds_foundation::md5(payload)),
200        }
201    }
202
203    /// Verifiziert dass die in diesem Wert gehaltene Checksum mit der
204    /// ueber `payload` berechneten uebereinstimmt.
205    ///
206    /// Liefert `true` wenn der Algorithmus aktiv ist UND der Wert
207    /// passt; `true` auch wenn `kind() == None` (keine Checksum
208    /// deklariert, nichts zu verifizieren); `false` bei Mismatch.
209    ///
210    /// Spec §9.4.2.15.2: Receiver verwirft die Message bei Mismatch.
211    #[must_use]
212    pub fn verify(&self, payload: &[u8]) -> bool {
213        let computed = Self::compute(self.kind(), payload);
214        computed == *self
215    }
216}
217
218/// 8-Byte Sender-Timestamp (Time_t = i32 sec + u32 nanosec, Big-Endian
219/// im RTPS-Wire-Sinn — wird im HE-Body in Submessage-Endianness
220/// geschrieben).
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub struct HeTimestamp {
223    /// Sekunden seit Unix-Epoch.
224    pub seconds: i32,
225    /// Nanosekunden im laufenden Sekunden-Tick.
226    pub fraction: u32,
227}
228
229/// Geparste HeaderExtension-Submessage.
230#[derive(Debug, Clone, PartialEq, Eq, Default)]
231pub struct HeaderExtension {
232    /// Endianness des Bodies (`true` = Little-Endian).
233    pub little_endian: bool,
234    /// Falls L-Flag gesetzt: Restlaenge der Message.
235    pub message_length: Option<u32>,
236    /// Falls W-Flag gesetzt: Sender-Timestamp.
237    pub timestamp: Option<HeTimestamp>,
238    /// Falls U-Flag gesetzt: 4-Byte vendor-spezifischer Wert.
239    pub uextension4: Option<[u8; 4]>,
240    /// Falls V-Flag gesetzt: 16-Byte vendor-spezifischer Wert.
241    pub wextension8: Option<[u8; 8]>,
242    /// Falls C-Flag != 0: Checksum-Wert.
243    pub checksum: ChecksumValue,
244    /// Falls P-Flag gesetzt: ParameterList.
245    pub parameters: Option<ParameterList>,
246}
247
248impl HeaderExtension {
249    /// Leerer HE (alle optionalen Felder absent). Ueber den Builder-
250    /// Stil der Felder direkt manipulieren.
251    #[must_use]
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// Berechnet das Flag-Byte (inkl. E-Bit) aus den gesetzten Feldern.
257    #[must_use]
258    pub fn flag_byte(&self) -> u8 {
259        let mut f = 0u8;
260        if self.little_endian {
261            f |= HE_FLAG_E;
262        }
263        if self.message_length.is_some() {
264            f |= HE_FLAG_L;
265        }
266        if self.timestamp.is_some() {
267            f |= HE_FLAG_W;
268        }
269        if self.uextension4.is_some() {
270            f |= HE_FLAG_U;
271        }
272        if self.wextension8.is_some() {
273            f |= HE_FLAG_V;
274        }
275        f |= self.checksum.kind().to_flag_bits();
276        if self.parameters.is_some() {
277            f |= HE_FLAG_P;
278        }
279        f
280    }
281
282    /// Encoded den Body **ohne** Submessage-Header. Body-Laenge ist
283    /// per Konstruktion ≤ [`MAX_HE_LENGTH`].
284    ///
285    /// Reihenfolge: L → W → U → V → C → P, gem. Spec §9.4.5.2.
286    ///
287    /// # Errors
288    /// `ValueOutOfRange` wenn der Body > `u16::MAX` oder
289    /// > `MAX_HE_LENGTH` waere.
290    pub fn encode_body(&self) -> Result<Vec<u8>, WireError> {
291        let le = self.little_endian;
292        let mut body = Vec::with_capacity(64);
293
294        if let Some(len) = self.message_length {
295            write_u32(&mut body, len, le);
296        }
297        if let Some(ts) = self.timestamp {
298            write_i32(&mut body, ts.seconds, le);
299            write_u32(&mut body, ts.fraction, le);
300        }
301        if let Some(u) = self.uextension4 {
302            body.extend_from_slice(&u);
303        }
304        if let Some(v) = self.wextension8 {
305            body.extend_from_slice(&v);
306        }
307        match self.checksum {
308            ChecksumValue::None => {}
309            ChecksumValue::Crc32c(c) => write_u32(&mut body, c, le),
310            ChecksumValue::Crc64(c) => write_u64(&mut body, c, le),
311            ChecksumValue::Md5(m) => body.extend_from_slice(&m),
312        }
313        if let Some(pl) = &self.parameters {
314            // ParameterList in Submessage-Endianness anfuegen.
315            body.extend_from_slice(&pl.to_bytes(le));
316        }
317        if body.len() > MAX_HE_LENGTH {
318            return Err(WireError::ValueOutOfRange {
319                message: "HeaderExtension body exceeds MAX_HE_LENGTH",
320            });
321        }
322        Ok(body)
323    }
324
325    /// Encoded HE als komplettes Submessage = 4-Byte SubmessageHeader
326    /// + Body. `octets_to_next_header` wird auf Body-Laenge gesetzt.
327    ///
328    /// # Errors
329    /// `ValueOutOfRange` wenn Body-Laenge > `u16::MAX`.
330    pub fn encode(&self) -> Result<Vec<u8>, WireError> {
331        let body = self.encode_body()?;
332        let body_len = u16::try_from(body.len()).map_err(|_| WireError::ValueOutOfRange {
333            message: "HE body exceeds u16::MAX",
334        })?;
335        let mut out = Vec::with_capacity(4 + body.len());
336        out.push(SUBMESSAGE_ID_HEADER_EXTENSION);
337        out.push(self.flag_byte());
338        let len_bytes = if self.little_endian {
339            body_len.to_le_bytes()
340        } else {
341            body_len.to_be_bytes()
342        };
343        out.extend_from_slice(&len_bytes);
344        out.extend_from_slice(&body);
345        Ok(out)
346    }
347
348    /// Decoded den HE-Body aus einem Slice der Laenge
349    /// `octets_to_next_header`, gegeben das Flag-Byte aus dem
350    /// Submessage-Header.
351    ///
352    /// # Errors
353    /// `UnexpectedEof`, `ValueOutOfRange` (Body groesser
354    /// `MAX_HE_LENGTH`).
355    pub fn decode_body(body: &[u8], flags: u8) -> Result<Self, WireError> {
356        if body.len() > MAX_HE_LENGTH {
357            return Err(WireError::ValueOutOfRange {
358                message: "HE body exceeds MAX_HE_LENGTH",
359            });
360        }
361        let le = (flags & HE_FLAG_E) != 0;
362        let mut pos = 0usize;
363        let need = |n: usize, p: usize| -> Result<(), WireError> {
364            if body.len() < p + n {
365                Err(WireError::UnexpectedEof {
366                    needed: n,
367                    offset: p,
368                })
369            } else {
370                Ok(())
371            }
372        };
373
374        let message_length = if (flags & HE_FLAG_L) != 0 {
375            need(4, pos)?;
376            let v = read_u32(&body[pos..pos + 4], le);
377            pos += 4;
378            Some(v)
379        } else {
380            None
381        };
382        let timestamp = if (flags & HE_FLAG_W) != 0 {
383            need(8, pos)?;
384            let s = read_i32(&body[pos..pos + 4], le);
385            let f = read_u32(&body[pos + 4..pos + 8], le);
386            pos += 8;
387            Some(HeTimestamp {
388                seconds: s,
389                fraction: f,
390            })
391        } else {
392            None
393        };
394        let uextension4 = if (flags & HE_FLAG_U) != 0 {
395            need(4, pos)?;
396            let mut u = [0u8; 4];
397            u.copy_from_slice(&body[pos..pos + 4]);
398            pos += 4;
399            Some(u)
400        } else {
401            None
402        };
403        let wextension8 = if (flags & HE_FLAG_V) != 0 {
404            need(8, pos)?;
405            let mut v = [0u8; 8];
406            v.copy_from_slice(&body[pos..pos + 8]);
407            pos += 8;
408            Some(v)
409        } else {
410            None
411        };
412
413        let kind = ChecksumKind::from_flags(flags);
414        let checksum = match kind {
415            ChecksumKind::None => ChecksumValue::None,
416            ChecksumKind::Crc32c => {
417                need(4, pos)?;
418                let v = read_u32(&body[pos..pos + 4], le);
419                pos += 4;
420                ChecksumValue::Crc32c(v)
421            }
422            ChecksumKind::Crc64 => {
423                need(8, pos)?;
424                let v = read_u64(&body[pos..pos + 8], le);
425                pos += 8;
426                ChecksumValue::Crc64(v)
427            }
428            ChecksumKind::Md5 => {
429                need(16, pos)?;
430                let mut m = [0u8; 16];
431                m.copy_from_slice(&body[pos..pos + 16]);
432                pos += 16;
433                ChecksumValue::Md5(m)
434            }
435        };
436
437        let parameters = if (flags & HE_FLAG_P) != 0 {
438            // ParameterList belegt den verbleibenden Body.
439            let pl = ParameterList::from_bytes(&body[pos..], le)?;
440            // pos auf Ende setzen — die ParameterList endet am Sentinel
441            // und kann von trailing-Bytes gefolgt sein, die wir hier als
442            // Fehler-Indikator werten (Spec verbietet trailing-Garbage
443            // im HE-Body).
444            //
445            // Konkret: Encoder schreibt am Ende den Sentinel + 0; ein
446            // sauberer Body endet damit. Wenn `from_bytes` schon
447            // erfolgreich zurueckkehrt, hat es alles bis zum Sentinel
448            // gelesen — wir validieren keine Trailing-Bytes (RFC sagt
449            // ParameterList nimmt den Rest; defensives Verwerfen wuerde
450            // legale Vendor-Padding brechen).
451            Some(pl)
452        } else {
453            // Kein P-Flag: trailing bytes sind unerwartet.
454            if pos != body.len() {
455                return Err(WireError::ValueOutOfRange {
456                    message: "HE trailing bytes without P-flag",
457                });
458            }
459            None
460        };
461
462        Ok(Self {
463            little_endian: le,
464            message_length,
465            timestamp,
466            uextension4,
467            wextension8,
468            checksum,
469            parameters,
470        })
471    }
472
473    /// Decoded eine komplette HE-Submessage (Header + Body) aus
474    /// `bytes`. Ueberprueft die SubmessageId.
475    ///
476    /// # Errors
477    /// `UnexpectedEof`, `UnknownSubmessageId`, `ValueOutOfRange`.
478    pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
479        if bytes.len() < 4 {
480            return Err(WireError::UnexpectedEof {
481                needed: 4,
482                offset: 0,
483            });
484        }
485        if bytes[0] != SUBMESSAGE_ID_HEADER_EXTENSION {
486            return Err(WireError::UnknownSubmessageId { id: bytes[0] });
487        }
488        let flags = bytes[1];
489        let le = (flags & HE_FLAG_E) != 0;
490        let mut len_bytes = [0u8; 2];
491        len_bytes.copy_from_slice(&bytes[2..4]);
492        let octets = if le {
493            u16::from_le_bytes(len_bytes)
494        } else {
495            u16::from_be_bytes(len_bytes)
496        } as usize;
497        if bytes.len() < 4 + octets {
498            return Err(WireError::UnexpectedEof {
499                needed: octets,
500                offset: 4,
501            });
502        }
503        Self::decode_body(&bytes[4..4 + octets], flags)
504    }
505
506    /// Cross-Check: wenn `message_length` gesetzt ist, MUSS sie der
507    /// tatsaechlichen Restlaenge der RTPS-Message ab dem ersten
508    /// Submessage-Header (also ab Byte 20 = nach RtpsHeader) bis zum
509    /// Datagram-Ende entsprechen (Spec §8.3.7.4).
510    ///
511    /// `actual_message_length` ist die vom Receiver gemessene
512    /// Restlaenge.
513    ///
514    /// # Errors
515    /// `ValueOutOfRange` bei Mismatch.
516    pub fn validate_message_length(&self, actual_message_length: u32) -> Result<(), WireError> {
517        if let Some(declared) = self.message_length {
518            if declared != actual_message_length {
519                return Err(WireError::ValueOutOfRange {
520                    message: "HeaderExtension messageLength mismatch",
521                });
522            }
523        }
524        Ok(())
525    }
526}
527
528// =====================================================================
529// PID-Helpers — Must-Understand-Bit (Spec §9.4.2.11.2)
530// =====================================================================
531
532/// Bit-Maske: gesetzt -> Reader MUSS diesen PID verstehen, sonst die
533/// gesamte Message (HE-Carrier) verwerfen.
534pub const PID_MUST_UNDERSTAND: u16 = 0x4000;
535
536/// Bit-Maske: gesetzt -> Vendor-spezifischer PID (Spec-Reservation 2.5
537/// Tab 9.13). Reader die diesen PID nicht kennen, duerfen ihn skippen
538/// **wenn** das `must_understand`-Bit nicht gesetzt ist.
539pub const PID_VENDOR_SPECIFIC: u16 = 0x8000;
540
541/// Liefert `true`, wenn der gegebene PID das Must-Understand-Bit
542/// gesetzt hat.
543#[must_use]
544pub fn pid_must_understand(pid: u16) -> bool {
545    (pid & PID_MUST_UNDERSTAND) != 0
546}
547
548/// Roher PID ohne Must-Understand- und Vendor-Bits.
549#[must_use]
550pub fn pid_strip(pid: u16) -> u16 {
551    pid & !(PID_MUST_UNDERSTAND | PID_VENDOR_SPECIFIC)
552}
553
554// =====================================================================
555// Bit-Helpers (private)
556// =====================================================================
557
558fn write_u32(out: &mut Vec<u8>, v: u32, le: bool) {
559    let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
560    out.extend_from_slice(&bytes);
561}
562
563fn write_i32(out: &mut Vec<u8>, v: i32, le: bool) {
564    let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
565    out.extend_from_slice(&bytes);
566}
567
568fn write_u64(out: &mut Vec<u8>, v: u64, le: bool) {
569    let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
570    out.extend_from_slice(&bytes);
571}
572
573fn read_u32(bytes: &[u8], le: bool) -> u32 {
574    let mut buf = [0u8; 4];
575    buf.copy_from_slice(&bytes[..4]);
576    if le {
577        u32::from_le_bytes(buf)
578    } else {
579        u32::from_be_bytes(buf)
580    }
581}
582
583fn read_i32(bytes: &[u8], le: bool) -> i32 {
584    let mut buf = [0u8; 4];
585    buf.copy_from_slice(&bytes[..4]);
586    if le {
587        i32::from_le_bytes(buf)
588    } else {
589        i32::from_be_bytes(buf)
590    }
591}
592
593fn read_u64(bytes: &[u8], le: bool) -> u64 {
594    let mut buf = [0u8; 8];
595    buf.copy_from_slice(&bytes[..8]);
596    if le {
597        u64::from_le_bytes(buf)
598    } else {
599        u64::from_be_bytes(buf)
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    #![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
606    use super::*;
607    use crate::parameter_list::Parameter;
608    use alloc::vec;
609
610    fn roundtrip(he: HeaderExtension) {
611        let bytes = he.encode().unwrap();
612        let decoded = HeaderExtension::decode(&bytes).unwrap();
613        assert_eq!(decoded, he);
614    }
615
616    #[test]
617    fn empty_he_le_roundtrip() {
618        let he = HeaderExtension {
619            little_endian: true,
620            ..HeaderExtension::default()
621        };
622        roundtrip(he);
623    }
624
625    #[test]
626    fn empty_he_be_roundtrip() {
627        let he = HeaderExtension::default();
628        // little_endian = false
629        roundtrip(he);
630    }
631
632    #[test]
633    fn flag_byte_empty_le() {
634        let he = HeaderExtension {
635            little_endian: true,
636            ..HeaderExtension::default()
637        };
638        assert_eq!(he.flag_byte(), HE_FLAG_E);
639    }
640
641    #[test]
642    fn flag_byte_with_message_length() {
643        let mut he = HeaderExtension {
644            little_endian: true,
645            message_length: Some(4242),
646            ..HeaderExtension::default()
647        };
648        assert_eq!(he.flag_byte(), HE_FLAG_E | HE_FLAG_L);
649        he.message_length = None;
650        assert_eq!(he.flag_byte(), HE_FLAG_E);
651    }
652
653    #[test]
654    fn flag_byte_all_optional_set_le() {
655        let he = HeaderExtension {
656            little_endian: true,
657            message_length: Some(0x0102_0304),
658            timestamp: Some(HeTimestamp {
659                seconds: 7,
660                fraction: 8,
661            }),
662            uextension4: Some([0xAA, 0xBB, 0xCC, 0xDD]),
663            wextension8: Some([0x11; 8]),
664            checksum: ChecksumValue::Md5([0x22; 16]),
665            parameters: Some(ParameterList::new()),
666        };
667        let f = he.flag_byte();
668        assert_eq!(
669            f,
670            HE_FLAG_E
671                | HE_FLAG_L
672                | HE_FLAG_W
673                | HE_FLAG_U
674                | HE_FLAG_V
675                | HE_FLAG_P
676                | ChecksumKind::Md5.to_flag_bits()
677        );
678    }
679
680    #[test]
681    fn checksum_kind_roundtrip() {
682        for kind in [
683            ChecksumKind::None,
684            ChecksumKind::Crc32c,
685            ChecksumKind::Crc64,
686            ChecksumKind::Md5,
687        ] {
688            let bits = kind.to_flag_bits();
689            assert_eq!(ChecksumKind::from_flags(HE_FLAG_E | bits), kind);
690        }
691    }
692
693    #[test]
694    fn checksum_kind_wire_size() {
695        assert_eq!(ChecksumKind::None.wire_size(), 0);
696        assert_eq!(ChecksumKind::Crc32c.wire_size(), 4);
697        assert_eq!(ChecksumKind::Crc64.wire_size(), 8);
698        assert_eq!(ChecksumKind::Md5.wire_size(), 16);
699    }
700
701    #[test]
702    fn roundtrip_with_message_length_only_le() {
703        let he = HeaderExtension {
704            little_endian: true,
705            message_length: Some(0xDEAD_BEEF),
706            ..HeaderExtension::default()
707        };
708        roundtrip(he);
709    }
710
711    #[test]
712    fn roundtrip_with_timestamp_le() {
713        let he = HeaderExtension {
714            little_endian: true,
715            timestamp: Some(HeTimestamp {
716                seconds: 1_710_000_000,
717                fraction: 500_000_000,
718            }),
719            ..HeaderExtension::default()
720        };
721        roundtrip(he);
722    }
723
724    #[test]
725    fn roundtrip_with_uextension4_be() {
726        let he = HeaderExtension {
727            little_endian: false,
728            uextension4: Some([1, 2, 3, 4]),
729            ..HeaderExtension::default()
730        };
731        roundtrip(he);
732    }
733
734    #[test]
735    fn roundtrip_with_wextension8_le() {
736        let he = HeaderExtension {
737            little_endian: true,
738            wextension8: Some([0xA5; 8]),
739            ..HeaderExtension::default()
740        };
741        roundtrip(he);
742    }
743
744    #[test]
745    fn roundtrip_with_crc32c_le() {
746        let he = HeaderExtension {
747            little_endian: true,
748            checksum: ChecksumValue::Crc32c(0xCAFE_BABE),
749            ..HeaderExtension::default()
750        };
751        roundtrip(he);
752    }
753
754    #[test]
755    fn roundtrip_with_crc64_be() {
756        let he = HeaderExtension {
757            little_endian: false,
758            checksum: ChecksumValue::Crc64(0x0123_4567_89AB_CDEF),
759            ..HeaderExtension::default()
760        };
761        roundtrip(he);
762    }
763
764    #[test]
765    fn roundtrip_with_md5_le() {
766        let he = HeaderExtension {
767            little_endian: true,
768            checksum: ChecksumValue::Md5([
769                0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
770                0x42, 0x7e,
771            ]),
772            ..HeaderExtension::default()
773        };
774        roundtrip(he);
775    }
776
777    #[test]
778    fn roundtrip_with_parameter_list() {
779        let mut pl = ParameterList::new();
780        pl.push(Parameter::new(0x0015, vec![2, 5, 0, 0]));
781        let he = HeaderExtension {
782            little_endian: true,
783            parameters: Some(pl),
784            ..HeaderExtension::default()
785        };
786        roundtrip(he);
787    }
788
789    #[test]
790    fn decode_rejects_malformed_parameter_list_when_p_flag_set() {
791        // Spec §8.3.7.3: Bei P-Flag muss der Rest des Bodies eine
792        // valide ParameterList sein. Ein Body, der einen halben
793        // Parameter-Header enthaelt (4 Byte Header + 4 Byte fehlt),
794        // muss mit Error abgelehnt werden.
795        let mut pl_bytes = Vec::<u8>::new();
796        // PID=0x0002, length=0x0010 (16 byte angekuendigt), aber nur 2
797        // Byte body geliefert. (PID=0x0001 ist Sentinel — vermeide.)
798        pl_bytes.extend_from_slice(&0x0002u16.to_le_bytes());
799        pl_bytes.extend_from_slice(&0x0010u16.to_le_bytes());
800        pl_bytes.extend_from_slice(&[0xAA, 0xBB]); // truncated body
801        // Body = E-flag in flags + P-flag → kein L/W/U/V/C, danach PL.
802        let flags = HE_FLAG_E | HE_FLAG_P;
803        let r = HeaderExtension::decode_body(&pl_bytes, flags);
804        assert!(r.is_err(), "malformed PL must be rejected");
805    }
806
807    #[test]
808    fn decode_rejects_truncated_uextension4_body() {
809        // Spec §8.3.7.3 Length-Mismatch: U-Flag angekuendigt, aber
810        // Body enthaelt nur 2 Byte statt 4.
811        let body: [u8; 2] = [0xAA, 0xBB];
812        let flags = HE_FLAG_E | HE_FLAG_U;
813        let r = HeaderExtension::decode_body(&body, flags);
814        assert!(matches!(r, Err(WireError::UnexpectedEof { .. })));
815    }
816
817    #[test]
818    fn decode_rejects_truncated_wextension8_body() {
819        // Spec §8.3.7.3: V-Flag angekuendigt, Body kuerzer als 8 Byte.
820        let body: [u8; 4] = [0; 4];
821        let flags = HE_FLAG_E | HE_FLAG_V;
822        let r = HeaderExtension::decode_body(&body, flags);
823        assert!(matches!(r, Err(WireError::UnexpectedEof { .. })));
824    }
825
826    #[test]
827    fn decode_rejects_truncated_md5_checksum_body() {
828        // C-Flag = MD5 (=3), Body < 16 Byte → reject.
829        let body: [u8; 4] = [0; 4];
830        let flags = HE_FLAG_E | HE_FLAG_C0 | HE_FLAG_C1;
831        let r = HeaderExtension::decode_body(&body, flags);
832        assert!(matches!(r, Err(WireError::UnexpectedEof { .. })));
833    }
834
835    #[test]
836    fn flag_byte_includes_all_seven_logical_flags() {
837        // Spec §8.3.3.2: 7 Flags E/L/W/U/V/C/P.
838        let mut pl = ParameterList::new();
839        pl.push(Parameter::new(0x0001, vec![1, 2, 3, 4]));
840        let he = HeaderExtension {
841            little_endian: true,
842            message_length: Some(99),
843            timestamp: Some(HeTimestamp::default()),
844            uextension4: Some([1; 4]),
845            wextension8: Some([2; 8]),
846            checksum: ChecksumValue::Crc64(0xABCD),
847            parameters: Some(pl),
848        };
849        let f = he.flag_byte();
850        assert!(f & HE_FLAG_E != 0, "E");
851        assert!(f & HE_FLAG_L != 0, "L");
852        assert!(f & HE_FLAG_W != 0, "W");
853        assert!(f & HE_FLAG_U != 0, "U");
854        assert!(f & HE_FLAG_V != 0, "V");
855        assert!(f & HE_FLAG_C_MASK != 0, "C");
856        assert!(f & HE_FLAG_P != 0, "P");
857    }
858
859    #[test]
860    fn roundtrip_all_fields_set_le() {
861        let mut pl = ParameterList::new();
862        pl.push(Parameter::new(0x0015, vec![2, 5, 0, 0]));
863        let he = HeaderExtension {
864            little_endian: true,
865            message_length: Some(1234),
866            timestamp: Some(HeTimestamp {
867                seconds: 1,
868                fraction: 2,
869            }),
870            uextension4: Some([0xDE, 0xAD, 0xBE, 0xEF]),
871            wextension8: Some([0xC0; 8]),
872            checksum: ChecksumValue::Crc64(0x1122_3344_5566_7788),
873            parameters: Some(pl),
874        };
875        roundtrip(he);
876    }
877
878    #[test]
879    fn roundtrip_all_fields_set_be() {
880        let mut pl = ParameterList::new();
881        pl.push(Parameter::new(0x0050, vec![0xAA; 16]));
882        let he = HeaderExtension {
883            little_endian: false,
884            message_length: Some(99),
885            timestamp: Some(HeTimestamp {
886                seconds: -3,
887                fraction: 9,
888            }),
889            uextension4: Some([0; 4]),
890            wextension8: Some([0xFF; 8]),
891            checksum: ChecksumValue::Md5([0x77; 16]),
892            parameters: Some(pl),
893        };
894        roundtrip(he);
895    }
896
897    #[test]
898    fn decode_rejects_wrong_submessage_id() {
899        // Falscher SubmessageId (0x15 = DATA).
900        let bytes = [0x15u8, HE_FLAG_E, 0, 0];
901        let res = HeaderExtension::decode(&bytes);
902        assert!(matches!(
903            res,
904            Err(WireError::UnknownSubmessageId { id: 0x15 })
905        ));
906    }
907
908    #[test]
909    fn decode_rejects_truncated_header() {
910        let bytes = [0x80u8, HE_FLAG_E];
911        let res = HeaderExtension::decode(&bytes);
912        assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
913    }
914
915    #[test]
916    fn decode_rejects_truncated_body_before_message_length() {
917        // L-Flag aber nur 2 Byte Body.
918        let bytes = [
919            SUBMESSAGE_ID_HEADER_EXTENSION,
920            HE_FLAG_E | HE_FLAG_L,
921            2,
922            0,
923            0xAA,
924            0xBB,
925        ];
926        let res = HeaderExtension::decode(&bytes);
927        assert!(matches!(res, Err(WireError::UnexpectedEof { .. })));
928    }
929
930    #[test]
931    fn decode_rejects_oversized_body() {
932        // Konstruieren eines Headers mit grossem Body — wir koennen
933        // nur bis u16::MAX adressieren, aber wir koennen
934        // `decode_body` direkt mit > MAX_HE_LENGTH Body fuettern.
935        let big = vec![0u8; MAX_HE_LENGTH + 1];
936        let res = HeaderExtension::decode_body(&big, HE_FLAG_E);
937        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
938    }
939
940    #[test]
941    fn decode_rejects_trailing_bytes_without_p_flag() {
942        // Body hat 4 Byte trailing aber kein P-Flag.
943        let body = vec![0xAA, 0xBB, 0xCC, 0xDD];
944        let res = HeaderExtension::decode_body(&body, HE_FLAG_E);
945        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
946    }
947
948    #[test]
949    fn validate_message_length_matches() {
950        let he = HeaderExtension {
951            little_endian: true,
952            message_length: Some(42),
953            ..HeaderExtension::default()
954        };
955        assert!(he.validate_message_length(42).is_ok());
956    }
957
958    #[test]
959    fn validate_message_length_mismatch() {
960        let he = HeaderExtension {
961            little_endian: true,
962            message_length: Some(42),
963            ..HeaderExtension::default()
964        };
965        let res = he.validate_message_length(43);
966        assert!(matches!(res, Err(WireError::ValueOutOfRange { .. })));
967    }
968
969    #[test]
970    fn validate_message_length_no_l_flag_succeeds() {
971        let he = HeaderExtension::default();
972        assert!(he.validate_message_length(123).is_ok());
973    }
974
975    #[test]
976    fn checksum_value_kind_matches() {
977        assert_eq!(ChecksumValue::None.kind(), ChecksumKind::None);
978        assert_eq!(ChecksumValue::Crc32c(1).kind(), ChecksumKind::Crc32c);
979        assert_eq!(ChecksumValue::Crc64(1).kind(), ChecksumKind::Crc64);
980        assert_eq!(ChecksumValue::Md5([0; 16]).kind(), ChecksumKind::Md5);
981    }
982
983    #[test]
984    fn pid_must_understand_helpers() {
985        assert!(pid_must_understand(0x4000));
986        assert!(pid_must_understand(0x4015));
987        assert!(!pid_must_understand(0x0015));
988        assert_eq!(pid_strip(0xC015), 0x0015);
989        assert_eq!(pid_strip(0x4015), 0x0015);
990        assert_eq!(pid_strip(0x8015), 0x0015);
991        assert_eq!(pid_strip(0x0015), 0x0015);
992    }
993
994    #[test]
995    fn wire_layout_le_message_length_correct() {
996        // L-Flag, 4 Byte Body = 0xDEADBEEF LE.
997        let he = HeaderExtension {
998            little_endian: true,
999            message_length: Some(0xDEAD_BEEF),
1000            ..HeaderExtension::default()
1001        };
1002        let bytes = he.encode().unwrap();
1003        assert_eq!(bytes[0], SUBMESSAGE_ID_HEADER_EXTENSION);
1004        assert_eq!(bytes[1], HE_FLAG_E | HE_FLAG_L);
1005        // octets_to_next_header LE = 4
1006        assert_eq!(&bytes[2..4], &[0x04, 0x00]);
1007        // body LE
1008        assert_eq!(&bytes[4..8], &[0xEF, 0xBE, 0xAD, 0xDE]);
1009    }
1010
1011    #[test]
1012    fn wire_layout_be_message_length_correct() {
1013        let he = HeaderExtension {
1014            little_endian: false,
1015            message_length: Some(0xDEAD_BEEF),
1016            ..HeaderExtension::default()
1017        };
1018        let bytes = he.encode().unwrap();
1019        assert_eq!(bytes[1], HE_FLAG_L); // E-Flag NICHT gesetzt
1020        // octets_to_next_header BE = 4
1021        assert_eq!(&bytes[2..4], &[0x00, 0x04]);
1022        assert_eq!(&bytes[4..8], &[0xDE, 0xAD, 0xBE, 0xEF]);
1023    }
1024
1025    #[test]
1026    fn empty_he_has_zero_octets_to_next_header() {
1027        let he = HeaderExtension {
1028            little_endian: true,
1029            ..HeaderExtension::default()
1030        };
1031        let bytes = he.encode().unwrap();
1032        assert_eq!(bytes.len(), 4);
1033        assert_eq!(&bytes[2..4], &[0, 0]);
1034    }
1035
1036    // ========================================================================
1037    // ChecksumValue::compute / verify (Spec §9.4.2.15.2)
1038    // ========================================================================
1039
1040    #[test]
1041    fn checksum_compute_none_yields_none() {
1042        let cs = ChecksumValue::compute(ChecksumKind::None, b"any payload");
1043        assert_eq!(cs, ChecksumValue::None);
1044    }
1045
1046    #[test]
1047    fn checksum_compute_crc32c_matches_rfc4960_vector() {
1048        // RFC 4960 Appendix B.4: CRC32C("123456789") = 0xE3069283
1049        let cs = ChecksumValue::compute(ChecksumKind::Crc32c, b"123456789");
1050        assert_eq!(cs, ChecksumValue::Crc32c(0xE306_9283));
1051    }
1052
1053    #[test]
1054    fn checksum_compute_crc64_xz_matches_known_vector() {
1055        // XZ utils CRC-64-XZ Standard-Test-Vector:
1056        // CRC-64("123456789") = 0x995DC9BBDF1939FA
1057        let cs = ChecksumValue::compute(ChecksumKind::Crc64, b"123456789");
1058        assert_eq!(cs, ChecksumValue::Crc64(0x995D_C9BB_DF19_39FA));
1059    }
1060
1061    #[test]
1062    fn checksum_compute_md5_matches_rfc1321_vector() {
1063        // RFC 1321 §A.5: MD5("abc") = 900150983cd24fb0 d6963f7d28e17f72
1064        let cs = ChecksumValue::compute(ChecksumKind::Md5, b"abc");
1065        let expected: [u8; 16] = [
1066            0x90, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0, 0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1,
1067            0x7f, 0x72,
1068        ];
1069        assert_eq!(cs, ChecksumValue::Md5(expected));
1070    }
1071
1072    #[test]
1073    fn checksum_verify_round_trip_crc32c_succeeds() {
1074        let payload = b"the quick brown fox jumps over the lazy dog";
1075        let cs = ChecksumValue::compute(ChecksumKind::Crc32c, payload);
1076        assert!(cs.verify(payload));
1077    }
1078
1079    #[test]
1080    fn checksum_verify_detects_tampered_payload() {
1081        let original = b"original message";
1082        let cs = ChecksumValue::compute(ChecksumKind::Crc32c, original);
1083        let tampered = b"original Message";
1084        assert!(!cs.verify(tampered));
1085    }
1086
1087    #[test]
1088    fn checksum_verify_none_kind_always_succeeds() {
1089        // Kind=None bedeutet "keine Checksum deklariert" — Verify ist
1090        // dann no-op und MUSS true zurueckgeben (sonst wuerden alle
1091        // Messages ohne deklarierte Checksum verworfen).
1092        let cs = ChecksumValue::None;
1093        assert!(cs.verify(b"any payload"));
1094        assert!(cs.verify(b""));
1095    }
1096
1097    #[test]
1098    fn checksum_verify_crc64_round_trip() {
1099        let payload = b"\x52\x54\x50\x53\x02\x05\x01\x0F";
1100        let cs = ChecksumValue::compute(ChecksumKind::Crc64, payload);
1101        assert!(cs.verify(payload));
1102    }
1103
1104    #[test]
1105    fn checksum_verify_md5_round_trip() {
1106        let payload = b"DDSI-RTPS 2.5 HEADER_EXTENSION";
1107        let cs = ChecksumValue::compute(ChecksumKind::Md5, payload);
1108        assert!(cs.verify(payload));
1109    }
1110}