1use alloc::vec::Vec;
10use core::time::Duration;
11
12use crate::pid::{Pid, well_known};
13use crate::ts::{CC_MASK, TS_PACKET_SIZE, TsHeader};
14
15const PUSI_PAYLOAD_CAP: usize = 183;
17const PAYLOAD_CAP: usize = 184;
19use crate::ts::STUFFING_BYTE;
20
21pub struct SectionPacketiser {
30 pid: Pid,
31 continuity_counter: u8,
32}
33
34impl SectionPacketiser {
35 pub fn new(pid: Pid) -> Self {
37 Self {
38 pid,
39 continuity_counter: 0,
40 }
41 }
42
43 pub fn with_continuity(pid: Pid, cc: u8) -> Self {
45 Self {
46 pid,
47 continuity_counter: cc & CC_MASK,
48 }
49 }
50
51 pub fn pid(&self) -> Pid {
53 self.pid
54 }
55
56 pub fn continuity_counter(&self) -> u8 {
58 self.continuity_counter
59 }
60
61 pub fn packetise_into(
66 &mut self,
67 sections: &[&[u8]],
68 out: &mut Vec<[u8; TS_PACKET_SIZE]>,
69 ) -> usize {
70 out.clear();
71
72 if sections.is_empty() {
73 return 0;
74 }
75
76 let total_len: usize = sections.iter().map(|s| s.len()).sum();
78 if total_len == 0 {
79 return 0;
80 }
81 let mut data = Vec::with_capacity(total_len);
82 let mut starts = Vec::with_capacity(sections.len());
83 for s in sections {
84 starts.push(data.len());
85 data.extend_from_slice(s);
86 }
87
88 let count_before = out.len();
89 let mut pos = 0usize;
90 let mut next_idx = 0usize;
95
96 while pos < data.len() {
97 while starts.get(next_idx).is_some_and(|&s| s < pos) {
98 next_idx += 1;
99 }
100 let next_start = starts.get(next_idx).copied();
101
102 let pusi: bool;
103 let pointer_field: u8;
104 let cap: usize;
105
106 if let Some(ns) = next_start {
107 let diff = ns.saturating_sub(pos);
108 if diff < PUSI_PAYLOAD_CAP {
119 pusi = true;
120 pointer_field = diff as u8;
121 cap = PUSI_PAYLOAD_CAP;
122 } else {
123 pusi = false;
124 pointer_field = 0;
125 cap = PAYLOAD_CAP.min(diff);
126 }
127 } else {
128 pusi = false;
129 pointer_field = 0;
130 cap = PAYLOAD_CAP;
131 }
132
133 let mut pkt = [0u8; TS_PACKET_SIZE];
134
135 let header = TsHeader {
136 tei: false,
137 pusi,
138 pid: self.pid,
139 scrambling: 0,
140 has_adaptation: false,
141 has_payload: true,
142 continuity_counter: self.continuity_counter,
143 };
144 header
145 .serialize_into(&mut pkt[..4])
146 .expect("4-byte header buffer");
147
148 self.continuity_counter = (self.continuity_counter + 1) & CC_MASK;
149
150 let mut write_pos = 4usize;
151
152 if pusi {
153 pkt[write_pos] = pointer_field;
154 write_pos += 1;
155 }
156
157 let remaining = data.len() - pos;
158 let to_copy = remaining.min(cap);
159 pkt[write_pos..write_pos + to_copy].copy_from_slice(&data[pos..pos + to_copy]);
160 pos += to_copy;
161 write_pos += to_copy;
162
163 for b in &mut pkt[write_pos..] {
165 *b = STUFFING_BYTE;
166 }
167
168 out.push(pkt);
169 }
170
171 out.len() - count_before
172 }
173
174 pub fn packetise(&mut self, sections: &[&[u8]]) -> Vec<[u8; TS_PACKET_SIZE]> {
176 let mut out = Vec::new();
177 self.packetise_into(sections, &mut out);
178 out
179 }
180}
181
182pub const NIT_MAX_INTERVAL: Duration = Duration::from_secs(10);
186pub const BAT_MAX_INTERVAL: Duration = Duration::from_secs(10);
188pub const SDT_ACTUAL_MAX_INTERVAL: Duration = Duration::from_secs(2);
190pub const SDT_OTHER_MAX_INTERVAL: Duration = Duration::from_secs(10);
192pub const EIT_PF_ACTUAL_MAX_INTERVAL: Duration = Duration::from_secs(2);
194pub const EIT_PF_OTHER_MAX_INTERVAL: Duration = Duration::from_secs(10);
197pub const EIT_SCHED_MAX_INTERVAL: Duration = Duration::from_secs(10);
199pub const EIT_SCHED_EXT_MAX_INTERVAL: Duration = Duration::from_secs(30);
201pub const TDT_MAX_INTERVAL: Duration = Duration::from_secs(30);
203pub const TOT_MAX_INTERVAL: Duration = Duration::from_secs(30);
205
206pub const PAT_MAX_INTERVAL: Duration = Duration::from_millis(100);
208pub const PMT_MAX_INTERVAL: Duration = Duration::from_millis(100);
210
211pub const MIN_SECTION_INTERVAL: Duration = Duration::from_millis(25);
214
215pub struct SiMux {
244 entries: Vec<Entry>,
245 packetisers: Vec<SectionPacketiser>,
247}
248
249struct Entry {
250 pid: Pid,
251 table_id: u8,
252 sections: Vec<u8>,
253 interval: Duration,
254 last_emit: Option<Duration>,
255}
256
257fn table_id_of(sections: &[u8]) -> u8 {
263 sections.first().copied().unwrap_or(0)
264}
265
266fn packetiser_for(packetisers: &mut Vec<SectionPacketiser>, pid: Pid) -> &mut SectionPacketiser {
270 if let Some(idx) = packetisers.iter().position(|p| p.pid() == pid) {
271 &mut packetisers[idx]
272 } else {
273 packetisers.push(SectionPacketiser::new(pid));
274 packetisers.last_mut().expect("just pushed")
275 }
276}
277
278impl SiMux {
279 pub fn new() -> Self {
281 Self {
282 entries: Vec::new(),
283 packetisers: Vec::new(),
284 }
285 }
286
287 pub fn upsert(&mut self, pid: Pid, sections: Vec<u8>, interval: Duration) {
301 debug_assert!(
302 interval >= MIN_SECTION_INTERVAL,
303 "interval {interval:?} is below the 25 ms minimum (EN 300 468 §5.1.4.1)"
304 );
305
306 let table_id = table_id_of(§ions);
307 if let Some(entry) = self
308 .entries
309 .iter_mut()
310 .find(|e| e.pid == pid && e.table_id == table_id)
311 {
312 entry.sections = sections;
313 entry.interval = interval;
314 } else {
315 self.entries.push(Entry {
316 pid,
317 table_id,
318 sections,
319 interval,
320 last_emit: None,
321 });
322 }
323 }
324
325 pub fn upsert_pat(&mut self, sections: Vec<u8>) {
327 self.upsert(well_known::PAT, sections, PAT_MAX_INTERVAL);
328 }
329
330 pub fn upsert_pmt(&mut self, pid: Pid, sections: Vec<u8>) {
334 self.upsert(pid, sections, PMT_MAX_INTERVAL);
335 }
336
337 pub fn upsert_sdt_actual(&mut self, sections: Vec<u8>) {
341 self.upsert(well_known::SDT_BAT, sections, SDT_ACTUAL_MAX_INTERVAL);
342 }
343
344 pub fn upsert_nit(&mut self, sections: Vec<u8>) {
346 self.upsert(well_known::NIT, sections, NIT_MAX_INTERVAL);
347 }
348
349 pub fn upsert_tdt(&mut self, sections: Vec<u8>) {
353 self.upsert(well_known::TDT_TOT, sections, TDT_MAX_INTERVAL);
354 }
355
356 pub fn upsert_tot(&mut self, sections: Vec<u8>) {
360 self.upsert(well_known::TDT_TOT, sections, TOT_MAX_INTERVAL);
361 }
362
363 pub fn poll_into(&mut self, now: Duration, out: &mut Vec<[u8; TS_PACKET_SIZE]>) -> usize {
370 out.clear();
371 let before = out.len();
372
373 let mut tmp = Vec::new();
374 let packetisers = &mut self.packetisers;
375 for entry in &mut self.entries {
376 let due = match entry.last_emit {
377 None => true,
378 Some(last) => now.saturating_sub(last) >= entry.interval,
379 };
380 if due {
381 let refs = split_sections(&entry.sections);
382 if !refs.is_empty() {
383 let packetiser = packetiser_for(packetisers, entry.pid);
384 packetiser.packetise_into(&refs, &mut tmp);
385 out.append(&mut tmp);
386 }
387 entry.last_emit = Some(now);
388 }
389 }
390
391 out.len() - before
392 }
393
394 pub fn poll(&mut self, now: Duration) -> Vec<[u8; TS_PACKET_SIZE]> {
396 let mut out = Vec::new();
397 self.poll_into(now, &mut out);
398 out
399 }
400}
401
402impl Default for SiMux {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408fn split_sections(data: &[u8]) -> Vec<&[u8]> {
414 let mut result = Vec::new();
415 let mut pos = 0;
416 while let Some(end) = crate::ts::section_total_len(&data[pos..]).map(|len| pos + len) {
417 if end > data.len() {
418 break;
419 }
420 result.push(&data[pos..end]);
421 pos = end;
422 }
423 result
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use crate::ts::{SectionReassembler, TsPacket};
430 use alloc::vec;
431 use alloc::vec::Vec;
432
433 fn concat_sections(sections: &[Vec<u8>]) -> Vec<u8> {
436 let total: usize = sections.iter().map(|s| s.len()).sum();
437 let mut out = Vec::with_capacity(total);
438 for s in sections {
439 out.extend_from_slice(s);
440 }
441 out
442 }
443
444 fn assert_round_trip(sections: &[Vec<u8>]) {
447 let mut packetiser = SectionPacketiser::new(Pid::new(0x0100));
448 let refs: Vec<&[u8]> = sections.iter().map(|s| s.as_slice()).collect();
449 let packets = packetiser.packetise(&refs);
450
451 let mut reasm = SectionReassembler::default();
452 for pkt_raw in &packets {
453 let pkt = TsPacket::parse(pkt_raw).expect("parse generated packet");
454 let payload = pkt.payload.expect("payload present");
455 let pusi = pkt.header.pusi;
456 reasm.feed(payload, pusi);
457 }
458
459 let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
460 assert_eq!(
461 got.len(),
462 sections.len(),
463 "section count mismatch: expected {}, got {}",
464 sections.len(),
465 got.len()
466 );
467 for (i, (orig, round)) in sections.iter().zip(got.iter()).enumerate() {
468 assert_eq!(
469 round.as_ref(),
470 orig.as_slice(),
471 "section {i} round-trip mismatch"
472 );
473 }
474 assert!(reasm.is_empty(), "reassembler should be empty after drain");
475 }
476
477 #[test]
483 fn many_sections_with_boundary_straddling_sizes_round_trip() {
484 let sections: Vec<Vec<u8>> = (0..400usize)
485 .map(|n| {
486 let body: Vec<u8> = (0..n).map(|i| (i % 251) as u8).collect();
487 test_fixtures::psi::short_section((n % 200) as u8, &body)
488 })
489 .collect();
490 assert_round_trip(§ions);
491 }
492
493 #[test]
496 fn split_single_section() {
497 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 5]);
498 let refs = split_sections(&s);
499 assert_eq!(refs.len(), 1);
500 assert_eq!(refs[0], s.as_slice());
501 }
502
503 #[test]
504 fn split_two_sections() {
505 let s1 = test_fixtures::psi::short_section(0x42, &[0x01, 0x02]);
506 let s2 = test_fixtures::psi::short_section(0x46, &[0x03, 0x04, 0x05]);
507 let both = concat_sections(&[s1.clone(), s2.clone()]);
508 let refs = split_sections(&both);
509 assert_eq!(refs.len(), 2);
510 assert_eq!(refs[0], s1.as_slice());
511 assert_eq!(refs[1], s2.as_slice());
512 }
513
514 #[test]
515 fn split_empty_input() {
516 let refs = split_sections(&[]);
517 assert!(refs.is_empty());
518 }
519
520 #[test]
521 fn split_trailing_garbage_ignored() {
522 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 3]);
523 let mut data = s.clone();
524 data.push(0xFF); let refs = split_sections(&data);
526 assert_eq!(refs.len(), 1);
527 assert_eq!(refs[0], s.as_slice());
528 }
529
530 #[test]
533 fn interval_constants_match_spec() {
534 assert_eq!(NIT_MAX_INTERVAL, Duration::from_secs(10));
536 assert_eq!(BAT_MAX_INTERVAL, Duration::from_secs(10));
537 assert_eq!(SDT_ACTUAL_MAX_INTERVAL, Duration::from_secs(2));
538 assert_eq!(SDT_OTHER_MAX_INTERVAL, Duration::from_secs(10));
539 assert_eq!(EIT_PF_ACTUAL_MAX_INTERVAL, Duration::from_secs(2));
540 assert_eq!(EIT_PF_OTHER_MAX_INTERVAL, Duration::from_secs(10));
541 assert_eq!(EIT_SCHED_MAX_INTERVAL, Duration::from_secs(10));
542 assert_eq!(EIT_SCHED_EXT_MAX_INTERVAL, Duration::from_secs(30));
543 assert_eq!(TDT_MAX_INTERVAL, Duration::from_secs(30));
544 assert_eq!(TOT_MAX_INTERVAL, Duration::from_secs(30));
545
546 assert_eq!(PAT_MAX_INTERVAL, Duration::from_millis(100));
548 assert_eq!(PMT_MAX_INTERVAL, Duration::from_millis(100));
549
550 assert_eq!(MIN_SECTION_INTERVAL, Duration::from_millis(25));
552 }
553
554 #[test]
557 fn new_and_default_simux_are_empty() {
558 for mut mux in [SiMux::new(), SiMux::default()] {
559 let pkts = mux.poll(Duration::ZERO);
560 assert!(pkts.is_empty());
561 }
562 }
563
564 #[test]
565 fn first_poll_always_emits() {
566 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 10]);
567 let mut mux = SiMux::new();
568 mux.upsert(Pid::new(0x0100), s.clone(), Duration::from_millis(100));
569 let pkts = mux.poll(Duration::ZERO);
570 assert!(!pkts.is_empty(), "first poll must emit");
571 }
572
573 #[test]
574 fn first_poll_emits_all_entries() {
575 let s1 = test_fixtures::psi::short_section(0x42, &[0x01; 5]);
576 let s2 = test_fixtures::psi::short_section(0x46, &[0x02; 5]);
577 let mut mux = SiMux::new();
578 mux.upsert(Pid::new(0x0100), s1.clone(), Duration::from_millis(100));
579 mux.upsert(Pid::new(0x0200), s2.clone(), Duration::from_millis(200));
580 let pkts = mux.poll(Duration::ZERO);
581 let pids: Vec<Pid> = pkts
583 .iter()
584 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
585 .collect();
586 assert!(pids.contains(&Pid::new(0x0100)));
587 assert!(pids.contains(&Pid::new(0x0200)));
588 }
589
590 #[test]
593 fn entry_emits_only_when_due() {
594 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 10]);
595 let mut mux = SiMux::new();
596 mux.upsert(Pid::new(0x0100), s.clone(), Duration::from_millis(100));
597
598 let pkts0 = mux.poll(Duration::ZERO);
600 assert!(!pkts0.is_empty());
601
602 let pkts50 = mux.poll(Duration::from_millis(50));
604 assert!(pkts50.is_empty(), "should not emit at t=50");
605
606 let pkts99 = mux.poll(Duration::from_millis(99));
608 assert!(pkts99.is_empty(), "should not emit at t=99");
609
610 let pkts100 = mux.poll(Duration::from_millis(100));
612 assert!(!pkts100.is_empty(), "should emit at t=100");
613 }
614
615 #[test]
616 fn two_entries_different_cadence() {
617 let s1 = test_fixtures::psi::short_section(0x42, &[0x01; 5]);
618 let s2 = test_fixtures::psi::short_section(0x46, &[0x02; 5]);
619 let mut mux = SiMux::new();
620 mux.upsert(
621 Pid::new(0x0100),
622 concat_sections(&[s1]),
623 Duration::from_millis(100),
624 );
625 mux.upsert(
626 Pid::new(0x0200),
627 concat_sections(&[s2]),
628 Duration::from_millis(200),
629 );
630
631 let pkts = mux.poll(Duration::ZERO);
633 assert!(!pkts.is_empty());
634
635 let pkts100 = mux.poll(Duration::from_millis(100));
637 assert!(!pkts100.is_empty());
638 let pids100: Vec<Pid> = pkts100
639 .iter()
640 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
641 .collect();
642 assert!(
643 pids100.contains(&Pid::new(0x0100)),
644 "PID 0x0100 should emit at t=100"
645 );
646 assert!(
647 !pids100.contains(&Pid::new(0x0200)),
648 "PID 0x0200 should NOT emit at t=100"
649 );
650
651 let pkts150 = mux.poll(Duration::from_millis(150));
653 assert!(pkts150.is_empty(), "neither should emit at t=150");
654
655 let pkts200 = mux.poll(Duration::from_millis(200));
657 let pids200: Vec<Pid> = pkts200
658 .iter()
659 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
660 .collect();
661 assert!(
662 pids200.contains(&Pid::new(0x0100)),
663 "PID 0x0100 should emit at t=200"
664 );
665 assert!(
666 pids200.contains(&Pid::new(0x0200)),
667 "PID 0x0200 should emit at t=200"
668 );
669 }
670
671 #[test]
672 fn entry_emits_again_after_full_interval() {
673 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 20]);
674 let mut mux = SiMux::new();
675 mux.upsert(Pid::new(0x0100), s, Duration::from_secs(1));
676
677 let n0 = mux.poll_into(Duration::ZERO, &mut Vec::new());
679 assert!(n0 > 0);
680
681 let mut buf = Vec::new();
683 let n999 = mux.poll_into(Duration::from_millis(999), &mut buf);
684 assert_eq!(n999, 0);
685
686 let mut buf2 = Vec::new();
688 let n1000 = mux.poll_into(Duration::from_millis(1000), &mut buf2);
689 assert!(n1000 > 0);
690 }
691
692 #[test]
695 fn upsert_updates_existing_entry() {
696 let s1 = test_fixtures::psi::short_section(0x42, &[0xAA; 5]);
699 let s2 = test_fixtures::psi::short_section(0x42, &[0xBB; 10]);
700 let mut mux = SiMux::new();
701
702 mux.upsert(Pid::new(0x0100), s1, Duration::from_millis(100));
703 mux.upsert(Pid::new(0x0100), s2.clone(), Duration::from_millis(200));
704
705 let pkts = mux.poll(Duration::ZERO);
706
707 let mut reasm = SectionReassembler::default();
709 for raw in &pkts {
710 let pkt = TsPacket::parse(raw).unwrap();
711 if pkt.header.pid == Pid::new(0x0100) {
712 reasm.feed(pkt.payload.unwrap(), pkt.header.pusi);
713 }
714 }
715 let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
716 assert_eq!(got.len(), 1);
717 assert_eq!(got[0].as_ref(), s2.as_slice());
718 }
719
720 #[test]
727 fn upsert_tdt_then_tot_both_emitted_with_continuous_cc() {
728 let tdt = test_fixtures::psi::short_section(0x70, &[0xAA; 5]); let tot = test_fixtures::psi::short_section(0x73, &[0xBB; 8]); let mut mux = SiMux::new();
731
732 mux.upsert_tdt(tdt.clone());
733 mux.upsert_tot(tot.clone());
734
735 let pkts = mux.poll(Duration::ZERO);
736 assert!(!pkts.is_empty());
737
738 let mut table_ids_seen = Vec::new();
739 let mut reasm = SectionReassembler::default();
740 let mut prev_cc: Option<u8> = None;
741 for raw in &pkts {
742 let pkt = TsPacket::parse(raw).unwrap();
743 assert_eq!(pkt.header.pid, well_known::TDT_TOT);
744 if let Some(prev) = prev_cc {
745 assert_eq!(
746 pkt.header.continuity_counter,
747 (prev + 1) & CC_MASK,
748 "continuity counter must be continuous across table kinds on one PID"
749 );
750 }
751 prev_cc = Some(pkt.header.continuity_counter);
752 reasm.feed(pkt.payload.unwrap(), pkt.header.pusi);
753 }
754 while let Some(section) = reasm.pop_section() {
755 table_ids_seen.push(section.as_ref()[0]);
756 }
757
758 assert!(table_ids_seen.contains(&0x70), "TDT missing from output");
759 assert!(table_ids_seen.contains(&0x73), "TOT missing from output");
760 }
761
762 #[test]
765 fn upsert_sdt_and_bat_both_emitted() {
766 let sdt = test_fixtures::psi::short_section(0x42, &[0xCC; 5]);
767 let bat = test_fixtures::psi::short_section(0x4A, &[0xDD; 6]);
768 let mut mux = SiMux::new();
769
770 mux.upsert_sdt_actual(sdt.clone());
771 mux.upsert(
772 well_known::SDT_BAT,
773 bat.clone(),
774 Duration::from_millis(1000),
775 );
776
777 let pkts = mux.poll(Duration::ZERO);
778 let mut table_ids_seen = Vec::new();
779 let mut reasm = SectionReassembler::default();
780 for raw in &pkts {
781 let pkt = TsPacket::parse(raw).unwrap();
782 reasm.feed(pkt.payload.unwrap(), pkt.header.pusi);
783 }
784 while let Some(section) = reasm.pop_section() {
785 table_ids_seen.push(section.as_ref()[0]);
786 }
787
788 assert!(table_ids_seen.contains(&0x42), "SDT actual missing");
789 assert!(table_ids_seen.contains(&0x4A), "BAT missing");
790 }
791
792 #[test]
795 fn upsert_sdt_actual_uses_correct_pid() {
796 let s = test_fixtures::psi::short_section(0x42, &[0x01]);
797 let mut mux = SiMux::new();
798 mux.upsert_sdt_actual(s);
799 let pkts = mux.poll(Duration::ZERO);
800 let pids: Vec<Pid> = pkts
801 .iter()
802 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
803 .collect();
804 assert!(pids.iter().all(|&p| p == well_known::SDT_BAT));
805 }
806
807 #[test]
808 fn upsert_pat_uses_correct_pid() {
809 let s = test_fixtures::psi::short_section(0x00, &[0x01, 0x02]);
810 let mut mux = SiMux::new();
811 mux.upsert_pat(s);
812 let pkts = mux.poll(Duration::ZERO);
813 assert!(!pkts.is_empty());
814 let pids: Vec<Pid> = pkts
815 .iter()
816 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
817 .collect();
818 assert!(pids.iter().all(|&p| p == well_known::PAT));
819 assert!(mux.poll(PAT_MAX_INTERVAL / 2).is_empty());
820 assert!(!mux.poll(PAT_MAX_INTERVAL).is_empty());
821 }
822
823 #[test]
824 fn upsert_nit_uses_correct_pid() {
825 let s = test_fixtures::psi::short_section(0x40, &[0x01]);
826 let mut mux = SiMux::new();
827 mux.upsert_nit(s);
828 let pkts = mux.poll(Duration::ZERO);
829 assert!(!pkts.is_empty());
830 let pids: Vec<Pid> = pkts
831 .iter()
832 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
833 .collect();
834 assert!(pids.iter().all(|&p| p == well_known::NIT));
835 assert!(mux.poll(NIT_MAX_INTERVAL / 2).is_empty());
836 assert!(!mux.poll(NIT_MAX_INTERVAL).is_empty());
837 }
838
839 #[test]
840 fn upsert_tdt_uses_correct_pid() {
841 let s = test_fixtures::psi::short_section(0x70, &[0x01]);
842 let mut mux = SiMux::new();
843 mux.upsert_tdt(s);
844 let pkts = mux.poll(Duration::ZERO);
845 let pids: Vec<Pid> = pkts
846 .iter()
847 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
848 .collect();
849 assert!(pids.iter().all(|&p| p == well_known::TDT_TOT));
850 }
851
852 #[test]
853 fn upsert_tot_uses_correct_pid() {
854 let s = test_fixtures::psi::short_section(0x73, &[0x01]);
855 let mut mux = SiMux::new();
856 mux.upsert_tot(s);
857 let pkts = mux.poll(Duration::ZERO);
858 let pids: Vec<Pid> = pkts
859 .iter()
860 .map(|raw| TsPacket::parse(raw).unwrap().header.pid)
861 .collect();
862 assert!(pids.iter().all(|&p| p == well_known::TDT_TOT));
863 }
864
865 #[test]
868 fn simux_round_trip_single_entry() {
869 let s1 = test_fixtures::psi::short_section(0x42, &[0xAA; 20]);
870 let s2 = test_fixtures::psi::short_section(0x46, &[0xBB; 15]);
871 let mut mux = SiMux::new();
872 mux.upsert(
873 Pid::new(0x0100),
874 concat_sections(&[s1.clone(), s2.clone()]),
875 Duration::from_millis(100),
876 );
877
878 let pkts = mux.poll(Duration::ZERO);
879
880 let mut reasm = SectionReassembler::default();
881 for raw in &pkts {
882 let pkt = TsPacket::parse(raw).unwrap();
883 if pkt.header.pid == Pid::new(0x0100) {
884 reasm.feed(pkt.payload.unwrap(), pkt.header.pusi);
885 }
886 }
887 let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
888 assert_eq!(got.len(), 2, "round-trip must recover both sections");
889 assert_eq!(got[0].as_ref(), s1.as_slice());
890 assert_eq!(got[1].as_ref(), s2.as_slice());
891 }
892
893 #[test]
894 fn simux_round_trip_multi_pid() {
895 let s_a = test_fixtures::psi::short_section(0x42, &[0xA0; 10]);
896 let s_b = test_fixtures::psi::short_section(0x46, &[0xB0; 10]);
897 let mut mux = SiMux::new();
898 mux.upsert(Pid::new(0x0100), s_a.clone(), Duration::from_millis(100));
899 mux.upsert(Pid::new(0x0200), s_b.clone(), Duration::from_millis(100));
900
901 let pkts = mux.poll(Duration::ZERO);
902
903 let mut reasm_a = SectionReassembler::default();
904 let mut reasm_b = SectionReassembler::default();
905 for raw in &pkts {
906 let pkt = TsPacket::parse(raw).unwrap();
907 match pkt.header.pid.value() {
908 0x0100 => reasm_a.feed(pkt.payload.unwrap(), pkt.header.pusi),
909 0x0200 => reasm_b.feed(pkt.payload.unwrap(), pkt.header.pusi),
910 _ => {}
911 }
912 }
913 let got_a: Vec<_> = core::iter::from_fn(|| reasm_a.pop_section()).collect();
914 let got_b: Vec<_> = core::iter::from_fn(|| reasm_b.pop_section()).collect();
915 assert_eq!(got_a.len(), 1);
916 assert_eq!(got_b.len(), 1);
917 assert_eq!(got_a[0].as_ref(), s_a.as_slice());
918 assert_eq!(got_b[0].as_ref(), s_b.as_slice());
919 }
920
921 #[test]
924 fn continuity_counter_continuous_across_polls() {
925 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 250]);
927 let mut mux = SiMux::new();
928 mux.upsert(Pid::new(0x0100), s, Duration::from_millis(100));
929
930 let pkts1 = mux.poll(Duration::ZERO);
931 assert!(pkts1.len() >= 2, "need ≥2 packets to test CC continuity");
932
933 let last_cc_pkts1 = TsPacket::parse(&pkts1[pkts1.len() - 1])
934 .unwrap()
935 .header
936 .continuity_counter;
937
938 let pkts2 = mux.poll(Duration::from_millis(100));
939 assert!(!pkts2.is_empty(), "second poll must emit");
940
941 let first_cc_pkts2 = TsPacket::parse(&pkts2[0])
942 .unwrap()
943 .header
944 .continuity_counter;
945
946 assert_eq!(
947 first_cc_pkts2,
948 (last_cc_pkts1 + 1) & 0x0F,
949 "CC must continue across poll cycles"
950 );
951 }
952
953 #[test]
956 fn poll_into_clears_out_before_appending() {
957 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 5]);
958 let mut mux = SiMux::new();
959 mux.upsert(Pid::new(0x0100), s, Duration::from_millis(100));
960
961 let mut out = vec![[0u8; TS_PACKET_SIZE]; 42];
962 let n = mux.poll_into(Duration::ZERO, &mut out);
963 assert_eq!(n, out.len(), "out must contain only new packets");
964 }
965
966 #[test]
969 #[cfg(debug_assertions)]
970 #[should_panic(expected = "25 ms")]
971 fn upsert_rejects_sub_25ms_interval_in_debug() {
972 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 5]);
973 let mut mux = SiMux::new();
974 mux.upsert(Pid::new(0x0100), s, Duration::from_millis(10));
975 }
976
977 #[test]
978 fn upsert_accepts_25ms_interval() {
979 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 5]);
980 let mut mux = SiMux::new();
981 mux.upsert(Pid::new(0x0100), s, MIN_SECTION_INTERVAL);
982 let pkts = mux.poll(Duration::ZERO);
983 assert!(!pkts.is_empty());
984 }
985
986 #[test]
989 fn round_trip_single_short_section() {
990 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 10]);
991 assert_round_trip(&[s]);
992 }
993
994 #[test]
995 fn round_trip_one_byte_body() {
996 let s = test_fixtures::psi::short_section(0x46, &[0xBB]); assert_round_trip(&[s]);
998 }
999
1000 #[test]
1001 fn round_trip_section_exactly_pusi_cap_boundary() {
1002 let body = vec![0xCC; PUSI_PAYLOAD_CAP - 3];
1004 let s = test_fixtures::psi::short_section(0x50, &body);
1005 assert_eq!(s.len(), PUSI_PAYLOAD_CAP);
1006 assert_round_trip(&[s]);
1007 }
1008
1009 #[test]
1010 fn round_trip_section_just_over_pusi_cap() {
1011 let body = vec![0xDD; PUSI_PAYLOAD_CAP - 3 + 1];
1013 let s = test_fixtures::psi::short_section(0x52, &body);
1014 assert_eq!(s.len(), PUSI_PAYLOAD_CAP + 1);
1015 assert_round_trip(&[s]);
1016 }
1017
1018 #[test]
1019 fn round_trip_section_spans_many_packets() {
1020 let body = vec![0xEE; 2000 - 3];
1022 let s = test_fixtures::psi::short_section(0x60, &body);
1023 assert_round_trip(&[s]);
1024 }
1025
1026 #[test]
1027 fn round_trip_section_at_max_size() {
1028 let body = vec![0x11; 4096 - 3];
1033 let s = test_fixtures::psi::short_section(0x80, &body);
1034 assert_eq!(s.len(), 4096);
1035 assert_round_trip(&[s]);
1036 }
1037
1038 #[test]
1039 fn round_trip_multiple_short_sections_in_one_batch() {
1040 let s1 = test_fixtures::psi::short_section(0x42, &[0x01, 0x02]); let s2 = test_fixtures::psi::short_section(0x46, &[0x03]); let s3 = test_fixtures::psi::short_section(0x4A, &[0x04, 0x05, 0x06]); assert_round_trip(&[s1, s2, s3]);
1044 }
1045
1046 #[test]
1047 fn round_trip_section_ends_exactly_at_boundary() {
1048 let body1 = vec![0xA1; PUSI_PAYLOAD_CAP - 3];
1052 let s1 = test_fixtures::psi::short_section(0x50, &body1);
1053 assert_eq!(s1.len(), PUSI_PAYLOAD_CAP);
1054
1055 let s2 = test_fixtures::psi::short_section(0x52, &[0xB1, 0xB2]);
1056 assert_round_trip(&[s1, s2]);
1057 }
1058
1059 #[test]
1060 fn continuation_packet_never_overshoots_next_section_start() {
1061 let body1 = vec![0xA5; 366 - 3];
1068 let s1 = test_fixtures::psi::short_section(0x50, &body1);
1069 assert_eq!(s1.len(), 366);
1070 let s2 = test_fixtures::psi::short_section(0x52, &[0xB1, 0xB2, 0xB3]);
1071
1072 let mut packetiser = SectionPacketiser::new(Pid::new(0x0100));
1073 let packets = packetiser.packetise(&[s1.as_slice(), s2.as_slice()]);
1074
1075 for pkt_raw in &packets {
1076 let pkt = TsPacket::parse(pkt_raw).expect("parse generated packet");
1077 let payload = pkt.payload.expect("payload present");
1078 if pkt.header.pusi {
1079 let pointer = payload[0] as usize;
1080 assert!(
1084 pointer < PUSI_PAYLOAD_CAP,
1085 "pointer_field {pointer} leaves no room for a section to \
1086 start within this packet (H.222.0 §2.4.4)"
1087 );
1088 }
1089 }
1090
1091 assert_round_trip(&[s1, s2]);
1092 }
1093
1094 #[test]
1095 fn round_trip_mix_small_large_sections() {
1096 let s1 = test_fixtures::psi::short_section(0x10, &[0xAA; 5]);
1099 let body2 = vec![0xBB; 200];
1100 let s2 = test_fixtures::psi::short_section(0x20, &body2);
1101 let s3 = test_fixtures::psi::short_section(0x30, &[0xCC; 50]);
1102 let body4 = vec![0xDD; 800];
1103 let s4 = test_fixtures::psi::short_section(0x40, &body4);
1104 let s5 = test_fixtures::psi::short_section(0x50, &[0xEE]); assert_round_trip(&[s1, s2, s3, s4, s5]);
1106 }
1107
1108 #[test]
1111 fn continuity_counter_increments_per_packet() {
1112 let body = vec![0xAA; 500];
1114 let section = test_fixtures::psi::short_section(0x42, &body);
1115 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1116
1117 let packets = p.packetise(&[§ion]);
1118 assert!(packets.len() >= 3, "need multiple packets to test CC");
1119
1120 let mut last_cc: Option<u8> = None;
1121 for pkt_raw in &packets {
1122 let pkt = TsPacket::parse(pkt_raw).unwrap();
1123 let cc = pkt.header.continuity_counter;
1124 if let Some(last) = last_cc {
1125 assert_eq!(cc, (last + 1) & 0x0F, "CC must increment per packet");
1126 }
1127 last_cc = Some(cc);
1128 }
1129 }
1130
1131 #[test]
1132 fn continuity_counter_wraps_and_continues_across_calls() {
1133 let mut p = SectionPacketiser::with_continuity(Pid::new(0x0100), 14);
1134 let body = vec![0xBB; 500];
1136 let s = test_fixtures::psi::short_section(0x42, &body);
1137
1138 let pkts1 = p.packetise(&[&s]);
1140 assert!(pkts1.len() >= 3, "section must span ≥3 packets");
1141 let ccs1: Vec<u8> = pkts1
1142 .iter()
1143 .map(|b| TsPacket::parse(b).unwrap().header.continuity_counter)
1144 .collect();
1145 assert_eq!(ccs1[0], 14);
1146 assert_eq!(ccs1[1], 15);
1147 assert_eq!(ccs1[2], 0);
1148
1149 let pkts2 = p.packetise(&[&s]);
1151 let cc_first_pkt2 = TsPacket::parse(&pkts2[0])
1152 .unwrap()
1153 .header
1154 .continuity_counter;
1155 assert_eq!(cc_first_pkt2, ccs1.last().map(|c| (c + 1) & 0x0F).unwrap());
1156 }
1157
1158 #[test]
1161 fn pusi_set_when_section_starts() {
1162 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 10]);
1163 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1164 let packets = p.packetise(&[&s]);
1165 assert!(!packets.is_empty());
1166 let pkt = TsPacket::parse(&packets[0]).unwrap();
1167 assert!(pkt.header.pusi, "first packet must have PUSI=1");
1168 }
1169
1170 #[test]
1171 fn pusi_not_set_on_mid_section_continuation() {
1172 let body = vec![0xAA; 500];
1173 let s = test_fixtures::psi::short_section(0x42, &body);
1174 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1175 let packets = p.packetise(&[&s]);
1176 assert!(packets.len() >= 2);
1177 let pkt1 = TsPacket::parse(&packets[0]).unwrap();
1178 let pkt2 = TsPacket::parse(&packets[1]).unwrap();
1179 assert!(pkt1.header.pusi, "first packet must have PUSI=1");
1180 assert!(
1181 !pkt2.header.pusi,
1182 "second packet is continuation, must have PUSI=0"
1183 );
1184 }
1185
1186 #[test]
1187 fn pointer_field_equals_tail_length_before_new_section() {
1188 let body1 = vec![0xA1; 197]; let s1 = test_fixtures::psi::short_section(0x52, &body1);
1193 assert_eq!(s1.len(), 200);
1194 let s2 = test_fixtures::psi::short_section(0x54, &[0xB1; 47]); assert_eq!(s2.len(), 50);
1196
1197 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1198 let packets = p.packetise(&[&s1, &s2]);
1199
1200 let pkt_with_pointer = packets
1202 .iter()
1203 .map(|raw| TsPacket::parse(raw).unwrap())
1204 .find(|pkt| pkt.header.pusi && pkt.payload.is_some_and(|pl| pl.first() != Some(&0)))
1205 .expect("must have a PUSI packet with non-zero pointer");
1206
1207 let payload = pkt_with_pointer.payload.unwrap();
1208 let pointer = payload[0] as usize;
1209 assert!(pointer > 0, "pointer must be non-zero");
1210 let tail_start = s1.len() - pointer;
1212 assert_eq!(&payload[1..1 + pointer], &s1[tail_start..]);
1213 }
1214
1215 #[test]
1218 fn final_packet_unused_tail_is_stuffing() {
1219 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 5]); let mut p = SectionPacketiser::new(Pid::new(0x0100));
1221 let packets = p.packetise(&[&s]);
1222
1223 let pkt = TsPacket::parse(&packets[0]).unwrap();
1224 let payload = pkt.payload.unwrap();
1225 assert_eq!(payload[0], 0, "pointer_field should be 0");
1226
1227 let section_end = 1 + s.len(); assert!(
1229 section_end < payload.len(),
1230 "must have stuffing after section"
1231 );
1232 for &b in &payload[section_end..] {
1233 assert_eq!(b, STUFFING_BYTE, "all trailing bytes must be 0xFF");
1234 }
1235 }
1236
1237 #[test]
1238 fn reassembler_discards_stuffing() {
1239 let s1 = test_fixtures::psi::short_section(0x42, &[0xAA; 10]);
1240 let s2 = test_fixtures::psi::short_section(0x46, &[0xBB; 5]);
1241
1242 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1243 let packets = p.packetise(&[&s1, &s2]);
1244
1245 let mut reasm = SectionReassembler::default();
1246 for pkt_raw in &packets {
1247 let pkt = TsPacket::parse(pkt_raw).unwrap();
1248 reasm.feed(pkt.payload.unwrap(), pkt.header.pusi);
1249 }
1250
1251 let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
1252 assert_eq!(got.len(), 2);
1253 assert!(
1254 reasm.is_empty(),
1255 "stuffing tail must be discarded, not buffered"
1256 );
1257 }
1258
1259 #[test]
1262 fn empty_batch_produces_no_packets() {
1263 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1264 let packets: Vec<[u8; TS_PACKET_SIZE]> = p.packetise(&[]);
1265 assert!(packets.is_empty());
1266 }
1267
1268 #[test]
1269 fn packetise_into_clears_out_first() {
1270 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 5]);
1271 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1272
1273 let mut out = vec![[0u8; TS_PACKET_SIZE]; 99]; let n = p.packetise_into(&[&s], &mut out);
1275 assert_eq!(n, out.len(), "out must contain only the new packets");
1276 let mut reasm = SectionReassembler::default();
1278 for pkt_raw in &out {
1279 let pkt = TsPacket::parse(pkt_raw).unwrap();
1280 reasm.feed(pkt.payload.unwrap(), pkt.header.pusi);
1281 }
1282 let got = reasm.pop_section().unwrap();
1283 assert_eq!(got.as_ref(), s.as_slice());
1284 }
1285
1286 #[test]
1287 fn pid_is_correct() {
1288 let p = SectionPacketiser::new(Pid::new(0x1234));
1289 assert_eq!(p.pid(), Pid::new(0x1234));
1290 }
1291
1292 #[test]
1293 fn with_continuity_masks_to_4_bits() {
1294 let p = SectionPacketiser::with_continuity(Pid::new(0x0100), 0xFE);
1295 assert_eq!(p.continuity_counter(), 0x0E);
1296 }
1297
1298 #[test]
1299 fn has_payload_always_true_no_adaptation() {
1300 let s = test_fixtures::psi::short_section(0x42, &[0xAA; 50]);
1301 let mut p = SectionPacketiser::new(Pid::new(0x0100));
1302 let packets = p.packetise(&[&s]);
1303 for pkt_raw in &packets {
1304 let pkt = TsPacket::parse(pkt_raw).unwrap();
1305 assert!(pkt.header.has_payload, "every packet must carry payload");
1306 assert!(!pkt.header.has_adaptation, "no adaptation field is emitted");
1307 assert!(!pkt.header.tei, "TEI must be false");
1308 assert_eq!(pkt.header.scrambling, 0, "scrambling must be 0");
1309 }
1310 }
1311}