Skip to main content

dvb_ule/
ext_header.rs

1//! ULE Extension Headers — RFC 4326 §5, RFC 5163 §3.
2//!
3//! Extension headers are chained: each is introduced by a 16-bit Type field
4//! (the [`TypeField`] of the *preceding* header, or the SNDU base header's Type
5//! for the first one). A Type field `< 0x0600` introduces a further extension
6//! header; a Type field `>= 0x0600` is the EtherType of the PDU that follows.
7//!
8//! H-LEN semantics (RFC 4326 §5):
9//!
10//! - `H-LEN = 0` — Mandatory Extension Header: length is predefined per H-Type,
11//!   not signalled in H-LEN. (Test SNDU 0x00, Bridged-Frame 0x01, TS-Concat
12//!   0x02, PDU-Concat 0x03 — these consume the rest of the SNDU payload.)
13//! - `H-LEN = 1..=5` — Optional Extension Header: total extension length is
14//!   `2 * H-LEN` bytes **including** the 2-byte Type field, so the body is
15//!   `2 * H-LEN - 2` bytes.
16//! - `H-LEN >= 6` — not a Next-Header (the 16-bit field is itself an
17//!   EtherType); handled by [`TypeField`], never reaches this module.
18
19use alloc::vec::Vec;
20
21use crate::error::{Error, Result};
22use crate::type_field::TypeField;
23
24/// H-Type of the Test-SNDU mandatory extension header (RFC 4326 §5.1).
25pub const H_TYPE_TEST_SNDU: u8 = 0x00;
26/// H-Type of the Bridged-Frame mandatory extension header (RFC 4326 §5.2).
27pub const H_TYPE_BRIDGED_FRAME: u8 = 0x01;
28/// H-Type of the MPEG-2 TS-Concat mandatory extension header (RFC 5163 §3.1).
29pub const H_TYPE_TS_CONCAT: u8 = 0x02;
30/// H-Type of the PDU-Concat mandatory extension header (RFC 5163 §3.2).
31pub const H_TYPE_PDU_CONCAT: u8 = 0x03;
32/// H-Type of the TimeStamp optional extension header (RFC 5163 §3.3),
33/// decimal 257 → `H-Type` byte `0x01` with `H-LEN = 3`.
34pub const H_TYPE_TIMESTAMP: u8 = 0x01;
35/// H-Type of the Extension-Padding optional extension header (RFC 4326 §5.3),
36/// IANA value `0x100` → `H-Type` byte `0x00`, `H-LEN` 1..=5.
37pub const H_TYPE_EXT_PADDING: u8 = 0x00;
38
39/// Typed H-Type for a Mandatory extension header (`H-LEN = 0`, RFC 4326 §5).
40///
41/// Mandatory H-Types and Optional H-Types are separate IANA registries; value
42/// `0x00` means "Test SNDU" in the mandatory space and "Extension-Padding" in
43/// the optional space.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize))]
46#[non_exhaustive]
47pub enum MandatoryHType {
48    /// Test SNDU — H-Type `0x00` (RFC 4326 §5.1).
49    TestSndu,
50    /// Bridged Frame — H-Type `0x01` (RFC 4326 §5.2).
51    BridgedFrame,
52    /// MPEG-2 TS Concatenation — H-Type `0x02` (RFC 5163 §3.1).
53    TsConcat,
54    /// PDU Concatenation — H-Type `0x03` (RFC 5163 §3.2).
55    PduConcat,
56    /// An unrecognised mandatory H-Type.
57    Other(u8),
58}
59
60impl MandatoryHType {
61    /// Decode from the raw 8-bit H-Type byte.
62    pub fn from_u8(raw: u8) -> Self {
63        match raw {
64            H_TYPE_TEST_SNDU => MandatoryHType::TestSndu,
65            H_TYPE_BRIDGED_FRAME => MandatoryHType::BridgedFrame,
66            H_TYPE_TS_CONCAT => MandatoryHType::TsConcat,
67            H_TYPE_PDU_CONCAT => MandatoryHType::PduConcat,
68            other => MandatoryHType::Other(other),
69        }
70    }
71
72    /// Encode back to the raw 8-bit H-Type byte.
73    pub fn to_u8(self) -> u8 {
74        match self {
75            MandatoryHType::TestSndu => H_TYPE_TEST_SNDU,
76            MandatoryHType::BridgedFrame => H_TYPE_BRIDGED_FRAME,
77            MandatoryHType::TsConcat => H_TYPE_TS_CONCAT,
78            MandatoryHType::PduConcat => H_TYPE_PDU_CONCAT,
79            MandatoryHType::Other(v) => v,
80        }
81    }
82
83    /// Spec label for this mandatory H-Type.
84    pub fn name(&self) -> &'static str {
85        match self {
86            MandatoryHType::TestSndu => "test-sndu",
87            MandatoryHType::BridgedFrame => "bridged-frame",
88            MandatoryHType::TsConcat => "ts-concat",
89            MandatoryHType::PduConcat => "pdu-concat",
90            MandatoryHType::Other(_) => "mandatory",
91        }
92    }
93}
94
95dvb_common::impl_spec_display!(MandatoryHType, Other);
96
97/// Typed H-Type for an Optional extension header (`H-LEN = 1..=5`, RFC 4326 §5).
98///
99/// Optional H-Types share the `H-Type` byte namespace with Mandatory H-Types
100/// but are distinguished by a non-zero `H-LEN`.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103#[non_exhaustive]
104pub enum OptionalHType {
105    /// Extension-Padding — H-Type `0x00`, `H-LEN` 1..=5 (RFC 4326 §5.3).
106    ExtPadding,
107    /// TimeStamp — H-Type `0x01`, `H-LEN = 3` (RFC 5163 §3.3).
108    TimeStamp,
109    /// An unrecognised optional H-Type.
110    Other(u8),
111}
112
113impl OptionalHType {
114    /// Decode from the raw 8-bit H-Type byte.
115    pub fn from_u8(raw: u8) -> Self {
116        match raw {
117            H_TYPE_EXT_PADDING => OptionalHType::ExtPadding,
118            H_TYPE_TIMESTAMP => OptionalHType::TimeStamp,
119            other => OptionalHType::Other(other),
120        }
121    }
122
123    /// Encode back to the raw 8-bit H-Type byte.
124    pub fn to_u8(self) -> u8 {
125        match self {
126            OptionalHType::ExtPadding => H_TYPE_EXT_PADDING,
127            OptionalHType::TimeStamp => H_TYPE_TIMESTAMP,
128            OptionalHType::Other(v) => v,
129        }
130    }
131
132    /// Spec label for this optional H-Type.
133    pub fn name(&self) -> &'static str {
134        match self {
135            OptionalHType::ExtPadding => "extension-padding",
136            OptionalHType::TimeStamp => "timestamp",
137            OptionalHType::Other(_) => "optional",
138        }
139    }
140}
141
142dvb_common::impl_spec_display!(OptionalHType, Other);
143
144/// A single ULE extension header in a chain (RFC 4326 §5).
145///
146/// Each variant carries the `H-Type`/`H-LEN` implicitly; the body bytes that
147/// follow the introducing Type field are stored typed where the spec defines a
148/// layout, else as opaque bytes for forward compatibility.
149#[derive(Debug, Clone, PartialEq, Eq)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize))]
151#[non_exhaustive]
152pub enum ExtensionHeader {
153    /// Optional header (`H-LEN = 1..=5`), opaque body of `2 * h_len - 2` bytes.
154    ///
155    /// Covers TimeStamp, Extension-Padding and any unrecognised optional
156    /// header: the body is preserved verbatim so the chain round-trips.
157    Optional {
158        /// 3-bit length selector (`1..=5`).
159        h_len: u8,
160        /// 8-bit type code.
161        h_type: u8,
162        /// Body bytes (`2 * h_len - 2` of them).
163        body: Vec<u8>,
164    },
165    /// Mandatory header (`H-LEN = 0`) whose body consumes the remainder of the
166    /// SNDU payload up to (but excluding) the CRC.
167    ///
168    /// Test SNDU / Bridged-Frame / TS-Concat / PDU-Concat are all of this form;
169    /// their inner structure is preserved as opaque bytes (the SNDU `Length`
170    /// and CRC give the boundary).
171    Mandatory {
172        /// 8-bit type code (`0x00`..`0x03` for the RFC-registered set).
173        h_type: u8,
174        /// Body bytes — everything up to the CRC.
175        body: Vec<u8>,
176    },
177}
178
179impl ExtensionHeader {
180    /// The `H-LEN` nibble this header serializes with.
181    pub fn h_len(&self) -> u8 {
182        match self {
183            ExtensionHeader::Optional { h_len, .. } => *h_len,
184            ExtensionHeader::Mandatory { .. } => 0,
185        }
186    }
187
188    /// The `H-Type` byte this header serializes with.
189    pub fn h_type(&self) -> u8 {
190        match self {
191            ExtensionHeader::Optional { h_type, .. } => *h_type,
192            ExtensionHeader::Mandatory { h_type, .. } => *h_type,
193        }
194    }
195
196    /// The introducing [`TypeField`] for this header.
197    pub fn type_field(&self) -> TypeField {
198        TypeField::NextHeader {
199            h_len: self.h_len(),
200            h_type: self.h_type(),
201        }
202    }
203
204    /// `true` if this is a mandatory (`H-LEN = 0`) extension header.
205    pub fn is_mandatory(&self) -> bool {
206        matches!(self, ExtensionHeader::Mandatory { .. })
207    }
208
209    /// The typed [`MandatoryHType`] for a Mandatory header, or `None` if this
210    /// is an Optional header.
211    pub fn mandatory_h_type(&self) -> Option<MandatoryHType> {
212        match self {
213            ExtensionHeader::Mandatory { h_type, .. } => Some(MandatoryHType::from_u8(*h_type)),
214            ExtensionHeader::Optional { .. } => None,
215        }
216    }
217
218    /// The typed [`OptionalHType`] for an Optional header, or `None` if this
219    /// is a Mandatory header.
220    pub fn optional_h_type(&self) -> Option<OptionalHType> {
221        match self {
222            ExtensionHeader::Optional { h_type, .. } => Some(OptionalHType::from_u8(*h_type)),
223            ExtensionHeader::Mandatory { .. } => None,
224        }
225    }
226
227    /// Spec label for this header kind.
228    pub fn name(&self) -> &'static str {
229        match self {
230            ExtensionHeader::Optional { h_type, .. } => OptionalHType::from_u8(*h_type).name(),
231            ExtensionHeader::Mandatory { h_type, .. } => MandatoryHType::from_u8(*h_type).name(),
232        }
233    }
234
235    /// Total wire length of this header *including* its 2-byte introducing Type
236    /// field.
237    pub fn wire_len(&self) -> usize {
238        match self {
239            ExtensionHeader::Optional { h_len, .. } => 2 * (*h_len as usize),
240            ExtensionHeader::Mandatory { body, .. } => 2 + body.len(),
241        }
242    }
243}
244
245dvb_common::impl_spec_display!(ExtensionHeader);
246
247/// The decoded payload area of an SNDU (RFC 4326 §5): a chain of extension
248/// headers terminated by a final [`TypeField`] (an EtherType, or the
249/// introducing Type of a trailing Mandatory header) and the opaque PDU bytes.
250#[derive(Debug, Clone, PartialEq, Eq)]
251#[cfg_attr(feature = "serde", derive(serde::Serialize))]
252pub struct PayloadChain<'a> {
253    /// Zero or more optional extension headers, in wire order.
254    ///
255    /// A Mandatory header is always last and is represented by `final_type`
256    /// being a Next-Header plus the PDU being its body, so this list only ever
257    /// holds Optional headers in the typed-chain model.
258    pub headers: Vec<ExtensionHeader>,
259    /// The Type field that terminates the optional-header chain: either an
260    /// EtherType naming the PDU, or a Next-Header introducing a final Mandatory
261    /// header (whose body is `pdu`).
262    pub final_type: TypeField,
263    /// The opaque PDU bytes (or the Mandatory header's body).
264    pub pdu: &'a [u8],
265}
266
267impl<'a> PayloadChain<'a> {
268    /// Parse a payload chain: walk the optional extension headers, then read
269    /// the final Type field and treat everything after it as the PDU.
270    ///
271    /// `first_type` is the SNDU base header's Type field; `data` is the SNDU
272    /// payload area between the base header (+NPA) and the CRC.
273    pub fn parse(first_type: TypeField, data: &'a [u8]) -> Result<Self> {
274        let mut headers = Vec::new();
275        let mut cur = first_type;
276        let mut off = 0usize;
277
278        loop {
279            match cur {
280                TypeField::EtherType(_) => {
281                    // Terminal: the rest is the PDU.
282                    return Ok(PayloadChain {
283                        headers,
284                        final_type: cur,
285                        pdu: &data[off..],
286                    });
287                }
288                TypeField::NextHeader { h_len, h_type } => {
289                    if h_len == 0 {
290                        // Mandatory header — body runs to the CRC. Terminal.
291                        return Ok(PayloadChain {
292                            headers,
293                            final_type: cur,
294                            pdu: &data[off..],
295                        });
296                    }
297                    // Optional header: total = 2*h_len bytes incl. the 2-byte
298                    // Type field that introduced it (already consumed when we
299                    // read `cur`, except for the very first which sits in the
300                    // base header). The body is 2*h_len - 2 bytes, followed by
301                    // the next Type field (2 bytes).
302                    let body_len = 2 * (h_len as usize) - 2;
303                    let next_type_at = off + body_len;
304                    if next_type_at + 2 > data.len() {
305                        return Err(Error::InvalidExtensionHeader {
306                            reason: "optional extension header body/next-type exceeds payload",
307                        });
308                    }
309                    let body = data[off..next_type_at].to_vec();
310                    headers.push(ExtensionHeader::Optional {
311                        h_len,
312                        h_type,
313                        body,
314                    });
315                    let next_raw = u16::from_be_bytes([data[next_type_at], data[next_type_at + 1]]);
316                    cur = TypeField::from_u16(next_raw);
317                    off = next_type_at + 2;
318                }
319            }
320        }
321    }
322
323    /// Wire length of the chain *excluding* the SNDU base header's Type field
324    /// (which the SNDU serializer writes), i.e. the bytes from the first
325    /// optional-header body onward, including intervening Type fields, the
326    /// final Type field, and the PDU.
327    pub fn serialized_len(&self) -> usize {
328        // The SNDU base header writes the *first* Type field (`base_type()`),
329        // so the chain content here begins at the first header's body. The wire
330        // is:  body₀, type₁, body₁, type₂, …, body_{n-1}, final_type, pdu
331        // i.e. for N headers: Σ bodyᵢ + N·2 (each body is followed by a 2-byte
332        // Type field, the last being `final_type`) + pdu. With zero headers the
333        // chain content is just the PDU (`final_type` is the base Type).
334        let mut n = 0usize;
335        for h in &self.headers {
336            n += (h.wire_len() - 2) + 2;
337        }
338        n + self.pdu.len()
339    }
340
341    /// The Type field the SNDU base header must carry to introduce this chain:
342    /// the first optional header's Type, or `final_type` when there are no
343    /// optional headers.
344    pub fn base_type(&self) -> TypeField {
345        match self.headers.first() {
346            Some(h) => h.type_field(),
347            None => self.final_type,
348        }
349    }
350
351    /// Serialize the chain into `out`, starting *after* the base header's Type
352    /// field. Returns the number of bytes written.
353    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
354        let need = self.serialized_len();
355        if out.len() < need {
356            return Err(Error::OutputBufferTooSmall {
357                need,
358                have: out.len(),
359            });
360        }
361        // Wire order after the base-header Type field is:
362        //   body₀, type₁, body₁, type₂, …, type_final, pdu
363        // where typeᵢ introduces headerᵢ and the first header is introduced by
364        // the base-header Type field (written by the SNDU serializer).
365        let mut off = 0usize;
366        for (i, h) in self.headers.iter().enumerate() {
367            let body = match h {
368                ExtensionHeader::Optional { body, .. } => body,
369                ExtensionHeader::Mandatory { .. } => {
370                    return Err(Error::InvalidExtensionHeader {
371                        reason: "mandatory header must be the chain terminator, not a link",
372                    });
373                }
374            };
375            out[off..off + body.len()].copy_from_slice(body);
376            off += body.len();
377            let following = if i + 1 < self.headers.len() {
378                self.headers[i + 1].type_field()
379            } else {
380                self.final_type
381            };
382            out[off..off + 2].copy_from_slice(&following.to_u16().to_be_bytes());
383            off += 2;
384        }
385        // When there are no optional headers, `final_type` IS the base Type
386        // field (written by the SNDU serializer), so the chain content is just
387        // the PDU — nothing extra to write here.
388        out[off..off + self.pdu.len()].copy_from_slice(self.pdu);
389        off += self.pdu.len();
390        Ok(off)
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    // An optional (TimeStamp-shaped, H-LEN=3) header followed by an EtherType
399    // terminator round-trips through a full SNDU.
400    #[test]
401    fn optional_header_chain_round_trip() {
402        use crate::sndu::Sndu;
403
404        // TimeStamp: H-LEN=3 (6 bytes total = 2 Type + 4 body), H-Type=0x01.
405        let ts = ExtensionHeader::Optional {
406            h_len: 3,
407            h_type: H_TYPE_TIMESTAMP,
408            body: alloc::vec![0xAA, 0xBB, 0xCC, 0xDD],
409        };
410        assert_eq!(ts.wire_len(), 6);
411        let pdu = [0x45u8, 0x00, 0x00, 0x10];
412        let chain = PayloadChain {
413            headers: alloc::vec![ts.clone()],
414            final_type: TypeField::EtherType(0x0800),
415            pdu: &pdu,
416        };
417        // base_type must be the TimeStamp Next-Header (H-LEN=3,H-Type=1)=0x0301.
418        assert_eq!(chain.base_type().to_u16(), 0x0301);
419
420        let sndu = Sndu {
421            dest_address: None,
422            payload: chain.clone(),
423        };
424        let mut buf = alloc::vec![0u8; sndu.serialized_len()];
425        sndu.serialize_into(&mut buf).unwrap();
426        let parsed = Sndu::parse(&buf).unwrap();
427        assert_eq!(parsed.payload.headers.len(), 1);
428        assert_eq!(parsed.payload.headers[0], ts);
429        assert_eq!(parsed.payload.final_type, TypeField::EtherType(0x0800));
430        assert_eq!(parsed.payload.pdu, &pdu);
431        assert_eq!(parsed, sndu);
432    }
433
434    // A mandatory header (Test-SNDU, H-LEN=0) terminates the chain; its body is
435    // the rest of the payload.
436    #[test]
437    fn mandatory_header_round_trip() {
438        use crate::sndu::Sndu;
439
440        let body = [0xDEu8, 0xAD, 0xBE, 0xEF, 0x00];
441        // Base Type field = Mandatory Next-Header: H-LEN=0, H-Type=0x00 -> 0x0000.
442        let chain = PayloadChain {
443            headers: Vec::new(),
444            final_type: TypeField::NextHeader {
445                h_len: 0,
446                h_type: H_TYPE_TEST_SNDU,
447            },
448            pdu: &body,
449        };
450        assert_eq!(chain.base_type().to_u16(), 0x0000);
451
452        let sndu = Sndu {
453            dest_address: Some([1, 2, 3, 4, 5, 6]),
454            payload: chain,
455        };
456        let mut buf = alloc::vec![0u8; sndu.serialized_len()];
457        sndu.serialize_into(&mut buf).unwrap();
458        let parsed = Sndu::parse(&buf).unwrap();
459        assert!(parsed.payload.headers.is_empty());
460        assert_eq!(
461            parsed.payload.final_type,
462            TypeField::NextHeader {
463                h_len: 0,
464                h_type: 0
465            }
466        );
467        assert_eq!(parsed.payload.pdu, &body);
468        assert_eq!(parsed, sndu);
469    }
470
471    // Two chained optional headers (H-LEN=1 and H-LEN=2) before an EtherType.
472    #[test]
473    fn two_optional_headers_chain() {
474        use crate::sndu::Sndu;
475
476        let h1 = ExtensionHeader::Optional {
477            h_len: 1,
478            h_type: H_TYPE_EXT_PADDING,
479            body: Vec::new(), // 2*1-2 = 0 body bytes
480        };
481        let h2 = ExtensionHeader::Optional {
482            h_len: 2,
483            h_type: 0x42,
484            body: alloc::vec![0x11, 0x22], // 2*2-2 = 2 body bytes
485        };
486        let pdu = [0x99u8];
487        let chain = PayloadChain {
488            headers: alloc::vec![h1.clone(), h2.clone()],
489            final_type: TypeField::EtherType(0x86DD),
490            pdu: &pdu,
491        };
492        let sndu = Sndu {
493            dest_address: None,
494            payload: chain,
495        };
496        let mut buf = alloc::vec![0u8; sndu.serialized_len()];
497        sndu.serialize_into(&mut buf).unwrap();
498        let parsed = Sndu::parse(&buf).unwrap();
499        assert_eq!(parsed.payload.headers, alloc::vec![h1, h2]);
500        assert_eq!(parsed.payload.final_type, TypeField::EtherType(0x86DD));
501        assert_eq!(parsed.payload.pdu, &pdu);
502        assert_eq!(parsed, sndu);
503    }
504
505    // typed H-Type accessors return the expected variants.
506    #[test]
507    fn typed_h_type_accessors() {
508        let ts = ExtensionHeader::Optional {
509            h_len: 3,
510            h_type: H_TYPE_TIMESTAMP,
511            body: alloc::vec![0, 0, 0, 0],
512        };
513        assert_eq!(ts.optional_h_type(), Some(OptionalHType::TimeStamp));
514        assert_eq!(ts.mandatory_h_type(), None);
515
516        let mand = ExtensionHeader::Mandatory {
517            h_type: H_TYPE_BRIDGED_FRAME,
518            body: alloc::vec![],
519        };
520        assert_eq!(mand.mandatory_h_type(), Some(MandatoryHType::BridgedFrame));
521        assert_eq!(mand.optional_h_type(), None);
522
523        // Other arms
524        let unk_m = ExtensionHeader::Mandatory {
525            h_type: 0xF0,
526            body: alloc::vec![],
527        };
528        assert_eq!(unk_m.mandatory_h_type(), Some(MandatoryHType::Other(0xF0)));
529
530        let unk_o = ExtensionHeader::Optional {
531            h_len: 2,
532            h_type: 0xF0,
533            body: alloc::vec![0, 0],
534        };
535        assert_eq!(unk_o.optional_h_type(), Some(OptionalHType::Other(0xF0)));
536    }
537
538    // Every H_TYPE_* constant must map to a non-default name() — so a new
539    // registered H-Type without a label arm fails CI.
540    #[test]
541    fn all_h_type_constants_have_non_default_mandatory_label() {
542        let mandatory_constants: &[(u8, &str)] = &[
543            (H_TYPE_TEST_SNDU, "test-sndu"),
544            (H_TYPE_BRIDGED_FRAME, "bridged-frame"),
545            (H_TYPE_TS_CONCAT, "ts-concat"),
546            (H_TYPE_PDU_CONCAT, "pdu-concat"),
547        ];
548        for &(raw, expected_label) in mandatory_constants {
549            let t = MandatoryHType::from_u8(raw);
550            assert_ne!(
551                t.name(),
552                "mandatory",
553                "H_TYPE constant 0x{raw:02X} maps to the default fallback label — add a named arm"
554            );
555            assert_eq!(
556                t.name(),
557                expected_label,
558                "H_TYPE constant 0x{raw:02X} label mismatch"
559            );
560        }
561    }
562
563    #[test]
564    fn all_h_type_constants_have_non_default_optional_label() {
565        let optional_constants: &[(u8, &str)] = &[
566            (H_TYPE_EXT_PADDING, "extension-padding"),
567            (H_TYPE_TIMESTAMP, "timestamp"),
568        ];
569        for &(raw, expected_label) in optional_constants {
570            let t = OptionalHType::from_u8(raw);
571            assert_ne!(
572                t.name(), "optional",
573                "optional H_TYPE constant 0x{raw:02X} maps to the default fallback label — add a named arm"
574            );
575            assert_eq!(
576                t.name(),
577                expected_label,
578                "optional H_TYPE constant 0x{raw:02X} label mismatch"
579            );
580        }
581    }
582}