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