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 alloc::vec::Vec;
24
25use crate::header::{BBHEADER_LEN, Bbheader, Mode};
26
27/// User packet size in Normal Mode (188 bytes = full MPEG-2 TS packet).
28pub const NM_UP_SIZE: usize = 188;
29
30/// User packet size in High Efficiency Mode (187 bytes = TS minus sync byte).
31pub const HEM_UP_SIZE: usize = 187;
32
33/// MPEG-2 sync byte that CRC-8 replaces in NM.
34pub const TS_SYNC_BYTE: u8 = 0x47;
35
36/// Iterator over NM TS user packets.
37///
38/// Each item is a `[u8; 188]` with the sync byte restored to 0x47,
39/// replacing the CRC-8 byte that occupies position 0 in the data field.
40#[derive(Clone, Copy)]
41#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
42pub struct NmTsIter<'a> {
43    data: &'a [u8],
44    pos: usize,
45}
46
47impl<'a> NmTsIter<'a> {
48    /// Create a new NM TS user packet iterator.
49    ///
50    /// `data` is the full data field (after skipping SYNCD bytes if
51    /// already aligned). The iterator starts at byte 0 of `data`.
52    pub fn new(data: &'a [u8]) -> Self {
53        Self { data, pos: 0 }
54    }
55
56    /// Return the unconsumed tail of the data field.
57    pub fn remaining(self) -> &'a [u8] {
58        self.data.get(self.pos..).unwrap_or(&[])
59    }
60}
61
62impl Iterator for NmTsIter<'_> {
63    type Item = [u8; NM_UP_SIZE];
64
65    fn next(&mut self) -> Option<Self::Item> {
66        if self.pos + NM_UP_SIZE > self.data.len() {
67            return None;
68        }
69        let mut pkt = [0u8; NM_UP_SIZE];
70        pkt[0] = TS_SYNC_BYTE; // Replace CRC-8 byte with sync byte
71        pkt[1..].copy_from_slice(&self.data[self.pos + 1..self.pos + NM_UP_SIZE]);
72        self.pos += NM_UP_SIZE;
73        Some(pkt)
74    }
75
76    fn size_hint(&self) -> (usize, Option<usize>) {
77        let remaining = self.data.len().saturating_sub(self.pos);
78        let count = remaining / NM_UP_SIZE;
79        (count, Some(count))
80    }
81}
82
83/// Iterator over HEM TS user packets.
84///
85/// Each item is a `[u8; 188]` with the sync byte prepended (0x47).
86/// The 187-byte user packets in the data field have no sync byte.
87/// If NPD is active, DNP bytes are skipped automatically.
88#[derive(Clone, Copy)]
89#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
90pub struct HemTsIter<'a> {
91    data: &'a [u8],
92    pos: usize,
93    npd: bool,
94}
95
96impl<'a> HemTsIter<'a> {
97    /// Create a new HEM TS user packet iterator.
98    ///
99    /// `data` is the full data field (after skipping SYNCD bytes if
100    /// already aligned). The iterator starts at byte 0 of `data`.
101    pub fn new(data: &'a [u8], npd: bool) -> Self {
102        Self { data, pos: 0, npd }
103    }
104
105    /// Return the unconsumed tail of the data field.
106    pub fn remaining(self) -> &'a [u8] {
107        self.data.get(self.pos..).unwrap_or(&[])
108    }
109}
110
111impl Iterator for HemTsIter<'_> {
112    type Item = [u8; NM_UP_SIZE];
113
114    fn next(&mut self) -> Option<Self::Item> {
115        let stride = HEM_UP_SIZE + if self.npd { 1 } else { 0 };
116        if self.pos + stride > self.data.len() {
117            return None;
118        }
119        let mut pkt = [0u8; NM_UP_SIZE];
120        pkt[0] = TS_SYNC_BYTE;
121        pkt[1..].copy_from_slice(&self.data[self.pos..self.pos + HEM_UP_SIZE]);
122        self.pos += stride;
123        Some(pkt)
124    }
125
126    fn size_hint(&self) -> (usize, Option<usize>) {
127        let stride = HEM_UP_SIZE + if self.npd { 1 } else { 0 };
128        let remaining = self.data.len().saturating_sub(self.pos);
129        let count = remaining / stride;
130        (count, Some(count))
131    }
132}
133
134/// Concrete user-packet iterator returned by [`up_iter`].
135///
136/// Selects NM or HEM iteration at runtime without heap allocation or
137/// dynamic dispatch — the mode is baked into the variant.
138#[derive(Clone, Copy)]
139#[non_exhaustive]
140pub enum UpIter<'a> {
141    /// Normal Mode iteration.
142    Normal(NmTsIter<'a>),
143    /// High Efficiency Mode iteration.
144    HighEfficiency(HemTsIter<'a>),
145}
146
147impl Iterator for UpIter<'_> {
148    type Item = [u8; NM_UP_SIZE];
149
150    fn next(&mut self) -> Option<Self::Item> {
151        match self {
152            Self::Normal(it) => it.next(),
153            Self::HighEfficiency(it) => it.next(),
154        }
155    }
156
157    fn size_hint(&self) -> (usize, Option<usize>) {
158        match self {
159            Self::Normal(it) => it.size_hint(),
160            Self::HighEfficiency(it) => it.size_hint(),
161        }
162    }
163}
164
165/// Build an appropriate user-packet iterator for the given BBHEADER.
166///
167/// Returns either an NM or HEM iterator depending on the detected mode.
168/// The caller must handle SYNCD alignment before calling this — typically
169/// by skipping `syncd / 8` bytes at the start of the data field.
170pub fn up_iter<'a>(data: &'a [u8], bbheader: &Bbheader) -> UpIter<'a> {
171    match bbheader.mode {
172        Mode::Normal => UpIter::Normal(NmTsIter::new(data)),
173        Mode::HighEfficiency => UpIter::HighEfficiency(HemTsIter::new(data, bbheader.matype.npd)),
174    }
175}
176
177/// Stateful UP extractor that carries partial user packets across BBFrame
178/// boundaries — a single UP can span multiple frames, especially in HEM
179/// where stride=187 bytes.
180///
181/// Use `feed_nm` / `feed_hem` per received frame; the returned Vec holds
182/// whichever 188-byte TS packets completed during that frame.
183///
184/// Diagnostic counters are accumulated and exposed via [`stats`](CarryOverExtractor::stats).
185pub struct CarryOverExtractor {
186    /// Partial TS packet being assembled (sync byte position 0).
187    buf: [u8; NM_UP_SIZE],
188    /// Bytes already written into `buf`.
189    pos: usize,
190    /// Diagnostic counters (see [`CarryOverStats`]).
191    stats: CarryOverStats,
192}
193
194impl Default for CarryOverExtractor {
195    fn default() -> Self {
196        Self {
197            buf: [0u8; NM_UP_SIZE],
198            pos: 0,
199            stats: CarryOverStats::default(),
200        }
201    }
202}
203
204/// Diagnostic counters for a [`CarryOverExtractor`], read via
205/// [`CarryOverExtractor::stats`].
206///
207/// The extractor stays resilient (it never errors or panics on wire-derived
208/// input — bad frames are skipped so a stream keeps flowing). These counters
209/// make the otherwise-silent skips observable. Note the distinction:
210/// `npd_unsupported` counts **valid data we failed to recover** (a capability
211/// gap), whereas the others count malformed/misrouted input.
212#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
213#[non_exhaustive]
214pub struct CarryOverStats {
215    /// HEM frames skipped because Null-Packet-Deletion (DNP) reinsertion is not
216    /// implemented. These carry **valid** user packets that are NOT recovered —
217    /// a known capability gap, not wire corruption. **Non-zero means real data
218    /// was dropped**; treat it as a signal that NPD-HEM input is unsupported.
219    pub npd_unsupported: u64,
220    /// Frames whose 10-byte BBHEADER failed to parse.
221    pub header_parse_failures: u64,
222    /// Frames fed to the wrong mode path (an NM header to `feed_hem_into`, or a
223    /// HEM header to `feed_nm_into`).
224    pub mode_mismatches: u64,
225    /// Carried-over partial user packets discarded on a SYNCD/stride mismatch
226    /// (the extractor resynchronised at the frame's SYNCD).
227    pub partial_discards: u64,
228}
229
230impl CarryOverExtractor {
231    /// Create a fresh extractor with no carried-over state.
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    /// Diagnostic counters accumulated across all `feed_*` calls — see
237    /// [`CarryOverStats`]. Check `npd_unsupported` in particular: a non-zero
238    /// value means valid HEM frames were dropped (NPD reinsertion unsupported).
239    #[must_use]
240    pub fn stats(&self) -> CarryOverStats {
241        self.stats
242    }
243
244    /// Feed a HEM BBFrame's header + data field. Returns any TS packets that
245    /// completed during this frame.
246    ///
247    /// `npd` is the MATYPE-1 NPD flag for the frame — when true the stream
248    /// would additionally carry DNP bytes between UPs. NPD reinsertion is
249    /// NOT YET implemented here; callers must not pass `npd=true` until
250    /// the DNP path lands.
251    pub fn feed_hem(
252        &mut self,
253        bbheader_bytes: &[u8; BBHEADER_LEN],
254        data_field: &[u8],
255        npd: bool,
256    ) -> Vec<[u8; NM_UP_SIZE]> {
257        let mut out = Vec::new();
258        self.feed_hem_into(bbheader_bytes, data_field, npd, &mut out);
259        out
260    }
261
262    /// Buffer-reusing variant of [`feed_hem`](Self::feed_hem). Clears `out`,
263    /// then appends the TS packets that completed during this frame. Reuse the
264    /// same `Vec` across frames to avoid a per-frame heap allocation.
265    pub fn feed_hem_into(
266        &mut self,
267        bbheader_bytes: &[u8; BBHEADER_LEN],
268        data_field: &[u8],
269        npd: bool,
270        out: &mut Vec<[u8; NM_UP_SIZE]>,
271    ) {
272        out.clear();
273        // NPD/DNP reinsertion is not yet implemented; rather than panic on
274        // wire-derived input, produce no output (out is already cleared).
275        if npd {
276            self.stats.npd_unsupported += 1;
277            return;
278        }
279        let hdr = match Bbheader::parse(bbheader_bytes) {
280            Ok(h) => h,
281            Err(_) => {
282                self.stats.header_parse_failures += 1;
283                return;
284            }
285        };
286        // Mismatched mode (caller fed a non-HEM header): no output, no panic.
287        if hdr.mode != Mode::HighEfficiency {
288            self.stats.mode_mismatches += 1;
289            return;
290        }
291
292        let stride = HEM_UP_SIZE;
293        // SYNCD=0xFFFF (65535) means "no UP starts in the DATA FIELD" — the
294        // entire data field is a continuation of the carried-over partial UP.
295        // EN 302 755 Table 2.
296        let all_continuation = hdr.syncd == 0xFFFF;
297        let syncd_bytes = if all_continuation {
298            0
299        } else {
300            (hdr.syncd / 8) as usize
301        };
302        let dfl_bytes = (hdr.dfl / 8) as usize;
303        let data = &data_field[..dfl_bytes.min(data_field.len())];
304
305        if all_continuation {
306            // The whole data field continues the previous partial UP.
307            if self.pos > 0 {
308                let space = stride + 1 - self.pos; // bytes still needed to complete
309                let take = data.len().min(space);
310                self.buf[self.pos..self.pos + take].copy_from_slice(&data[..take]);
311                self.pos += take;
312                if self.pos == stride + 1 {
313                    // UP is now complete.
314                    self.buf[0] = TS_SYNC_BYTE;
315                    out.push(self.buf);
316                    self.pos = 0;
317                }
318            }
319            // Any bytes beyond the completed UP are a new partial; in practice
320            // SYNCD=0xFFFF means the whole field is the continuation, so there
321            // should be nothing left — but buffer any excess defensively.
322            return;
323        }
324
325        // Complete the partial UP from the previous frame.
326        if self.pos > 0 {
327            let need = stride + 1 - self.pos; // +1 for the sync byte we'll prepend
328            if syncd_bytes == need && data.len() >= need {
329                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
330                self.buf[0] = TS_SYNC_BYTE;
331                out.push(self.buf);
332                self.pos = 0;
333            } else {
334                // Stride mismatch — discard partial and resync to syncd.
335                self.stats.partial_discards += 1;
336                self.pos = 0;
337            }
338        }
339
340        // Extract complete UPs at stride.
341        let mut i = syncd_bytes;
342        while i + stride <= data.len() {
343            // HEM: 187 bytes of UP, prepend sync byte.
344            self.buf[0] = TS_SYNC_BYTE;
345            self.buf[1..1 + stride].copy_from_slice(&data[i..i + stride]);
346            out.push(self.buf);
347            i += stride;
348        }
349
350        // Buffer trailing partial packet (may be filled by next frame).
351        if i < data.len() {
352            let tail = (data.len() - i).min(stride);
353            // Store at offset 1 (reserving byte 0 for the sync we prepend later).
354            self.buf[1..1 + tail].copy_from_slice(&data[i..i + tail]);
355            self.pos = 1 + tail;
356        } else {
357            self.pos = 0;
358        }
359    }
360
361    /// Feed an NM BBFrame. Stride=188, `byte[0]` of each UP is the CRC-8 of
362    /// the previous UP and gets replaced with 0x47.
363    pub fn feed_nm(
364        &mut self,
365        bbheader_bytes: &[u8; BBHEADER_LEN],
366        data_field: &[u8],
367    ) -> Vec<[u8; NM_UP_SIZE]> {
368        let mut out = Vec::new();
369        self.feed_nm_into(bbheader_bytes, data_field, &mut out);
370        out
371    }
372
373    /// Buffer-reusing variant of [`feed_nm`](Self::feed_nm). Clears `out`, then
374    /// appends the TS packets that completed during that frame. Reuse the same
375    /// `Vec` across frames to avoid a per-frame heap allocation.
376    pub fn feed_nm_into(
377        &mut self,
378        bbheader_bytes: &[u8; BBHEADER_LEN],
379        data_field: &[u8],
380        out: &mut Vec<[u8; NM_UP_SIZE]>,
381    ) {
382        out.clear();
383        let hdr = match Bbheader::parse(bbheader_bytes) {
384            Ok(h) => h,
385            Err(_) => {
386                self.stats.header_parse_failures += 1;
387                return;
388            }
389        };
390        // Mismatched mode (caller fed a non-NM header): no output, no panic.
391        if hdr.mode != Mode::Normal {
392            self.stats.mode_mismatches += 1;
393            return;
394        }
395
396        let stride = NM_UP_SIZE;
397        // SYNCD=0xFFFF (65535) means "no UP starts in the DATA FIELD" — the
398        // entire data field is a continuation of the carried-over partial UP.
399        // EN 302 755 Table 2.
400        let all_continuation = hdr.syncd == 0xFFFF;
401        let syncd_bytes = if all_continuation {
402            0
403        } else {
404            (hdr.syncd / 8) as usize
405        };
406        let dfl_bytes = (hdr.dfl / 8) as usize;
407        let data = &data_field[..dfl_bytes.min(data_field.len())];
408
409        if all_continuation {
410            // The whole data field continues the previous partial UP.
411            if self.pos > 0 {
412                let space = stride - self.pos; // bytes still needed to complete
413                let take = data.len().min(space);
414                self.buf[self.pos..self.pos + take].copy_from_slice(&data[..take]);
415                self.pos += take;
416                if self.pos == stride {
417                    // UP is now complete.
418                    self.buf[0] = TS_SYNC_BYTE; // replace CRC-8 with sync byte
419                    out.push(self.buf);
420                    self.pos = 0;
421                }
422            }
423            return;
424        }
425
426        // Complete partial UP from previous frame.
427        if self.pos > 0 {
428            let need = stride - self.pos;
429            if syncd_bytes == need && data.len() >= need {
430                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
431                self.buf[0] = TS_SYNC_BYTE; // replace CRC-8 with sync byte
432                out.push(self.buf);
433                self.pos = 0;
434            } else {
435                // Stride mismatch — discard partial and resync to syncd.
436                self.stats.partial_discards += 1;
437                self.pos = 0;
438            }
439        }
440
441        // Extract complete UPs at stride.
442        let mut i = syncd_bytes;
443        while i + stride <= data.len() {
444            self.buf.copy_from_slice(&data[i..i + stride]);
445            self.buf[0] = TS_SYNC_BYTE; // replace CRC-8 with sync byte
446            out.push(self.buf);
447            i += stride;
448        }
449
450        // Buffer trailing partial.
451        if i < data.len() {
452            let tail = (data.len() - i).min(stride);
453            self.buf[..tail].copy_from_slice(&data[i..i + tail]);
454            self.pos = tail;
455        } else {
456            self.pos = 0;
457        }
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use crate::header::{Bbheader, Matype, Mode, TsGs};
465
466    fn make_nm_header(syncd: u16) -> Bbheader {
467        Bbheader {
468            matype: Matype {
469                ts_gs: TsGs::Ts,
470                sis: true,
471                ccm: true,
472                issyi: false,
473                npd: false,
474                ext: 0,
475                isi: 0,
476            },
477            upl: 0,
478            sync: 0x47,
479            dfl: 0,
480            syncd,
481            mode: Mode::Normal,
482            issy_in_header: None,
483        }
484    }
485
486    fn make_hem_header(npd: bool) -> Bbheader {
487        Bbheader {
488            matype: Matype {
489                ts_gs: TsGs::Ts,
490                sis: true,
491                ccm: true,
492                issyi: false,
493                npd,
494                ext: 0,
495                isi: 0,
496            },
497            upl: 0,
498            sync: 0,
499            dfl: 0,
500            syncd: 0,
501            mode: Mode::HighEfficiency,
502            issy_in_header: None,
503        }
504    }
505
506    #[test]
507    fn nm_extracts_single_complete_up() {
508        let mut data = vec![0xAA; NM_UP_SIZE];
509        data[0] = 0xFF; // CRC-8 byte (will be replaced with sync)
510        for (i, byte) in data.iter_mut().enumerate().skip(1) {
511            *byte = i as u8;
512        }
513
514        let _hdr = make_nm_header(0);
515        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
516
517        assert_eq!(pkts.len(), 1);
518        assert_eq!(pkts[0][0], TS_SYNC_BYTE);
519        assert_eq!(&pkts[0][1..], &data[1..]);
520    }
521
522    #[test]
523    fn nm_multiple_back_to_back_ups() {
524        let num_ups = 3;
525        let mut data = Vec::with_capacity(num_ups * NM_UP_SIZE);
526
527        for i in 0..num_ups {
528            data.push(0x00); // CRC-8 byte
529            for j in 1..NM_UP_SIZE {
530                data.push((i * 10 + j) as u8);
531            }
532        }
533
534        let _hdr = make_nm_header(0);
535        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
536
537        assert_eq!(pkts.len(), num_ups);
538        for pkt in &pkts {
539            assert_eq!(pkt[0], TS_SYNC_BYTE);
540        }
541    }
542
543    #[test]
544    fn nm_partial_tail_does_not_yield() {
545        let mut data = vec![0xAA; NM_UP_SIZE + 50];
546        data[0] = 0xFF; // CRC-8
547        for (i, byte) in data.iter_mut().enumerate().skip(1) {
548            *byte = i as u8;
549        }
550
551        let _hdr = make_nm_header(0);
552        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
553
554        assert_eq!(pkts.len(), 1); // Only one complete UP
555    }
556
557    #[test]
558    fn nm_crc_byte_replaced_with_sync_only() {
559        let mut data = vec![0u8; NM_UP_SIZE];
560        data[0] = 0x42; // Some CRC value
561        data[1] = 0x47; // Actual sync byte in payload position 1
562        for (i, byte) in data.iter_mut().enumerate().skip(2) {
563            *byte = i as u8;
564        }
565
566        let _hdr = make_nm_header(0);
567        let pkt = up_iter(&data, &_hdr).next().unwrap();
568
569        assert_eq!(pkt[0], TS_SYNC_BYTE); // CRC replaced
570        assert_eq!(pkt[1], 0x47); // Original byte preserved
571        assert_eq!(&pkt[2..], &data[2..]);
572    }
573
574    #[test]
575    fn hem_extracts_up_with_sync_prepend() {
576        let data: Vec<u8> = (0..HEM_UP_SIZE as u8).cycle().take(HEM_UP_SIZE).collect();
577        let mut expected = [0u8; NM_UP_SIZE];
578        expected[0] = TS_SYNC_BYTE;
579        expected[1..].copy_from_slice(&data[..HEM_UP_SIZE]);
580
581        let _hdr = make_hem_header(false);
582        let pkt = up_iter(&data, &_hdr).next().unwrap();
583
584        assert_eq!(pkt, expected);
585    }
586
587    #[test]
588    fn hem_multiple_ups_without_npd() {
589        let num_ups = 3;
590        let data = vec![0xAB; num_ups * HEM_UP_SIZE];
591        let expected: [u8; NM_UP_SIZE] = {
592            let mut e = [0xAB; NM_UP_SIZE];
593            e[0] = TS_SYNC_BYTE;
594            e
595        };
596
597        let _hdr = make_hem_header(false);
598        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
599
600        assert_eq!(pkts.len(), num_ups);
601        for pkt in &pkts {
602            assert_eq!(*pkt, expected);
603        }
604    }
605
606    #[test]
607    fn hem_with_npd_skips_dnp_bytes() {
608        let up_size = HEM_UP_SIZE;
609        let num_ups = 2;
610        // Each UP followed by a DNP byte
611        let stride = up_size + 1;
612        let mut data = Vec::with_capacity(num_ups * stride);
613
614        for i in 0..num_ups {
615            for j in 0..up_size {
616                data.push((i * up_size + j) as u8);
617            }
618            data.push(i as u8); // DNP counter
619        }
620
621        let _hdr = make_hem_header(true);
622        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
623
624        assert_eq!(pkts.len(), num_ups);
625        for (i, pkt) in pkts.iter().enumerate() {
626            assert_eq!(pkt[0], TS_SYNC_BYTE);
627            // Verify the 187 bytes match the expected slice
628            let offset = i * stride;
629            assert_eq!(&pkt[1..], &data[offset..offset + up_size]);
630        }
631    }
632
633    #[test]
634    fn nm_remaining_returns_unconsumed_tail() {
635        let data = vec![0xAA; NM_UP_SIZE * 2 + 50];
636        let _hdr = make_nm_header(0);
637        let mut iter = NmTsIter::new(&data);
638
639        let _p1 = iter.next().unwrap();
640        let _p2 = iter.next().unwrap();
641        let remaining = iter.remaining();
642
643        assert_eq!(remaining.len(), 50);
644    }
645
646    #[test]
647    fn hem_remaining_returns_unconsumed_tail() {
648        let data = vec![0xAA; HEM_UP_SIZE * 2 + 30];
649        let _hdr = make_hem_header(false);
650        let mut iter = HemTsIter::new(&data, false);
651
652        let _p1 = iter.next().unwrap();
653        let _p2 = iter.next().unwrap();
654        let remaining = iter.remaining();
655
656        assert_eq!(remaining.len(), 30);
657    }
658
659    #[test]
660    fn empty_data_yields_nothing() {
661        let _hdr = make_nm_header(0);
662        let pkts: Vec<_> = up_iter(&[], &_hdr).collect();
663        assert!(pkts.is_empty());
664    }
665
666    #[test]
667    fn data_shorter_than_one_up_yields_nothing() {
668        let data = vec![0xAA; 100]; // Less than NM_UP_SIZE or HEM_UP_SIZE
669        let _hdr = make_nm_header(0);
670        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
671        assert!(pkts.is_empty());
672    }
673
674    #[test]
675    fn carry_over_extractor_emits_ts_across_two_bbframes_hem() {
676        // Two HEM BBFrames where the first ends with a partial UP (70 bytes) and
677        // the second completes it (117 bytes) then starts a new UP.
678        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
679            let mut h = [0u8; 10];
680            // MATYPE-1: TS=0b11 → 0xC0, SIS=1 → 0x20, CCM=1 → 0x10, ISSYI=0, NPD=0, EXT=0
681            h[0] = 0xF0;
682            h[1] = 0x00; // ISI=0
683            // UPL=0 (ignored in HEM)
684            h[2] = 0x00;
685            h[3] = 0x00;
686            h[4] = (dfl_bits >> 8) as u8;
687            h[5] = (dfl_bits & 0xFF) as u8;
688            h[6] = 0x00; // sync (ignored in HEM)
689            h[7] = (syncd_bits >> 8) as u8;
690            h[8] = (syncd_bits & 0xFF) as u8;
691            // byte 9 = CRC-8 XOR MODE with MODE=1 for HEM
692            let crc = crate::crc::crc8(&h[..9]);
693            h[9] = crc ^ 1;
694            h
695        };
696
697        // Two BBFrames, each 70 data bytes. Pattern: each data byte is (frame << 4) | offset_lo.
698        // We expect CarryOverExtractor to produce 0 packets on frame1 (partial tail),
699        // then 1 on frame2 (187-byte completion across the boundary).
700        let frame1_data = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
701        let frame2_data = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
702        let hdr1 = make_hem_header(0, (frame1_data.len() * 8) as u16);
703        let hdr2 = make_hem_header(0, (frame2_data.len() * 8) as u16);
704
705        let mut extractor = CarryOverExtractor::new();
706        let packets1 = extractor.feed_hem(&hdr1, &frame1_data, false);
707        assert_eq!(packets1.len(), 0, "70 bytes (< 187) must not yet emit a UP");
708
709        let packets2 = extractor.feed_hem(&hdr2, &frame2_data, false);
710        assert!(!packets2.is_empty(), "boundary UP should complete");
711        assert_eq!(
712            packets2[0][0], 0x47,
713            "first emitted packet has sync byte prepended"
714        );
715    }
716
717    #[test]
718    fn carry_over_hem_completion_success_path() {
719        // Exercises the carry-over COMPLETION path (feed_hem_into success branch
720        // `syncd_bytes == need`): frame2's syncd must point exactly past the bytes
721        // that finish frame1's partial UP. The sibling test above uses syncd=0, so
722        // it hits the discard branch instead — this one covers the success branch.
723        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
724            let mut h = [0u8; 10];
725            h[0] = 0xF0; // TS, SIS, CCM (HEM via byte-9 MODE xor)
726            h[4] = (dfl_bits >> 8) as u8;
727            h[5] = (dfl_bits & 0xFF) as u8;
728            h[7] = (syncd_bits >> 8) as u8;
729            h[8] = (syncd_bits & 0xFF) as u8;
730            h[9] = crate::crc::crc8(&h[..9]) ^ 1; // MODE=1 (HEM)
731            h
732        };
733
734        // Frame1: 70-byte partial UP → after it, extractor.pos = 1 + 70 = 71.
735        let frame1: Vec<u8> = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect();
736        // need = HEM_UP_SIZE(187) + 1 - pos(71) = 117. syncd must equal `need`.
737        let need = 117usize;
738        // Frame2: `need` completion bytes, then one fresh 187-byte UP.
739        let frame2: Vec<u8> = (0..(need + HEM_UP_SIZE) as u16)
740            .map(|i| 0xB0 | (i & 0x0F) as u8)
741            .collect();
742        let h1 = make_hem_header(0, (frame1.len() * 8) as u16);
743        let h2 = make_hem_header((need * 8) as u16, (frame2.len() * 8) as u16);
744
745        let mut ex = CarryOverExtractor::new();
746        let p1 = ex.feed_hem(&h1, &frame1, false);
747        assert_eq!(p1.len(), 0, "frame1's 70-byte partial emits nothing yet");
748
749        let p2 = ex.feed_hem(&h2, &frame2, false);
750        assert_eq!(
751            ex.stats().partial_discards,
752            0,
753            "completion path must be taken, NOT the discard branch"
754        );
755        assert_eq!(p2.len(), 2, "completed boundary UP + one fresh UP");
756        // Completed UP = sync + frame1's 70 carried bytes + frame2's 117 completion bytes.
757        assert_eq!(p2[0][0], TS_SYNC_BYTE);
758        assert_eq!(&p2[0][1..71], &frame1[..]);
759        assert_eq!(&p2[0][71..188], &frame2[..need]);
760        // Fresh UP = sync + frame2[need..need+187].
761        assert_eq!(p2[1][0], TS_SYNC_BYTE);
762        assert_eq!(&p2[1][1..188], &frame2[need..need + HEM_UP_SIZE]);
763    }
764
765    #[test]
766    fn feed_into_matches_allocating_api() {
767        // Run the same two-frame HEM sequence (partial UP carried across the
768        // boundary) through the allocating `feed_hem` and the buffer-reusing
769        // `feed_hem_into` (one Vec reused across both frames). Identical output
770        // proves `_into` clears + appends equivalently — if it failed to clear,
771        // frame 2's buffer would still hold frame 1's packets and diverge.
772        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
773            let mut h = [0u8; 10];
774            h[0] = 0xF0;
775            h[4] = (dfl_bits >> 8) as u8;
776            h[5] = (dfl_bits & 0xFF) as u8;
777            h[7] = (syncd_bits >> 8) as u8;
778            h[8] = (syncd_bits & 0xFF) as u8;
779            let crc = crate::crc::crc8(&h[..9]);
780            h[9] = crc ^ 1;
781            h
782        };
783        let f1 = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
784        let f2 = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
785        let h1 = make_hem_header(0, (f1.len() * 8) as u16);
786        let h2 = make_hem_header(0, (f2.len() * 8) as u16);
787
788        let mut alloc = CarryOverExtractor::new();
789        let a1 = alloc.feed_hem(&h1, &f1, false);
790        let a2 = alloc.feed_hem(&h2, &f2, false);
791
792        let mut reuse = CarryOverExtractor::new();
793        let mut buf = Vec::new();
794        reuse.feed_hem_into(&h1, &f1, false, &mut buf);
795        let b1 = buf.clone();
796        reuse.feed_hem_into(&h2, &f2, false, &mut buf);
797        let b2 = buf.clone();
798
799        assert_eq!(a1, b1, "frame 1 output matches across APIs");
800        assert_eq!(
801            a2, b2,
802            "frame 2 (carry-over) output matches; buffer was cleared"
803        );
804    }
805
806    #[test]
807    fn remaining_safe_when_pos_equals_len() {
808        let data = vec![0xAA; NM_UP_SIZE];
809        let mut iter = NmTsIter::new(&data);
810        let _p = iter.next().unwrap();
811        // pos == data.len() — must not panic
812        let remaining = iter.remaining();
813        assert!(remaining.is_empty());
814    }
815
816    #[test]
817    fn remaining_safe_when_pos_exceeds_len() {
818        // Construct an iterator and manually set pos beyond data length.
819        // This cannot happen through normal iteration, but the safe
820        // get().unwrap_or(&[]) handles it gracefully.
821        let data = vec![0xAA; 10];
822        let iter = NmTsIter {
823            data: &data,
824            pos: 20,
825        };
826        let remaining = iter.remaining();
827        assert!(remaining.is_empty());
828    }
829
830    /// Build a serialised HEM BBHEADER with given syncd (bits) and dfl (bits).
831    fn make_hem_hdr_bytes(syncd_bits: u16, dfl_bits: u16) -> [u8; 10] {
832        let hdr = Bbheader {
833            matype: Matype {
834                ts_gs: TsGs::Ts,
835                sis: true,
836                ccm: true,
837                issyi: false,
838                npd: false,
839                ext: 0,
840                isi: 0,
841            },
842            upl: 0,
843            sync: 0,
844            dfl: dfl_bits,
845            syncd: syncd_bits,
846            mode: crate::header::Mode::HighEfficiency,
847            issy_in_header: None,
848        };
849        hdr.serialize()
850    }
851
852    #[test]
853    fn syncd_65535_hem_continues_carry_over_without_partial_discard() {
854        // BUG 2 regression: SYNCD=65535 means "no UP starts in this DATA FIELD" —
855        // the entire data field is a continuation of the carried-over partial UP.
856        // The old code computed syncd_bytes = 65535/8 = 8191, which never matched
857        // `need`, and took the discard branch (partial_discards++).
858        //
859        // Test sequence (HEM, stride=187):
860        //   Frame A: data field = first 100 bytes of a 187-byte UP.
861        //            SYNCD=0 (UP starts at byte 0).  Extractor must carry 100 bytes.
862        //   Frame B: data field = next 87 bytes of the SAME UP.
863        //            SYNCD=0xFFFF (no new UP starts).  Must APPEND to partial, emit UP.
864        //   No partial_discards should occur.
865
866        // Build recognisable UP content: bytes 0..186 = 0x00..0xBA (distinct from sync)
867        let up_payload: Vec<u8> = (0u8..187).collect(); // 187 bytes
868
869        // Frame A: send the first 100 bytes; DFL = 100*8 bits; SYNCD=0.
870        let frame_a_data: Vec<u8> = up_payload[..100].to_vec();
871        let hdr_a = make_hem_hdr_bytes(0, (frame_a_data.len() * 8) as u16);
872
873        // Frame B: send the remaining 87 bytes; SYNCD=0xFFFF (no new UP starts).
874        // DFL = 87*8 bits.
875        let frame_b_data: Vec<u8> = up_payload[100..].to_vec();
876        let hdr_b = make_hem_hdr_bytes(0xFFFF, (frame_b_data.len() * 8) as u16);
877
878        let mut extractor = CarryOverExtractor::new();
879
880        let pkts_a = extractor.feed_hem(&hdr_a, &frame_a_data, false);
881        assert_eq!(
882            pkts_a.len(),
883            0,
884            "frame A: 100 bytes < 187, must not emit yet"
885        );
886
887        let pkts_b = extractor.feed_hem(&hdr_b, &frame_b_data, false);
888        assert_eq!(
889            extractor.stats().partial_discards,
890            0,
891            "SYNCD=0xFFFF must NOT trigger a partial discard"
892        );
893        assert_eq!(
894            pkts_b.len(),
895            1,
896            "SYNCD=0xFFFF: the UP must complete and be emitted"
897        );
898        assert_eq!(pkts_b[0][0], TS_SYNC_BYTE, "sync byte prepended correctly");
899        // Verify the 187 payload bytes are correct: [sync][up_payload[0..187]].
900        assert_eq!(
901            &pkts_b[0][1..],
902            up_payload.as_slice(),
903            "completed UP must contain the exact original 187-byte payload"
904        );
905    }
906
907    #[test]
908    fn stats_count_npd_skip_and_mode_mismatch() {
909        let mut ext = CarryOverExtractor::new();
910        let mut out = Vec::new();
911
912        // Valid HEM header but NPD set: unsupported → no output, counted as a
913        // dropped-valid-data event (not wire corruption).
914        let hem = make_hem_header(true).serialize();
915        ext.feed_hem_into(&hem, &[0u8; NM_UP_SIZE], true, &mut out);
916        assert!(out.is_empty());
917        assert_eq!(ext.stats().npd_unsupported, 1);
918
919        // NM header fed to the HEM path → mode mismatch, counted.
920        let nm = make_nm_header(0).serialize();
921        ext.feed_hem_into(&nm, &[0u8; NM_UP_SIZE], false, &mut out);
922        assert!(out.is_empty());
923        assert_eq!(ext.stats().mode_mismatches, 1);
924        // Earlier counter is unchanged.
925        assert_eq!(ext.stats().npd_unsupported, 1);
926    }
927}