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}