1use alloc::vec::Vec;
24
25use crate::header::{Bbheader, Mode, BBHEADER_LEN};
26
27pub const NM_UP_SIZE: usize = 188;
29
30pub const HEM_UP_SIZE: usize = 187;
32
33pub const TS_SYNC_BYTE: u8 = 0x47;
35
36#[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 pub fn new(data: &'a [u8]) -> Self {
53 Self { data, pos: 0 }
54 }
55
56 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; 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#[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 pub fn new(data: &'a [u8], npd: bool) -> Self {
102 Self { data, pos: 0, npd }
103 }
104
105 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#[derive(Clone, Copy)]
139#[non_exhaustive]
140pub enum UpIter<'a> {
141 Normal(NmTsIter<'a>),
143 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
165pub 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
177pub struct CarryOverExtractor {
186 buf: [u8; NM_UP_SIZE],
188 pos: usize,
190 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
213#[non_exhaustive]
214pub struct CarryOverStats {
215 pub npd_unsupported: u64,
220 pub header_parse_failures: u64,
222 pub mode_mismatches: u64,
225 pub partial_discards: u64,
228}
229
230impl CarryOverExtractor {
231 pub fn new() -> Self {
233 Self::default()
234 }
235
236 #[must_use]
240 pub fn stats(&self) -> CarryOverStats {
241 self.stats
242 }
243
244 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 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 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 if hdr.mode != Mode::HighEfficiency {
288 self.stats.mode_mismatches += 1;
289 return;
290 }
291
292 let stride = HEM_UP_SIZE;
293 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 if self.pos > 0 {
308 let space = stride + 1 - self.pos; 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 self.buf[0] = TS_SYNC_BYTE;
315 out.push(self.buf);
316 self.pos = 0;
317 }
318 }
319 return;
323 }
324
325 if self.pos > 0 {
327 let need = stride + 1 - self.pos; 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 self.stats.partial_discards += 1;
336 self.pos = 0;
337 }
338 }
339
340 let mut i = syncd_bytes;
342 while i + stride <= data.len() {
343 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 if i < data.len() {
352 let tail = (data.len() - i).min(stride);
353 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 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 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 if hdr.mode != Mode::Normal {
392 self.stats.mode_mismatches += 1;
393 return;
394 }
395
396 let stride = NM_UP_SIZE;
397 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 if self.pos > 0 {
412 let space = stride - self.pos; 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 self.buf[0] = TS_SYNC_BYTE; out.push(self.buf);
420 self.pos = 0;
421 }
422 }
423 return;
424 }
425
426 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; out.push(self.buf);
433 self.pos = 0;
434 } else {
435 self.stats.partial_discards += 1;
437 self.pos = 0;
438 }
439 }
440
441 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; out.push(self.buf);
447 i += stride;
448 }
449
450 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; 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); 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; 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); }
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; data[1] = 0x47; 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); assert_eq!(pkt[1], 0x47); 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 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); }
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 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]; 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 let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
679 let mut h = [0u8; 10];
680 h[0] = 0xF0;
682 h[1] = 0x00; 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; h[7] = (syncd_bits >> 8) as u8;
690 h[8] = (syncd_bits & 0xFF) as u8;
691 let crc = crate::crc::crc8(&h[..9]);
693 h[9] = crc ^ 1;
694 h
695 };
696
697 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 let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
724 let mut h = [0u8; 10];
725 h[0] = 0xF0; 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; h
732 };
733
734 let frame1: Vec<u8> = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect();
736 let need = 117usize;
738 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 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 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 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 let remaining = iter.remaining();
813 assert!(remaining.is_empty());
814 }
815
816 #[test]
817 fn remaining_safe_when_pos_exceeds_len() {
818 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 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 let up_payload: Vec<u8> = (0u8..187).collect(); 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 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 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 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 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 assert_eq!(ext.stats().npd_unsupported, 1);
926 }
927}