1use crate::header::{Bbheader, Mode, BBHEADER_LEN};
24
25pub const NM_UP_SIZE: usize = 188;
27
28pub const HEM_UP_SIZE: usize = 187;
30
31pub const TS_SYNC_BYTE: u8 = 0x47;
33
34#[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 pub fn new(data: &'a [u8]) -> Self {
51 Self { data, pos: 0 }
52 }
53
54 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; 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#[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 pub fn new(data: &'a [u8], npd: bool) -> Self {
100 Self { data, pos: 0, npd }
101 }
102
103 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#[derive(Clone, Copy)]
137pub enum UpIter<'a> {
138 Normal(NmTsIter<'a>),
140 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
162pub 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
174pub struct CarryOverExtractor {
183 buf: [u8; NM_UP_SIZE],
185 pos: usize,
187 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
210#[non_exhaustive]
211pub struct CarryOverStats {
212 pub npd_unsupported: u64,
217 pub header_parse_failures: u64,
219 pub mode_mismatches: u64,
222 pub partial_discards: u64,
225}
226
227impl CarryOverExtractor {
228 pub fn new() -> Self {
230 Self::default()
231 }
232
233 #[must_use]
237 pub fn stats(&self) -> CarryOverStats {
238 self.stats
239 }
240
241 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 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 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 if hdr.mode != Mode::HighEfficiency {
285 self.stats.mode_mismatches += 1;
286 return;
287 }
288
289 let stride = HEM_UP_SIZE;
290 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 if self.pos > 0 {
305 let space = stride + 1 - self.pos; 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 self.buf[0] = TS_SYNC_BYTE;
312 out.push(self.buf);
313 self.pos = 0;
314 }
315 }
316 return;
320 }
321
322 if self.pos > 0 {
324 let need = stride + 1 - self.pos; 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 self.stats.partial_discards += 1;
333 self.pos = 0;
334 }
335 }
336
337 let mut i = syncd_bytes;
339 while i + stride <= data.len() {
340 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 if i < data.len() {
349 let tail = (data.len() - i).min(stride);
350 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 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 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 if hdr.mode != Mode::Normal {
389 self.stats.mode_mismatches += 1;
390 return;
391 }
392
393 let stride = NM_UP_SIZE;
394 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 if self.pos > 0 {
409 let space = stride - self.pos; 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 self.buf[0] = TS_SYNC_BYTE; out.push(self.buf);
417 self.pos = 0;
418 }
419 }
420 return;
421 }
422
423 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; out.push(self.buf);
430 self.pos = 0;
431 } else {
432 self.stats.partial_discards += 1;
434 self.pos = 0;
435 }
436 }
437
438 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; out.push(self.buf);
444 i += stride;
445 }
446
447 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; 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); 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; 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); }
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; data[1] = 0x47; 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); assert_eq!(pkt[1], 0x47); 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 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); }
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 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]; 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 let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
676 let mut h = [0u8; 10];
677 h[0] = 0xF0;
679 h[1] = 0x00; 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; h[7] = (syncd_bits >> 8) as u8;
687 h[8] = (syncd_bits & 0xFF) as u8;
688 let crc = crate::crc::crc8(&h[..9]);
690 h[9] = crc ^ 1;
691 h
692 };
693
694 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 let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
721 let mut h = [0u8; 10];
722 h[0] = 0xF0; 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; h
729 };
730
731 let frame1: Vec<u8> = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect();
733 let need = 117usize;
735 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 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 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 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 let remaining = iter.remaining();
810 assert!(remaining.is_empty());
811 }
812
813 #[test]
814 fn remaining_safe_when_pos_exceeds_len() {
815 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 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 let up_payload: Vec<u8> = (0u8..187).collect(); 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 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 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 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 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 assert_eq!(ext.stats().npd_unsupported, 1);
923 }
924}