Skip to main content

dvb_bbframe/
packet.rs

1//! User packet extraction from BBFrame data fields.
2//!
3//! Supports Normal Mode (NM, 188-byte stride) and High Efficiency Mode
4//! (HEM, 187-byte stride) per EN 302 755 §5.1.8.
5//!
6//! In NM the first byte of each user packet in the data field is a CRC-8
7//! that replaces the original sync byte (0x47). In HEM the sync byte is
8//! simply absent and must be prepended.
9//!
10//! ## SYNCD handling
11//!
12//! `SYNCD` gives the bit offset from the start of the DATA FIELD to the
13//! first bit of the CRC-8 byte of the first user packet (NM) or the first
14//! byte of the first user packet (HEM).  Callers typically prepend any
15//! carry-over from the previous BBFrame and pass the result plus SYNCD.
16//!
17//! ## NPD/DNP reinsertion (HEM only)
18//!
19//! When NPD is active (`matype.npd == true`), each transmitted user
20//! packet is followed by a 1-byte DNP counter. The [`HemTsIter`] skips
21//! these DNP bytes automatically.
22
23use crate::header::{Bbheader, Mode, BBHEADER_LEN};
24
25/// User packet size in Normal Mode (188 bytes = full MPEG-2 TS packet).
26pub const NM_UP_SIZE: usize = 188;
27
28/// User packet size in High Efficiency Mode (187 bytes = TS minus sync byte).
29pub const HEM_UP_SIZE: usize = 187;
30
31/// MPEG-2 sync byte that CRC-8 replaces in NM.
32pub const TS_SYNC_BYTE: u8 = 0x47;
33
34/// Iterator over NM TS user packets.
35///
36/// Each item is a `[u8; 188]` with the sync byte restored to 0x47,
37/// replacing the CRC-8 byte that occupies position 0 in the data field.
38#[derive(Clone, Copy)]
39#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
40pub struct NmTsIter<'a> {
41    data: &'a [u8],
42    pos: usize,
43}
44
45impl<'a> NmTsIter<'a> {
46    /// Create a new NM TS user packet iterator.
47    ///
48    /// `data` is the full data field (after skipping SYNCD bytes if
49    /// already aligned). The iterator starts at byte 0 of `data`.
50    pub fn new(data: &'a [u8]) -> Self {
51        Self { data, pos: 0 }
52    }
53
54    /// Return the unconsumed tail of the data field.
55    pub fn remaining(self) -> &'a [u8] {
56        &self.data[self.pos..]
57    }
58}
59
60impl Iterator for NmTsIter<'_> {
61    type Item = [u8; NM_UP_SIZE];
62
63    fn next(&mut self) -> Option<Self::Item> {
64        if self.pos + NM_UP_SIZE > self.data.len() {
65            return None;
66        }
67        let mut pkt = [0u8; NM_UP_SIZE];
68        pkt[0] = TS_SYNC_BYTE; // Replace CRC-8 byte with sync byte
69        pkt[1..].copy_from_slice(&self.data[self.pos + 1..self.pos + NM_UP_SIZE]);
70        self.pos += NM_UP_SIZE;
71        Some(pkt)
72    }
73
74    fn size_hint(&self) -> (usize, Option<usize>) {
75        let remaining = self.data.len().saturating_sub(self.pos);
76        let count = remaining / NM_UP_SIZE;
77        (count, Some(count))
78    }
79}
80
81/// Iterator over HEM TS user packets.
82///
83/// Each item is a `[u8; 188]` with the sync byte prepended (0x47).
84/// The 187-byte user packets in the data field have no sync byte.
85/// If NPD is active, DNP bytes are skipped automatically.
86#[derive(Clone, Copy)]
87#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
88pub struct HemTsIter<'a> {
89    data: &'a [u8],
90    pos: usize,
91    npd: bool,
92}
93
94impl<'a> HemTsIter<'a> {
95    /// Create a new HEM TS user packet iterator.
96    ///
97    /// `data` is the full data field (after skipping SYNCD bytes if
98    /// already aligned). The iterator starts at byte 0 of `data`.
99    pub fn new(data: &'a [u8], npd: bool) -> Self {
100        Self { data, pos: 0, npd }
101    }
102
103    /// Return the unconsumed tail of the data field.
104    pub fn remaining(self) -> &'a [u8] {
105        &self.data[self.pos..]
106    }
107}
108
109impl Iterator for HemTsIter<'_> {
110    type Item = [u8; NM_UP_SIZE];
111
112    fn next(&mut self) -> Option<Self::Item> {
113        let stride = HEM_UP_SIZE + if self.npd { 1 } else { 0 };
114        if self.pos + stride > self.data.len() {
115            return None;
116        }
117        let mut pkt = [0u8; NM_UP_SIZE];
118        pkt[0] = TS_SYNC_BYTE;
119        pkt[1..].copy_from_slice(&self.data[self.pos..self.pos + HEM_UP_SIZE]);
120        self.pos += stride;
121        Some(pkt)
122    }
123
124    fn size_hint(&self) -> (usize, Option<usize>) {
125        let stride = HEM_UP_SIZE + if self.npd { 1 } else { 0 };
126        let remaining = self.data.len().saturating_sub(self.pos);
127        let count = remaining / stride;
128        (count, Some(count))
129    }
130}
131
132/// Build an appropriate user-packet iterator for the given BBHEADER.
133///
134/// Returns either an NM or HEM iterator depending on the detected mode.
135/// The caller must handle SYNCD alignment before calling this — typically
136/// by skipping `syncd / 8` bytes at the start of the data field.
137pub fn up_iter<'a>(
138    data: &'a [u8],
139    bbheader: &Bbheader,
140) -> Box<dyn Iterator<Item = [u8; NM_UP_SIZE]> + 'a> {
141    match bbheader.mode {
142        Mode::Normal => Box::new(NmTsIter::new(data)),
143        Mode::HighEfficiency => Box::new(HemTsIter::new(data, bbheader.matype.npd)),
144    }
145}
146
147/// Stateful UP extractor that carries partial user packets across BBFrame
148/// boundaries — a single UP can span multiple frames, especially in HEM
149/// where stride=187 bytes.
150///
151/// Use `feed_nm` / `feed_hem` per received frame; the returned Vec holds
152/// whichever 188-byte TS packets completed during that frame.
153pub struct CarryOverExtractor {
154    /// Partial TS packet being assembled (sync byte position 0).
155    buf: [u8; NM_UP_SIZE],
156    /// Bytes already written into `buf`.
157    pos: usize,
158}
159
160impl Default for CarryOverExtractor {
161    fn default() -> Self {
162        Self {
163            buf: [0u8; NM_UP_SIZE],
164            pos: 0,
165        }
166    }
167}
168
169impl CarryOverExtractor {
170    /// Create a fresh extractor with no carried-over state.
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Feed a HEM BBFrame's header + data field. Returns any TS packets that
176    /// completed during this frame.
177    ///
178    /// `npd` is the MATYPE-1 NPD flag for the frame — when true the stream
179    /// would additionally carry DNP bytes between UPs. NPD reinsertion is
180    /// NOT YET implemented here; callers must not pass `npd=true` until
181    /// the DNP path lands.
182    pub fn feed_hem(
183        &mut self,
184        bbheader_bytes: &[u8; BBHEADER_LEN],
185        data_field: &[u8],
186        npd: bool,
187    ) -> Vec<[u8; NM_UP_SIZE]> {
188        let mut out = Vec::new();
189        self.feed_hem_into(bbheader_bytes, data_field, npd, &mut out);
190        out
191    }
192
193    /// Buffer-reusing variant of [`feed_hem`](Self::feed_hem). Clears `out`,
194    /// then appends the TS packets that completed during this frame. Reuse the
195    /// same `Vec` across frames to avoid a per-frame heap allocation.
196    pub fn feed_hem_into(
197        &mut self,
198        bbheader_bytes: &[u8; BBHEADER_LEN],
199        data_field: &[u8],
200        npd: bool,
201        out: &mut Vec<[u8; NM_UP_SIZE]>,
202    ) {
203        out.clear();
204        assert!(
205            !npd,
206            "CarryOverExtractor does not yet implement NPD/DNP reinsertion"
207        );
208        let hdr = match Bbheader::parse(bbheader_bytes) {
209            Ok(h) => h,
210            Err(_) => return,
211        };
212        assert_eq!(
213            hdr.mode,
214            Mode::HighEfficiency,
215            "feed_hem called on non-HEM header"
216        );
217
218        let stride = HEM_UP_SIZE;
219        let syncd_bytes = (hdr.syncd / 8) as usize;
220        let dfl_bytes = (hdr.dfl / 8) as usize;
221        let data = &data_field[..dfl_bytes.min(data_field.len())];
222
223        // Complete the partial UP from the previous frame.
224        if self.pos > 0 {
225            let need = stride + 1 - self.pos; // +1 for the sync byte we'll prepend
226            if syncd_bytes == need && data.len() >= need {
227                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
228                self.buf[0] = 0x47;
229                out.push(self.buf);
230                self.pos = 0;
231            } else {
232                // Stride mismatch — discard partial and resync to syncd.
233                self.pos = 0;
234            }
235        }
236
237        // Extract complete UPs at stride.
238        let mut i = syncd_bytes;
239        while i + stride <= data.len() {
240            // HEM: 187 bytes of UP, prepend 0x47.
241            self.buf[0] = 0x47;
242            self.buf[1..1 + stride].copy_from_slice(&data[i..i + stride]);
243            out.push(self.buf);
244            i += stride;
245        }
246
247        // Buffer trailing partial packet (may be filled by next frame).
248        if i < data.len() {
249            let tail = (data.len() - i).min(stride);
250            // Store at offset 1 (reserving byte 0 for the sync we prepend later).
251            self.buf[1..1 + tail].copy_from_slice(&data[i..i + tail]);
252            self.pos = 1 + tail;
253        } else {
254            self.pos = 0;
255        }
256    }
257
258    /// Feed an NM BBFrame. Stride=188, `byte[0]` of each UP is the CRC-8 of
259    /// the previous UP and gets replaced with 0x47.
260    pub fn feed_nm(
261        &mut self,
262        bbheader_bytes: &[u8; BBHEADER_LEN],
263        data_field: &[u8],
264    ) -> Vec<[u8; NM_UP_SIZE]> {
265        let mut out = Vec::new();
266        self.feed_nm_into(bbheader_bytes, data_field, &mut out);
267        out
268    }
269
270    /// Buffer-reusing variant of [`feed_nm`](Self::feed_nm). Clears `out`, then
271    /// appends the TS packets that completed during this frame. Reuse the same
272    /// `Vec` across frames to avoid a per-frame heap allocation.
273    pub fn feed_nm_into(
274        &mut self,
275        bbheader_bytes: &[u8; BBHEADER_LEN],
276        data_field: &[u8],
277        out: &mut Vec<[u8; NM_UP_SIZE]>,
278    ) {
279        out.clear();
280        let hdr = match Bbheader::parse(bbheader_bytes) {
281            Ok(h) => h,
282            Err(_) => return,
283        };
284        assert_eq!(hdr.mode, Mode::Normal, "feed_nm called on non-NM header");
285
286        let stride = NM_UP_SIZE;
287        let syncd_bytes = (hdr.syncd / 8) as usize;
288        let dfl_bytes = (hdr.dfl / 8) as usize;
289        let data = &data_field[..dfl_bytes.min(data_field.len())];
290
291        // Complete partial UP from previous frame.
292        if self.pos > 0 {
293            let need = stride - self.pos;
294            if syncd_bytes == need && data.len() >= need {
295                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
296                self.buf[0] = 0x47; // replace CRC-8 with sync byte
297                out.push(self.buf);
298                self.pos = 0;
299            } else {
300                self.pos = 0;
301            }
302        }
303
304        // Extract complete UPs at stride.
305        let mut i = syncd_bytes;
306        while i + stride <= data.len() {
307            self.buf.copy_from_slice(&data[i..i + stride]);
308            self.buf[0] = 0x47; // replace CRC-8 with sync byte
309            out.push(self.buf);
310            i += stride;
311        }
312
313        // Buffer trailing partial.
314        if i < data.len() {
315            let tail = (data.len() - i).min(stride);
316            self.buf[..tail].copy_from_slice(&data[i..i + tail]);
317            self.pos = tail;
318        } else {
319            self.pos = 0;
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::header::{Bbheader, Matype, Mode, TsGs};
328
329    fn make_nm_header(syncd: u16) -> Bbheader {
330        Bbheader {
331            matype: Matype {
332                ts_gs: TsGs::Ts,
333                sis: true,
334                ccm: true,
335                issyi: false,
336                npd: false,
337                ext: 0,
338                isi: 0,
339            },
340            upl: 0,
341            sync: 0x47,
342            dfl: 0,
343            syncd,
344            mode: Mode::Normal,
345            issy_in_header: None,
346        }
347    }
348
349    fn make_hem_header(npd: bool) -> Bbheader {
350        Bbheader {
351            matype: Matype {
352                ts_gs: TsGs::Ts,
353                sis: true,
354                ccm: true,
355                issyi: false,
356                npd,
357                ext: 0,
358                isi: 0,
359            },
360            upl: 0,
361            sync: 0,
362            dfl: 0,
363            syncd: 0,
364            mode: Mode::HighEfficiency,
365            issy_in_header: None,
366        }
367    }
368
369    #[test]
370    fn nm_extracts_single_complete_up() {
371        let mut data = vec![0xAA; NM_UP_SIZE];
372        data[0] = 0xFF; // CRC-8 byte (will be replaced with sync)
373        for (i, byte) in data.iter_mut().enumerate().skip(1) {
374            *byte = i as u8;
375        }
376
377        let _hdr = make_nm_header(0);
378        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
379
380        assert_eq!(pkts.len(), 1);
381        assert_eq!(pkts[0][0], TS_SYNC_BYTE);
382        assert_eq!(&pkts[0][1..], &data[1..]);
383    }
384
385    #[test]
386    fn nm_multiple_back_to_back_ups() {
387        let num_ups = 3;
388        let mut data = Vec::with_capacity(num_ups * NM_UP_SIZE);
389
390        for i in 0..num_ups {
391            data.push(0x00); // CRC-8 byte
392            for j in 1..NM_UP_SIZE {
393                data.push((i * 10 + j) as u8);
394            }
395        }
396
397        let _hdr = make_nm_header(0);
398        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
399
400        assert_eq!(pkts.len(), num_ups);
401        for pkt in &pkts {
402            assert_eq!(pkt[0], TS_SYNC_BYTE);
403        }
404    }
405
406    #[test]
407    fn nm_partial_tail_does_not_yield() {
408        let mut data = vec![0xAA; NM_UP_SIZE + 50];
409        data[0] = 0xFF; // CRC-8
410        for (i, byte) in data.iter_mut().enumerate().skip(1) {
411            *byte = i as u8;
412        }
413
414        let _hdr = make_nm_header(0);
415        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
416
417        assert_eq!(pkts.len(), 1); // Only one complete UP
418    }
419
420    #[test]
421    fn nm_crc_byte_replaced_with_sync_only() {
422        let mut data = vec![0u8; NM_UP_SIZE];
423        data[0] = 0x42; // Some CRC value
424        data[1] = 0x47; // Actual sync byte in payload position 1
425        for (i, byte) in data.iter_mut().enumerate().skip(2) {
426            *byte = i as u8;
427        }
428
429        let _hdr = make_nm_header(0);
430        let pkt = up_iter(&data, &_hdr).next().unwrap();
431
432        assert_eq!(pkt[0], TS_SYNC_BYTE); // CRC replaced
433        assert_eq!(pkt[1], 0x47); // Original byte preserved
434        assert_eq!(&pkt[2..], &data[2..]);
435    }
436
437    #[test]
438    fn hem_extracts_up_with_sync_prepend() {
439        let data: Vec<u8> = (0..HEM_UP_SIZE as u8).cycle().take(HEM_UP_SIZE).collect();
440        let mut expected = [0u8; NM_UP_SIZE];
441        expected[0] = TS_SYNC_BYTE;
442        expected[1..].copy_from_slice(&data[..HEM_UP_SIZE]);
443
444        let _hdr = make_hem_header(false);
445        let pkt = up_iter(&data, &_hdr).next().unwrap();
446
447        assert_eq!(pkt, expected);
448    }
449
450    #[test]
451    fn hem_multiple_ups_without_npd() {
452        let num_ups = 3;
453        let data = vec![0xAB; num_ups * HEM_UP_SIZE];
454        let expected: [u8; NM_UP_SIZE] = {
455            let mut e = [0xAB; NM_UP_SIZE];
456            e[0] = TS_SYNC_BYTE;
457            e
458        };
459
460        let _hdr = make_hem_header(false);
461        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
462
463        assert_eq!(pkts.len(), num_ups);
464        for pkt in &pkts {
465            assert_eq!(*pkt, expected);
466        }
467    }
468
469    #[test]
470    fn hem_with_npd_skips_dnp_bytes() {
471        let up_size = HEM_UP_SIZE;
472        let num_ups = 2;
473        // Each UP followed by a DNP byte
474        let stride = up_size + 1;
475        let mut data = Vec::with_capacity(num_ups * stride);
476
477        for i in 0..num_ups {
478            for j in 0..up_size {
479                data.push((i * up_size + j) as u8);
480            }
481            data.push(i as u8); // DNP counter
482        }
483
484        let _hdr = make_hem_header(true);
485        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
486
487        assert_eq!(pkts.len(), num_ups);
488        for (i, pkt) in pkts.iter().enumerate() {
489            assert_eq!(pkt[0], TS_SYNC_BYTE);
490            // Verify the 187 bytes match the expected slice
491            let offset = i * stride;
492            assert_eq!(&pkt[1..], &data[offset..offset + up_size]);
493        }
494    }
495
496    #[test]
497    fn nm_remaining_returns_unconsumed_tail() {
498        let data = vec![0xAA; NM_UP_SIZE * 2 + 50];
499        let _hdr = make_nm_header(0);
500        let mut iter = NmTsIter::new(&data);
501
502        let _p1 = iter.next().unwrap();
503        let _p2 = iter.next().unwrap();
504        let remaining = iter.remaining();
505
506        assert_eq!(remaining.len(), 50);
507    }
508
509    #[test]
510    fn hem_remaining_returns_unconsumed_tail() {
511        let data = vec![0xAA; HEM_UP_SIZE * 2 + 30];
512        let _hdr = make_hem_header(false);
513        let mut iter = HemTsIter::new(&data, false);
514
515        let _p1 = iter.next().unwrap();
516        let _p2 = iter.next().unwrap();
517        let remaining = iter.remaining();
518
519        assert_eq!(remaining.len(), 30);
520    }
521
522    #[test]
523    fn empty_data_yields_nothing() {
524        let _hdr = make_nm_header(0);
525        let pkts: Vec<_> = up_iter(&[], &_hdr).collect();
526        assert!(pkts.is_empty());
527    }
528
529    #[test]
530    fn data_shorter_than_one_up_yields_nothing() {
531        let data = vec![0xAA; 100]; // Less than NM_UP_SIZE or HEM_UP_SIZE
532        let _hdr = make_nm_header(0);
533        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
534        assert!(pkts.is_empty());
535    }
536
537    #[test]
538    fn carry_over_extractor_emits_ts_across_two_bbframes_hem() {
539        // Two HEM BBFrames where the first ends with a partial UP (70 bytes) and
540        // the second completes it (117 bytes) then starts a new UP.
541        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
542            let mut h = [0u8; 10];
543            // MATYPE-1: TS=0b11 → 0xC0, SIS=1 → 0x20, CCM=1 → 0x10, ISSYI=0, NPD=0, EXT=0
544            h[0] = 0xF0;
545            h[1] = 0x00; // ISI=0
546                         // UPL=0 (ignored in HEM)
547            h[2] = 0x00;
548            h[3] = 0x00;
549            h[4] = (dfl_bits >> 8) as u8;
550            h[5] = (dfl_bits & 0xFF) as u8;
551            h[6] = 0x00; // sync (ignored in HEM)
552            h[7] = (syncd_bits >> 8) as u8;
553            h[8] = (syncd_bits & 0xFF) as u8;
554            // byte 9 = CRC-8 XOR MODE with MODE=1 for HEM
555            let crc = crate::crc::crc8(&h[..9]);
556            h[9] = crc ^ 1;
557            h
558        };
559
560        // Two BBFrames, each 70 data bytes. Pattern: each data byte is (frame << 4) | offset_lo.
561        // We expect CarryOverExtractor to produce 0 packets on frame1 (partial tail),
562        // then 1 on frame2 (187-byte completion across the boundary).
563        let frame1_data = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
564        let frame2_data = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
565        let hdr1 = make_hem_header(0, (frame1_data.len() * 8) as u16);
566        let hdr2 = make_hem_header(0, (frame2_data.len() * 8) as u16);
567
568        let mut extractor = CarryOverExtractor::new();
569        let packets1 = extractor.feed_hem(&hdr1, &frame1_data, false);
570        assert_eq!(packets1.len(), 0, "70 bytes (< 187) must not yet emit a UP");
571
572        let packets2 = extractor.feed_hem(&hdr2, &frame2_data, false);
573        assert!(!packets2.is_empty(), "boundary UP should complete");
574        assert_eq!(
575            packets2[0][0], 0x47,
576            "first emitted packet has sync byte prepended"
577        );
578    }
579
580    #[test]
581    fn feed_into_matches_allocating_api() {
582        // Run the same two-frame HEM sequence (partial UP carried across the
583        // boundary) through the allocating `feed_hem` and the buffer-reusing
584        // `feed_hem_into` (one Vec reused across both frames). Identical output
585        // proves `_into` clears + appends equivalently — if it failed to clear,
586        // frame 2's buffer would still hold frame 1's packets and diverge.
587        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
588            let mut h = [0u8; 10];
589            h[0] = 0xF0;
590            h[4] = (dfl_bits >> 8) as u8;
591            h[5] = (dfl_bits & 0xFF) as u8;
592            h[7] = (syncd_bits >> 8) as u8;
593            h[8] = (syncd_bits & 0xFF) as u8;
594            let crc = crate::crc::crc8(&h[..9]);
595            h[9] = crc ^ 1;
596            h
597        };
598        let f1 = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
599        let f2 = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
600        let h1 = make_hem_header(0, (f1.len() * 8) as u16);
601        let h2 = make_hem_header(0, (f2.len() * 8) as u16);
602
603        let mut alloc = CarryOverExtractor::new();
604        let a1 = alloc.feed_hem(&h1, &f1, false);
605        let a2 = alloc.feed_hem(&h2, &f2, false);
606
607        let mut reuse = CarryOverExtractor::new();
608        let mut buf = Vec::new();
609        reuse.feed_hem_into(&h1, &f1, false, &mut buf);
610        let b1 = buf.clone();
611        reuse.feed_hem_into(&h2, &f2, false, &mut buf);
612        let b2 = buf.clone();
613
614        assert_eq!(a1, b1, "frame 1 output matches across APIs");
615        assert_eq!(
616            a2, b2,
617            "frame 2 (carry-over) output matches; buffer was cleared"
618        );
619    }
620}