Skip to main content

dvb_ule/
sndu.rs

1//! SNDU — SubNetwork Data Unit (RFC 4326 §4).
2//!
3//! Wire layout (Figure 1): `D` bit (1) + `Length` (15) + `Type` (16) +
4//! optional 6-byte Destination NPA address (present iff `D = 0`) + PDU +
5//! CRC-32 (4 bytes). The `Length` field counts from the byte *after* the Type
6//! field up to and including the CRC (§4.2). The CRC-32 is the MPEG-2/DSM-CC
7//! CRC over the whole SNDU excluding the 4-byte trailer (§4.6) — provided by
8//! [`dvb_common::crc32_mpeg2`].
9
10use alloc::vec::Vec;
11
12use crate::error::{Error, Result};
13use crate::ext_header::PayloadChain;
14use crate::type_field::TypeField;
15
16/// Size in bytes of the SNDU base header (`D` + `Length` + `Type`).
17pub const BASE_HEADER_LEN: usize = 4;
18/// Size in bytes of the Destination NPA address (present when `D = 0`).
19pub const NPA_LEN: usize = 6;
20/// Size in bytes of the CRC-32 trailer.
21pub const CRC_LEN: usize = 4;
22/// The 15-bit `Length` value of an End Indicator — all length bits set.
23pub const END_INDICATOR_LENGTH: u16 = 0x7FFF;
24/// The two-byte value (`D = 1`, `Length = 0x7FFF`) that marks an End Indicator.
25pub const END_INDICATOR: u16 = 0xFFFF;
26/// The 0xFF byte used for TS-payload padding / stuffing (§4.3, §6).
27pub const PADDING_BYTE: u8 = 0xFF;
28
29/// Mask for the `D` bit (MSB of the first 16-bit word): `1` = no Destination
30/// NPA address present (RFC 4326 §4.2).
31pub(crate) const D_BIT_MASK: u16 = 0x8000;
32/// Mask for the 15-bit `Length` field in the first 16-bit word (RFC 4326 §4.2).
33pub(crate) const LENGTH_MASK: u16 = 0x7FFF;
34
35/// A parsed/owned SubNetwork Data Unit (RFC 4326 §4).
36///
37/// Holds typed header fields plus a borrowed view of the PDU. `Length` and the
38/// CRC are *not* stored: both are recomputed on serialize, so the round-trip is
39/// driven entirely from the typed fields (no raw passthrough).
40///
41/// The base-header `Type` field is derived from the `payload` chain (via
42/// [`PayloadChain::base_type`]) and is **not** stored as a separate field — the
43/// chain is the single source of truth so the two can never diverge. Use
44/// [`Sndu::type_field`] to read it.
45#[derive(Debug, Clone, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47pub struct Sndu<'a> {
48    /// The 6-byte Receiver Destination NPA address (§4.5), present iff `D = 0`.
49    /// `D` is derived from `Some`/`None`: `D = 0` ⇔ `Some`.
50    pub dest_address: Option<[u8; NPA_LEN]>,
51    /// Decoded extension-header chain + PDU (§5). The base-header Type field is
52    /// derived from the chain's `base_type()` on serialize.
53    pub payload: PayloadChain<'a>,
54}
55
56impl<'a> Sndu<'a> {
57    /// Construct an SNDU from a Type field, optional NPA address, and an opaque
58    /// PDU (no extension headers).
59    pub fn new(type_field: TypeField, dest_address: Option<[u8; NPA_LEN]>, pdu: &'a [u8]) -> Self {
60        Sndu {
61            dest_address,
62            payload: PayloadChain {
63                headers: Vec::new(),
64                final_type: type_field,
65                pdu,
66            },
67        }
68    }
69
70    /// The base-header Type field (§4.4): derived from the payload chain's
71    /// [`PayloadChain::base_type`]. This is the value that appears on the wire
72    /// at bytes `[2..4]` of the SNDU.
73    pub fn type_field(&self) -> TypeField {
74        self.payload.base_type()
75    }
76
77    /// The `D` bit value: `1` when no Destination Address is present.
78    pub fn d_bit(&self) -> bool {
79        self.dest_address.is_none()
80    }
81
82    /// The opaque PDU bytes (after any extension-header chain).
83    pub fn pdu(&self) -> &'a [u8] {
84        self.payload.pdu
85    }
86
87    /// The `Length` field as it appears on the wire: bytes counted from the
88    /// byte *after* the Type field, up to and including the CRC (§4.2).
89    /// = `NPA (if D=0)` + extension-chain content + PDU + CRC.
90    pub fn length_field(&self) -> usize {
91        let npa = if self.dest_address.is_some() {
92            NPA_LEN
93        } else {
94            0
95        };
96        npa + self.payload.serialized_len() + CRC_LEN
97    }
98
99    /// Total serialized size in bytes (base header + everything the `Length`
100    /// field counts).
101    pub fn serialized_len(&self) -> usize {
102        BASE_HEADER_LEN + self.length_field()
103    }
104
105    /// Parse an SNDU from the start of `data`. Requires the full SNDU
106    /// (header..CRC) to be present; trailing bytes are ignored. Validates the
107    /// CRC-32 trailer against a recomputed value (§4.6).
108    pub fn parse(data: &'a [u8]) -> Result<Self> {
109        if data.len() < BASE_HEADER_LEN {
110            return Err(Error::BufferTooShort {
111                need: BASE_HEADER_LEN,
112                have: data.len(),
113                what: "SNDU base header",
114            });
115        }
116        let first = u16::from_be_bytes([data[0], data[1]]);
117        let d_bit = (first & D_BIT_MASK) != 0;
118        let length = first & LENGTH_MASK;
119        let raw_type = u16::from_be_bytes([data[2], data[3]]);
120        let type_field = TypeField::from_u16(raw_type);
121
122        // The Length-counted region is `length` bytes starting at byte 4.
123        let region_end = BASE_HEADER_LEN + length as usize;
124        if length as usize > data.len() - BASE_HEADER_LEN {
125            return Err(Error::InvalidLength {
126                length,
127                reason: "Length field exceeds available bytes",
128            });
129        }
130        if (length as usize) < CRC_LEN {
131            return Err(Error::InvalidLength {
132                length,
133                reason: "Length shorter than the CRC trailer",
134            });
135        }
136
137        let dest_address = if d_bit {
138            None
139        } else {
140            if BASE_HEADER_LEN + NPA_LEN > region_end {
141                return Err(Error::InvalidLength {
142                    length,
143                    reason: "Length cannot hold the NPA address + CRC",
144                });
145            }
146            let mut npa = [0u8; NPA_LEN];
147            npa.copy_from_slice(&data[BASE_HEADER_LEN..BASE_HEADER_LEN + NPA_LEN]);
148            Some(npa)
149        };
150
151        let payload_start = BASE_HEADER_LEN + if d_bit { 0 } else { NPA_LEN };
152        let crc_start = region_end - CRC_LEN;
153        if crc_start < payload_start {
154            return Err(Error::InvalidLength {
155                length,
156                reason: "Length leaves no room for the payload",
157            });
158        }
159
160        // CRC covers the whole SNDU from byte 0 up to (not including) the CRC.
161        let computed = dvb_common::crc32_mpeg2::compute(&data[..crc_start]);
162        let found = u32::from_be_bytes([
163            data[crc_start],
164            data[crc_start + 1],
165            data[crc_start + 2],
166            data[crc_start + 3],
167        ]);
168        if computed != found {
169            return Err(Error::CrcMismatch { computed, found });
170        }
171
172        let payload = PayloadChain::parse(type_field, &data[payload_start..crc_start])?;
173
174        Ok(Sndu {
175            dest_address,
176            payload,
177        })
178    }
179
180    /// Serialize the SNDU into `out`, recomputing `Length` and the CRC-32 from
181    /// the typed fields. Returns the number of bytes written.
182    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
183        let total = self.serialized_len();
184        if out.len() < total {
185            return Err(Error::OutputBufferTooSmall {
186                need: total,
187                have: out.len(),
188            });
189        }
190
191        let length = self.length_field();
192        if length > END_INDICATOR_LENGTH as usize {
193            return Err(Error::FieldTooWide {
194                what: "Length",
195                value: length as u32,
196                bits: 15,
197            });
198        }
199
200        // Base Type field is always derived from the payload chain.
201        let base_type = self.payload.base_type();
202
203        // Byte 0..2: D | Length(15).
204        let mut first = length as u16 & LENGTH_MASK;
205        if self.d_bit() {
206            first |= D_BIT_MASK;
207        }
208        out[0..2].copy_from_slice(&first.to_be_bytes());
209        // Byte 2..4: Type.
210        out[2..4].copy_from_slice(&base_type.to_u16().to_be_bytes());
211
212        let mut off = BASE_HEADER_LEN;
213        if let Some(npa) = self.dest_address {
214            out[off..off + NPA_LEN].copy_from_slice(&npa);
215            off += NPA_LEN;
216        }
217        let written = self
218            .payload
219            .serialize_into(&mut out[off..total - CRC_LEN])?;
220        off += written;
221
222        // CRC-32 over bytes [0, off).
223        let crc = dvb_common::crc32_mpeg2::compute(&out[..off]);
224        out[off..off + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
225        off += CRC_LEN;
226        Ok(off)
227    }
228}
229
230/// `true` if the two bytes at the start of `data` are a ULE End Indicator
231/// (`0xFFFF`: `D = 1`, `Length = 0x7FFF`) — no further SNDUs in this TS packet
232/// (RFC 4326 §4.3).
233pub fn is_end_indicator(data: &[u8]) -> bool {
234    data.len() >= 2 && data[0] == PADDING_BYTE && data[1] == PADDING_BYTE
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use alloc::vec;
241
242    // A construct → serialize → assert exact wire bytes → reparse → equal test
243    // for an IPv4 SNDU WITH an NPA address (D=0).
244    #[test]
245    fn d0_with_npa_exact_wire_bytes() {
246        let pdu = [0x45u8, 0x00, 0x00, 0x14, 0xAB, 0xCD];
247        let npa = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06];
248        let sndu = Sndu::new(TypeField::EtherType(0x0800), Some(npa), &pdu);
249
250        // Length = NPA(6) + PDU(6) + CRC(4) = 16 = 0x10.
251        assert_eq!(sndu.length_field(), 16);
252        let mut out = vec![0u8; sndu.serialized_len()];
253        let n = sndu.serialize_into(&mut out).unwrap();
254        assert_eq!(n, 4 + 16);
255
256        // Header: D=0 -> first byte high bit clear. Length 0x0010, Type 0x0800.
257        assert_eq!(&out[0..2], &[0x00, 0x10]);
258        assert_eq!(&out[2..4], &[0x08, 0x00]);
259        // NPA then PDU.
260        assert_eq!(&out[4..10], &npa);
261        assert_eq!(&out[10..16], &pdu);
262        // CRC = MPEG-2 over bytes [0,16).
263        let crc = dvb_common::crc32_mpeg2::compute(&out[..16]);
264        assert_eq!(&out[16..20], &crc.to_be_bytes());
265
266        // Reparse → equal.
267        assert_eq!(Sndu::parse(&out).unwrap(), sndu);
268    }
269
270    // Same, for D=1 (no NPA).
271    #[test]
272    fn d1_without_npa_exact_wire_bytes() {
273        let pdu = [0x60u8, 0x00, 0x00, 0x00];
274        let sndu = Sndu::new(TypeField::EtherType(0x86DD), None, &pdu);
275
276        // Length = PDU(4) + CRC(4) = 8.
277        assert_eq!(sndu.length_field(), 8);
278        let mut out = vec![0u8; sndu.serialized_len()];
279        sndu.serialize_into(&mut out).unwrap();
280
281        // D=1 -> high bit set: 0x8000 | 0x0008 = 0x8008.
282        assert_eq!(&out[0..2], &[0x80, 0x08]);
283        assert_eq!(&out[2..4], &[0x86, 0xDD]);
284        assert_eq!(&out[4..8], &pdu);
285        let crc = dvb_common::crc32_mpeg2::compute(&out[..8]);
286        assert_eq!(&out[8..12], &crc.to_be_bytes());
287
288        assert_eq!(Sndu::parse(&out).unwrap(), sndu);
289    }
290
291    // Field-mutation bite: changing a PDU byte changes the serialized CRC, and
292    // the original CRC then fails to parse.
293    #[test]
294    fn mutating_a_field_changes_crc() {
295        let pdu = [0xDEu8, 0xAD, 0xBE, 0xEF];
296        let a = Sndu::new(TypeField::EtherType(0x0800), None, &pdu);
297        let mut buf_a = vec![0u8; a.serialized_len()];
298        a.serialize_into(&mut buf_a).unwrap();
299
300        let pdu2 = [0xDEu8, 0xAD, 0xBE, 0xEE]; // last byte changed
301        let b = Sndu::new(TypeField::EtherType(0x0800), None, &pdu2);
302        let mut buf_b = vec![0u8; b.serialized_len()];
303        b.serialize_into(&mut buf_b).unwrap();
304
305        assert_ne!(
306            buf_a, buf_b,
307            "different PDU must yield different wire bytes"
308        );
309        // The CRC trailers must differ.
310        assert_ne!(buf_a[8..12], buf_b[8..12]);
311    }
312
313    #[test]
314    fn npa_presence_drives_d_bit_and_length() {
315        let pdu = [0u8; 10];
316        let with = Sndu::new(TypeField::EtherType(0x0800), Some([0xFF; 6]), &pdu);
317        let without = Sndu::new(TypeField::EtherType(0x0800), None, &pdu);
318        assert!(!with.d_bit());
319        assert!(without.d_bit());
320        assert_eq!(with.length_field(), without.length_field() + NPA_LEN);
321    }
322
323    #[test]
324    fn rejects_bad_crc() {
325        let pdu = [1u8, 2, 3, 4];
326        let sndu = Sndu::new(TypeField::EtherType(0x0800), None, &pdu);
327        let mut buf = vec![0u8; sndu.serialized_len()];
328        sndu.serialize_into(&mut buf).unwrap();
329        let last = buf.len() - 1;
330        buf[last] ^= 0x01;
331        assert!(matches!(Sndu::parse(&buf), Err(Error::CrcMismatch { .. })));
332    }
333
334    #[test]
335    fn rejects_length_overrun() {
336        // Length claims 100 bytes but buffer is short.
337        let data = [0x00u8, 0x64, 0x08, 0x00, 0x00];
338        assert!(matches!(
339            Sndu::parse(&data),
340            Err(Error::InvalidLength { .. })
341        ));
342    }
343
344    #[test]
345    fn end_indicator_detected() {
346        assert!(is_end_indicator(&[0xFF, 0xFF, 0xFF]));
347        assert!(!is_end_indicator(&[0x00, 0x10]));
348    }
349
350    // type_field() accessor returns the chain's base type, which is the value
351    // that will be serialized to the wire — no divergence possible.
352    #[test]
353    fn type_field_accessor_matches_serialized_wire() {
354        let pdu = [0xAAu8; 4];
355        let sndu = Sndu::new(TypeField::EtherType(0x0800), None, &pdu);
356        assert_eq!(sndu.type_field(), TypeField::EtherType(0x0800));
357        // Confirm it's what's on the wire.
358        let mut buf = vec![0u8; sndu.serialized_len()];
359        sndu.serialize_into(&mut buf).unwrap();
360        let wire_type = u16::from_be_bytes([buf[2], buf[3]]);
361        assert_eq!(sndu.type_field().to_u16(), wire_type);
362    }
363}