Skip to main content

dvb_si/
ts.rs

1//! MPEG-TS packet parser + section reassembler. Feature-gated under `ts`.
2
3use crate::error::{Error, Result};
4
5/// Size of one MPEG-TS packet (ETSI EN 300 468 §3.2, ISO/IEC 13818-1 §2.4.3.2).
6pub const TS_PACKET_SIZE: usize = 188;
7/// Sync byte that every TS packet starts with (ISO/IEC 13818-1 §2.4.3.2).
8pub const TS_SYNC_BYTE: u8 = 0x47;
9/// Upper bound on a single section: `section_length` is 12 bits (max 4095)
10/// plus the 3-byte header = 4098. (Long-form SI caps `section_length` at
11/// 4093 → total 4096, but maximal short-form private sections may reach
12/// 4098; the reassembler accepts the absolute ceiling.)
13const MAX_SECTION_SIZE: usize = 4098;
14
15/// ETSI EN 300 468 §3.2.3: transport header byte 1 bits 7 = tei (Transport Error Indicator).
16const TEI_MASK: u8 = 0x80;
17/// ETSI EN 300 468 §3.2.3: byte 1 bits 6 = pusi (Payload Unit Start Indicator).
18const PUSI_MASK: u8 = 0x40;
19/// ETSI EN 300 468 §3.2.3: byte 1 bits 5..=1 = 13-bit PID (upper 5 bits).
20pub const PID_MASK_HI: u8 = 0x1F;
21/// ETSI EN 300 468 §3.2.3: byte 3 bits 7..=6 = 2-bit scrambling control.
22pub const SCRAMBLING_MASK: u8 = 0xC0;
23/// ETSI EN 300 468 §3.2.3: byte 3 bit 4 = adaptation_field_control (bit 4 = 1 means adaptation present).
24pub const ADAPTATION_FLAG: u8 = 0x20;
25/// ETSI EN 300 468 §3.2.3: byte 3 bit 3 = adaptation_field_control (bit 3 = 1 means payload present).
26pub const PAYLOAD_FLAG: u8 = 0x10;
27/// ETSI EN 300 468 §3.2.3: byte 3 bits 3..=0 = 4-bit continuity_counter.
28pub const CC_MASK: u8 = 0x0F;
29
30/// Parsed TS header — the 4-byte transport header fields.
31#[derive(Clone, Debug, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize))]
33pub struct TsHeader {
34    /// Transport Error Indicator — set by the demodulator when an
35    /// uncorrectable error is present in the packet.
36    pub tei: bool,
37    /// Payload Unit Start Indicator — first byte of the payload is a new
38    /// PES packet or PSI section header when set.
39    pub pusi: bool,
40    /// 13-bit Packet Identifier.
41    pub pid: u16,
42    /// 2-bit transport_scrambling_control (0 = not scrambled).
43    pub scrambling: u8,
44    /// Adaptation field present flag (adaptation_field_control bit 1).
45    pub has_adaptation: bool,
46    /// Payload present flag (adaptation_field_control bit 0).
47    pub has_payload: bool,
48    /// 4-bit continuity_counter (wraps 0..=15 per PID).
49    pub continuity_counter: u8,
50}
51
52/// Borrowed view into one 188-byte TS packet.
53///
54/// Serde: Serialize-only (re-parse from wire bytes to reconstruct). `raw` is
55/// excluded from the serialized form because it is redundant once the header
56/// has been parsed.
57#[derive(Clone, Debug)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize))]
59pub struct TsPacket<'a> {
60    /// Parsed header fields.
61    pub header: TsHeader,
62    /// Slice into the packet's payload, or `None` when `has_payload == false`
63    /// or the adaptation field consumed the whole packet body.
64    pub payload: Option<&'a [u8]>,
65    /// The adaptation-field bytes (after the length byte). Internal capture
66    /// feeding [`adaptation_field`](Self::adaptation_field); not public.
67    #[cfg_attr(feature = "serde", serde(skip))]
68    adaptation: Option<&'a [u8]>,
69    /// The raw 188 bytes of the packet — kept for cheap forwarding.
70    #[cfg_attr(feature = "serde", serde(skip))]
71    pub raw: &'a [u8; TS_PACKET_SIZE],
72}
73
74impl TsHeader {
75    /// Parse a 4-byte TS transport header.
76    ///
77    /// Returns `None` if `raw4` is shorter than 4 bytes.
78    pub fn parse(raw4: &[u8]) -> Option<Self> {
79        if raw4.len() < 4 {
80            return None;
81        }
82        let b1 = raw4[1];
83        let b2 = raw4[2];
84        let b3 = raw4[3];
85
86        let tei = (b1 & TEI_MASK) != 0;
87        let pusi = (b1 & PUSI_MASK) != 0;
88        let pid = (((b1 & PID_MASK_HI) as u16) << 8) | (b2 as u16);
89        let scrambling = (b3 & SCRAMBLING_MASK) >> 6;
90        let has_adaptation = (b3 & ADAPTATION_FLAG) != 0;
91        let has_payload = (b3 & PAYLOAD_FLAG) != 0;
92        let continuity_counter = b3 & CC_MASK;
93
94        Some(Self {
95            tei,
96            pusi,
97            pid,
98            scrambling,
99            has_adaptation,
100            has_payload,
101            continuity_counter,
102        })
103    }
104
105    /// Serialize this header into the first 4 bytes of `buf`.
106    ///
107    /// Panics if `buf` is shorter than 4 bytes.
108    pub fn serialize_into(&self, buf: &mut [u8]) {
109        assert!(
110            buf.len() >= 4,
111            "buffer must have at least 4 bytes for TS header"
112        );
113        buf[0] = TS_SYNC_BYTE;
114        buf[1] = 0;
115        if self.tei {
116            buf[1] |= TEI_MASK;
117        }
118        if self.pusi {
119            buf[1] |= PUSI_MASK;
120        }
121        buf[1] |= ((self.pid >> 8) as u8) & PID_MASK_HI;
122        buf[2] = (self.pid & 0xFF) as u8;
123        buf[3] = (self.scrambling << 6) & SCRAMBLING_MASK;
124        if self.has_adaptation {
125            buf[3] |= ADAPTATION_FLAG;
126        }
127        if self.has_payload {
128            buf[3] |= PAYLOAD_FLAG;
129        }
130        buf[3] |= self.continuity_counter & CC_MASK;
131    }
132}
133
134impl<'a> TsPacket<'a> {
135    /// Parse a single 188-byte TS packet from a buffer.
136    ///
137    /// Returns `Err(Error::InvalidSyncByte)` if the first byte is not `0x47`,
138    /// `Err(Error::BufferTooShort)` if fewer than 188 bytes, or `Ok` with
139    /// the parsed packet otherwise.
140    pub fn parse(buf: &'a [u8]) -> Result<Self> {
141        if buf.len() < TS_PACKET_SIZE {
142            return Err(Error::BufferTooShort {
143                need: TS_PACKET_SIZE,
144                have: buf.len(),
145                what: "TsPacket::parse",
146            });
147        }
148        if buf[0] != TS_SYNC_BYTE {
149            return Err(Error::InvalidSyncByte { found: buf[0] });
150        }
151
152        let raw: &[u8; TS_PACKET_SIZE] =
153            buf[..TS_PACKET_SIZE]
154                .try_into()
155                .map_err(|_| Error::BufferTooShort {
156                    need: TS_PACKET_SIZE,
157                    have: buf.len(),
158                    what: "TsPacket::parse (array conversion)",
159                })?;
160
161        let header = TsHeader::parse(&raw[..4])
162            .expect("raw is 188 bytes so first 4 bytes are always present");
163
164        let mut cursor = 4usize;
165        let mut payload = None;
166        let mut adaptation = None;
167
168        // Capture the adaptation field if present, then skip it (the section
169        // path does not need it; decode lazily via `adaptation_field`).
170        if header.has_adaptation && cursor < TS_PACKET_SIZE {
171            let af_len = raw[cursor] as usize;
172            let af_start = cursor + 1;
173            if af_len > 0 && af_start < TS_PACKET_SIZE {
174                let af_end = (af_start + af_len).min(TS_PACKET_SIZE);
175                adaptation = Some(&raw[af_start..af_end]);
176            }
177            cursor += 1 + af_len;
178        }
179
180        if header.has_payload && cursor < TS_PACKET_SIZE {
181            payload = Some(&raw[cursor..]);
182        }
183
184        Ok(TsPacket {
185            header,
186            payload,
187            adaptation,
188            raw,
189        })
190    }
191
192    /// Decode the adaptation field, if present.
193    ///
194    /// Returns `None` when the packet carries no adaptation field, and
195    /// `Some(Err(..))` when a present field is truncated. Layout per
196    /// ISO/IEC 13818-1:2007 §2.4.3.4 (`docs/iso_13818_1_systems.md`).
197    pub fn adaptation_field(&self) -> Option<crate::Result<AdaptationField>> {
198        self.adaptation.map(AdaptationField::parse)
199    }
200}
201
202// Adaptation-field flag bits, byte 0 (ISO/IEC 13818-1:2007 §2.4.3.4).
203const AF_DISCONTINUITY: u8 = 0x80;
204const AF_RANDOM_ACCESS: u8 = 0x40;
205const AF_ES_PRIORITY: u8 = 0x20;
206const AF_PCR_FLAG: u8 = 0x10;
207const AF_OPCR_FLAG: u8 = 0x08;
208const AF_SPLICING_FLAG: u8 = 0x04;
209/// Encoded PCR / OPCR field width: 33-bit base + 6 reserved + 9-bit extension.
210const PCR_FIELD_LEN: usize = 6;
211
212/// Program Clock Reference (ISO/IEC 13818-1:2007 §2.4.3.5): a 33-bit base on a
213/// 90 kHz clock plus a 9-bit extension on a 27 MHz clock.
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize))]
216pub struct Pcr {
217    /// 33-bit base (90 kHz units).
218    pub base: u64,
219    /// 9-bit extension (27 MHz units).
220    pub extension: u16,
221}
222
223impl Pcr {
224    /// Full PCR value on the 27 MHz clock: `base * 300 + extension`.
225    #[must_use]
226    pub fn as_27mhz(self) -> u64 {
227        self.base * 300 + self.extension as u64
228    }
229
230    /// Decode the 6-byte PCR/OPCR field starting at `at` within `af`.
231    fn parse(af: &[u8], at: usize) -> Result<Self> {
232        let b: &[u8; PCR_FIELD_LEN] = af
233            .get(at..at + PCR_FIELD_LEN)
234            .and_then(|s| s.try_into().ok())
235            .ok_or(Error::BufferTooShort {
236                need: at + PCR_FIELD_LEN,
237                have: af.len(),
238                what: "adaptation_field PCR",
239            })?;
240        let base = ((b[0] as u64) << 25)
241            | ((b[1] as u64) << 17)
242            | ((b[2] as u64) << 9)
243            | ((b[3] as u64) << 1)
244            | ((b[4] as u64) >> 7);
245        let extension = (((b[4] & 0x01) as u16) << 8) | (b[5] as u16);
246        Ok(Self { base, extension })
247    }
248}
249
250/// Decoded adaptation field — flags plus PCR/OPCR and splice point per
251/// ISO/IEC 13818-1:2007 §2.4.3.4. Transport-private data and the
252/// adaptation-field extension are not surfaced.
253#[derive(Clone, Copy, Debug, PartialEq, Eq)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize))]
255pub struct AdaptationField {
256    /// A timing/continuity discontinuity starts at this packet.
257    pub discontinuity_indicator: bool,
258    /// This packet is a random-access point.
259    pub random_access_indicator: bool,
260    /// Elementary-stream priority hint.
261    pub elementary_stream_priority_indicator: bool,
262    /// Program Clock Reference, present iff the PCR flag is set.
263    pub pcr: Option<Pcr>,
264    /// Original PCR, present iff the OPCR flag is set.
265    pub opcr: Option<Pcr>,
266    /// Splice countdown (packets until the splice point), iff the flag is set.
267    pub splice_countdown: Option<i8>,
268}
269
270impl AdaptationField {
271    /// Parse the adaptation-field bytes (those following the length byte).
272    fn parse(af: &[u8]) -> Result<Self> {
273        let flags = *af.first().ok_or(Error::BufferTooShort {
274            need: 1,
275            have: 0,
276            what: "adaptation_field flags",
277        })?;
278        let mut cursor = 1usize;
279
280        let pcr = if flags & AF_PCR_FLAG != 0 {
281            let p = Pcr::parse(af, cursor)?;
282            cursor += PCR_FIELD_LEN;
283            Some(p)
284        } else {
285            None
286        };
287        let opcr = if flags & AF_OPCR_FLAG != 0 {
288            let p = Pcr::parse(af, cursor)?;
289            cursor += PCR_FIELD_LEN;
290            Some(p)
291        } else {
292            None
293        };
294        let splice_countdown = if flags & AF_SPLICING_FLAG != 0 {
295            let b = *af.get(cursor).ok_or(Error::BufferTooShort {
296                need: cursor + 1,
297                have: af.len(),
298                what: "adaptation_field splice_countdown",
299            })?;
300            Some(b as i8)
301        } else {
302            None
303        };
304
305        Ok(AdaptationField {
306            discontinuity_indicator: flags & AF_DISCONTINUITY != 0,
307            random_access_indicator: flags & AF_RANDOM_ACCESS != 0,
308            elementary_stream_priority_indicator: flags & AF_ES_PRIORITY != 0,
309            pcr,
310            opcr,
311            splice_countdown,
312        })
313    }
314}
315
316/// Reassembles PSI/SI sections from TS packets on a single PID.
317///
318/// Feed each TS packet's payload with `feed`. Complete sections are
319/// appended to an internal queue; drain them with `pop_section`.
320#[derive(Default)]
321pub struct SectionReassembler {
322    buf: bytes::BytesMut,
323    ready: std::collections::VecDeque<bytes::Bytes>,
324}
325
326impl SectionReassembler {
327    /// Feed a TS payload and whether its packet had PUSI set.
328    ///
329    /// Extracts complete SI sections into the internal queue. A single call
330    /// can produce zero, one, or **several** sections — a payload may
331    /// concatenate multiple complete sections after the `pointer_field`
332    /// (EN 300 468 §5.1.4; common on EMM PIDs). Drain with a
333    /// `while let Some(s) = r.pop_section()` loop, not a single `if let`.
334    pub fn feed(&mut self, payload: &[u8], pusi: bool) {
335        if pusi {
336            // A PUSI packet whose adaptation field consumed the whole body is
337            // malformed but constructible — drop sync rather than panic.
338            if payload.is_empty() {
339                self.buf.clear();
340                return;
341            }
342            let pointer = payload[0] as usize;
343
344            // The `pointer_field` counts bytes that belong to a section still
345            // in progress from a previous packet (ISO/IEC 13818-1 §2.4.4): the
346            // `pointer` bytes immediately after it are that section's tail and
347            // must complete it BEFORE new sections begin at `1 + pointer`.
348            // Skipping them (or clearing `buf` first) drops any section that
349            // spans into a PUSI packet — silent loss biased toward whichever
350            // section happens to straddle a packet boundary.
351            if !self.buf.is_empty() && pointer > 0 {
352                let avail = payload.len() - 1;
353                let tail_len = pointer.min(avail);
354                if self.buf.len() + tail_len > MAX_SECTION_SIZE {
355                    self.buf.clear();
356                } else {
357                    self.buf.extend_from_slice(&payload[1..1 + tail_len]);
358                    self.drain_complete_sections();
359                }
360            }
361
362            // New sections start at `1 + pointer`; anything still buffered is
363            // an incomplete (corrupt / lost-packet) section — discard it.
364            self.buf.clear();
365
366            let start = 1 + pointer;
367            if start >= payload.len() {
368                // Pointer spans to (or past) the end — no new section here.
369                return;
370            }
371            let new_data = &payload[start..];
372            if new_data.len() > MAX_SECTION_SIZE {
373                return;
374            }
375            self.buf.extend_from_slice(new_data);
376        } else {
377            if self.buf.is_empty() {
378                return;
379            }
380            if self.buf.len() + payload.len() > MAX_SECTION_SIZE {
381                self.buf.clear();
382                return;
383            }
384            self.buf.extend_from_slice(payload);
385        }
386
387        self.drain_complete_sections();
388    }
389
390    /// Queue every complete section the buffer currently holds.
391    ///
392    /// A single TS payload may concatenate multiple complete sections after
393    /// the `pointer_field` (legal per ETSI EN 300 468 §5.1.4 and common on
394    /// EMM PIDs, which pack several short messages into one payload). We must
395    /// keep extracting until the buffer holds only a partial (multi-packet
396    /// spanning) section, whose bytes stay buffered for the next packet to
397    /// continue (the expected length is recomputed from the section header on
398    /// each drain). A `0xFF` where a `table_id` is expected marks the rest of
399    /// the payload as stuffing.
400    fn drain_complete_sections(&mut self) {
401        loop {
402            if self.buf.len() < 3 {
403                // Not enough for a section header yet; keep the partial bytes
404                // and wait for the next packet to complete the header.
405                break;
406            }
407            if self.buf[0] == 0xFF {
408                // Stuffing where a table_id is expected — payload tail is fill.
409                self.buf.clear();
410                break;
411            }
412            let exp = 3 + (((self.buf[1] & 0x0F) as usize) << 8 | self.buf[2] as usize);
413            if self.buf.len() >= exp {
414                // split_to returns the first `exp` bytes as an owned BytesMut,
415                // leaving the remainder in self.buf — cheap (shifts pointers).
416                let section = self.buf.split_to(exp).freeze();
417                self.ready.push_back(section);
418            } else {
419                // Partial section spanning into later packets.
420                break;
421            }
422        }
423    }
424
425    /// Pop one complete section. Returns `None` when the queue is empty.
426    pub fn pop_section(&mut self) -> Option<bytes::Bytes> {
427        self.ready.pop_front()
428    }
429
430    /// Number of bytes currently buffered (incomplete section).
431    pub fn len(&self) -> usize {
432        self.buf.len()
433    }
434
435    /// True if no bytes are currently buffered.
436    pub fn is_empty(&self) -> bool {
437        self.buf.is_empty()
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    /// Helper: construct a minimal 188-byte TS packet buffer with given header flags and payload.
446    fn make_packet(b1: u8, b2: u8, b3: u8, payload_data: &[u8]) -> [u8; TS_PACKET_SIZE] {
447        let mut pkt = [0u8; TS_PACKET_SIZE];
448        pkt[0] = TS_SYNC_BYTE;
449        pkt[1] = b1;
450        pkt[2] = b2;
451        pkt[3] = b3;
452        let payload_start = 4;
453        let end = (payload_start + payload_data.len()).min(TS_PACKET_SIZE);
454        let len = (end - payload_start).min(payload_data.len());
455        pkt[payload_start..payload_start + len].copy_from_slice(&payload_data[..len]);
456        pkt
457    }
458
459    #[test]
460    fn parse_rejects_non_0x47_sync_byte() {
461        let mut pkt = [0u8; TS_PACKET_SIZE];
462        pkt[0] = 0x46; // wrong sync byte
463        let err = TsPacket::parse(&pkt).unwrap_err();
464        match err {
465            Error::InvalidSyncByte { found } => assert_eq!(found, 0x46),
466            other => panic!("expected InvalidSyncByte, got {other:?}"),
467        }
468    }
469
470    #[test]
471    fn parse_extracts_pid_and_continuity_counter() {
472        // PID = 0x1234 → upper 5 bits = 0x12, lower 8 bits = 0x34
473        // CC = 5 → 0x05
474        // b1 = 0x47 (sync=0, tei=0, pusi=0) | (0x12) = 0x47 & 0xE0 | 0x12 = 0x47 & 0xE0 = 0x40 | 0x12 = 0x52
475        // Actually: b1 bits: [tei:1][pusi:1][pid_hi:5]
476        // pid_hi = 0x12 = 0b00100_10 → bits 5..=1 = 0x12
477        // b1 = 0b00_010010 = 0x12 (no tei, no pusi)
478        let pkt = make_packet(0x12, 0x34, 0x05, &[]);
479        let pkt = TsPacket::parse(&pkt).unwrap();
480        assert_eq!(pkt.header.pid, 0x1234);
481        assert_eq!(pkt.header.continuity_counter, 5);
482    }
483
484    #[test]
485    fn payload_unit_start_indicator_flag_extracted() {
486        // b1 = 0x40 → pusi = true (bit 6 set, no tei, no pid bits)
487        let pkt1 = make_packet(0x40, 0x00, 0x00, &[]);
488        let pkt1 = TsPacket::parse(&pkt1).unwrap();
489        assert!(pkt1.header.pusi);
490
491        // b1 = 0x00 → pusi = false
492        let pkt2 = make_packet(0x00, 0x00, 0x00, &[]);
493        let pkt2 = TsPacket::parse(&pkt2).unwrap();
494        assert!(!pkt2.header.pusi);
495    }
496
497    /// Build a PSI-carrying TS payload: `pointer_field` byte followed by
498    /// (optionally) some tail of a previous section, followed by a fresh
499    /// section. `pointer_field` is the number of bytes of the previous
500    /// section that precede the new one (per ETSI EN 300 468 §5.1.4).
501    fn build_pusi_payload(pointer_field: u8, previous_tail: &[u8], section: &[u8]) -> Vec<u8> {
502        assert_eq!(pointer_field as usize, previous_tail.len());
503        let mut v = Vec::with_capacity(1 + previous_tail.len() + section.len());
504        v.push(pointer_field);
505        v.extend_from_slice(previous_tail);
506        v.extend_from_slice(section);
507        v
508    }
509
510    /// Build a long-form section with the given table_id and body bytes.
511    /// Returns the full section including its 3-byte + 5-byte header and a
512    /// placeholder CRC — for reassembler testing we don't validate the CRC.
513    fn build_section(table_id: u8, body_after_length: &[u8]) -> Vec<u8> {
514        let section_length = body_after_length.len() as u16;
515        let mut v = Vec::with_capacity(3 + section_length as usize);
516        v.push(table_id);
517        // ssi=1, pi=0, reserved=11, length hi 4 bits
518        v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
519        v.push((section_length & 0xFF) as u8);
520        v.extend_from_slice(body_after_length);
521        v
522    }
523
524    // The reassembler tests below feed raw payload slices directly to
525    // `feed()` rather than wrapping them in 188-byte TS packets. This avoids
526    // the TS stuffing-byte tail (0xFF padding) bleeding into the reassembled
527    // section and keeps the assertions exact.
528
529    #[test]
530    fn reassembler_accumulates_multi_packet_section() {
531        // 200-byte section that spans two payload slices.
532        let body = vec![0xAAu8; 197];
533        let section = build_section(0x02, &body);
534        assert_eq!(section.len(), 200);
535
536        let first_chunk = 100;
537        let payload1 = build_pusi_payload(0, &[], &section[..first_chunk]);
538        let payload2 = section[first_chunk..].to_vec();
539
540        let mut reasm = SectionReassembler::default();
541        reasm.feed(&payload1, true);
542        reasm.feed(&payload2, false);
543
544        let out = reasm.pop_section().expect("section should be ready");
545        assert_eq!(out.len(), 200);
546        assert_eq!(out.as_ref(), &section[..]);
547    }
548
549    #[test]
550    fn reassembler_yields_complete_section_once_length_satisfied() {
551        // 1-byte-body section: table_id=0x42, section_length=1, total=4 bytes.
552        let section = build_section(0x42, &[0xAA]);
553        assert_eq!(section.len(), 4);
554        let payload = build_pusi_payload(0, &[], &section);
555
556        let mut reasm = SectionReassembler::default();
557        reasm.feed(&payload, true);
558
559        let out = reasm
560            .pop_section()
561            .expect("single-packet section should pop");
562        assert_eq!(out.as_ref(), &section[..]);
563    }
564
565    #[test]
566    fn reassembler_extracts_all_concatenated_sections_in_one_payload() {
567        // Issue #29: a single PUSI payload packing three complete short
568        // sections after the pointer_field. All three must be queued — the
569        // old `feed` stopped after the first and the rest were silently lost
570        // (the CAS/EMM data-loss bug: SHARED EMMs landing as the 2nd+ section).
571        let s1 = build_section(0x42, &[0x11, 0x22]); // 5 bytes
572        let s2 = build_section(0x46, &[0x33]); // 4 bytes
573        let s3 = build_section(0x4A, &[0x44, 0x55, 0x66]); // 6 bytes
574
575        let mut concat = Vec::new();
576        concat.extend_from_slice(&s1);
577        concat.extend_from_slice(&s2);
578        concat.extend_from_slice(&s3);
579        let payload = build_pusi_payload(0, &[], &concat);
580
581        let mut reasm = SectionReassembler::default();
582        reasm.feed(&payload, true);
583
584        // Consumers must drain with a loop, not a single `if let`.
585        let got: Vec<_> = std::iter::from_fn(|| reasm.pop_section()).collect();
586        assert_eq!(got.len(), 3, "all three concatenated sections must pop");
587        assert_eq!(got[0].as_ref(), &s1[..]);
588        assert_eq!(got[1].as_ref(), &s2[..]);
589        assert_eq!(got[2].as_ref(), &s3[..]);
590    }
591
592    #[test]
593    fn reassembler_stops_at_stuffing_after_concatenated_sections() {
594        // Two sections then 0xFF stuffing fill — the stuffing must not be
595        // mistaken for a section header (0xFF table_id) nor leak into a
596        // section; both real sections still pop.
597        let s1 = build_section(0x42, &[0xAA]); // 4 bytes
598        let s2 = build_section(0x46, &[0xBB, 0xCC]); // 5 bytes
599        let mut concat = Vec::new();
600        concat.extend_from_slice(&s1);
601        concat.extend_from_slice(&s2);
602        concat.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); // stuffing tail
603        let payload = build_pusi_payload(0, &[], &concat);
604
605        let mut reasm = SectionReassembler::default();
606        reasm.feed(&payload, true);
607
608        let got: Vec<_> = std::iter::from_fn(|| reasm.pop_section()).collect();
609        assert_eq!(got.len(), 2);
610        assert_eq!(got[0].as_ref(), &s1[..]);
611        assert_eq!(got[1].as_ref(), &s2[..]);
612        assert!(
613            reasm.is_empty(),
614            "stuffing tail must be discarded, not buffered"
615        );
616    }
617
618    #[test]
619    fn reassembler_concatenated_then_spanning_tail() {
620        // One complete section followed by the head of a second that spans
621        // into a continuation packet: first pops immediately, second pops
622        // once the continuation arrives.
623        let s1 = build_section(0x42, &[0x01, 0x02]); // 5 bytes
624        let s2 = build_section(0x46, &[0x09u8; 60]); // 63 bytes
625        let split = 30;
626
627        let mut head = Vec::new();
628        head.extend_from_slice(&s1);
629        head.extend_from_slice(&s2[..split]);
630        let payload1 = build_pusi_payload(0, &[], &head);
631        let payload2 = s2[split..].to_vec();
632
633        let mut reasm = SectionReassembler::default();
634        reasm.feed(&payload1, true);
635        let first = reasm.pop_section().expect("first section pops at once");
636        assert_eq!(first.as_ref(), &s1[..]);
637        assert!(reasm.pop_section().is_none(), "second is still partial");
638
639        reasm.feed(&payload2, false);
640        let second = reasm.pop_section().expect("second pops after continuation");
641        assert_eq!(second.as_ref(), &s2[..]);
642    }
643
644    #[test]
645    fn reassembler_completes_section_spanning_into_pusi_packet() {
646        // Issue #29 (second case): a section starts late in packet A and spills
647        // into packet B, but B is itself PUSI=1 because new sections begin in it.
648        // B's pointer_field = the count of leading tail bytes belonging to the
649        // section from A. Those bytes MUST complete A's section before new
650        // sections start. 3.1.1 cleared buf + skipped them → the spanning
651        // section was lost (the SHARED EMM the smartcard needed).
652        let spanning = build_section(0x42, &[0x5Au8; 62]); // 65 bytes
653        let head = 41;
654        let tail = &spanning[head..]; // 24 bytes — lands in packet B
655        assert_eq!(tail.len(), 24);
656
657        // New section that begins in packet B after the spanning tail.
658        let next = build_section(0x46, &[0x77, 0x88]); // 5 bytes
659
660        // Packet A (PUSI): pointer 0, then the 41-byte head (incomplete).
661        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
662        // Packet B (PUSI): pointer = 24 (tail of A's section), then `next`.
663        let payload_b = build_pusi_payload(24, tail, &next);
664
665        let mut reasm = SectionReassembler::default();
666        reasm.feed(&payload_a, true);
667        assert!(reasm.pop_section().is_none(), "head alone is incomplete");
668
669        reasm.feed(&payload_b, true);
670        let got: Vec<_> = std::iter::from_fn(|| reasm.pop_section()).collect();
671        assert_eq!(got.len(), 2, "spanning section + new section must both pop");
672        assert_eq!(
673            got[0].as_ref(),
674            &spanning[..],
675            "spanning section completed from B's pointer tail"
676        );
677        assert_eq!(got[1].as_ref(), &next[..]);
678    }
679
680    #[test]
681    fn reassembler_pusi_pointer_spans_whole_payload() {
682        // A section spans into a PUSI packet whose pointer covers the ENTIRE
683        // remaining payload (no new section starts here) — the tail must be
684        // appended and the section completed once the count is satisfied.
685        let spanning = build_section(0x42, &[0x33u8; 40]); // 43 bytes
686        let head = 20;
687        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
688        let tail = &spanning[head..]; // 23 bytes — exactly the rest of payload B
689
690        let mut reasm = SectionReassembler::default();
691        reasm.feed(&payload_a, true);
692        // Packet B: pointer = 23 = all remaining bytes; no new section follows.
693        reasm.feed(&payload_b_pointer_only(tail), true);
694
695        let out = reasm.pop_section().expect("spanning section completes");
696        assert_eq!(out.as_ref(), &spanning[..]);
697        assert!(reasm.pop_section().is_none());
698    }
699
700    /// Build a PUSI payload whose `pointer_field` equals the whole tail (so the
701    /// pointer spans to the end of the payload and no new section starts).
702    fn payload_b_pointer_only(tail: &[u8]) -> Vec<u8> {
703        let mut v = Vec::with_capacity(1 + tail.len());
704        v.push(tail.len() as u8);
705        v.extend_from_slice(tail);
706        v
707    }
708
709    #[test]
710    fn reassembler_discards_on_buffer_overflow() {
711        // Declare section_length larger than a single payload can carry. No
712        // pop happens until continuations arrive; if continuations push the
713        // buffer past MAX_SECTION_SIZE the reassembler must reset, not panic.
714        let mut section = Vec::with_capacity(3 + 4095);
715        section.push(0x00); // table_id
716        section.push(0xB0 | ((4095u16 >> 8) as u8 & 0x0F));
717        section.push(0xFF);
718        section.extend_from_slice(&[0u8; 160]);
719        let payload1 = build_pusi_payload(0, &[], &section);
720
721        let mut reasm = SectionReassembler::default();
722        reasm.feed(&payload1, true);
723        assert!(reasm.pop_section().is_none());
724
725        // Push enough continuation data to cross MAX_SECTION_SIZE.
726        let filler = vec![0u8; 180];
727        for _ in 0..(MAX_SECTION_SIZE / 180 + 1) {
728            reasm.feed(&filler, false);
729        }
730        assert!(
731            reasm.pop_section().is_none(),
732            "no section should pop after overflow reset"
733        );
734
735        // State must be resettable — a fresh valid PUSI section works.
736        let valid_section = build_section(0x00, &[0xAA]);
737        let payload2 = build_pusi_payload(0, &[], &valid_section);
738        reasm.feed(&payload2, true);
739        let out = reasm
740            .pop_section()
741            .expect("fresh section should pop after reset");
742        assert_eq!(out.as_ref(), &valid_section[..]);
743    }
744
745    #[test]
746    fn reassembler_handles_pusi_with_nonzero_pointer_field() {
747        // payload = pointer_field=3, 3 bytes of prior-section tail, then new section.
748        let prior_tail = vec![0x11, 0x22, 0x33];
749        let new_section = build_section(0x02, &[0xBB]);
750        assert_eq!(new_section.len(), 4);
751        let payload = build_pusi_payload(3, &prior_tail, &new_section);
752
753        let mut reasm = SectionReassembler::default();
754        reasm.feed(&payload, true);
755
756        let out = reasm
757            .pop_section()
758            .expect("section after pointer_field skip should pop");
759        assert_eq!(out.as_ref(), &new_section[..]);
760    }
761
762    #[test]
763    fn reassembler_ignores_continuation_before_pusi() {
764        // Feed a non-PUSI payload first (no prior PUSI seen).
765        // SectionReassembler should discard it and stay empty.
766        let pkt = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xAA, 0xBB, 0xCC]);
767
768        let mut reasm = SectionReassembler::default();
769        reasm.feed(&pkt[4..], false); // no PUSI
770
771        assert!(
772            reasm.pop_section().is_none(),
773            "no section should appear without prior PUSI"
774        );
775        assert!(
776            reasm.pop_section().is_none(),
777            "second pop should also be none"
778        );
779    }
780
781    /// A PUSI packet with an empty payload (adaptation field ate the body)
782    /// is malformed but must not panic — it drops sync.
783    #[test]
784    fn reassembler_empty_pusi_payload_does_not_panic() {
785        let mut reasm = SectionReassembler::default();
786        reasm.feed(&[], true);
787        assert!(reasm.pop_section().is_none());
788        // Recovers on the next clean PUSI.
789        let mut payload = vec![0x00u8, 0x72, 0x70, 0x01, 0x00];
790        payload.resize(5, 0);
791        reasm.feed(&payload, true);
792        assert!(reasm.pop_section().is_some());
793    }
794
795    /// A maximal short-form private section (section_length 0xFFF, total
796    /// 4098 bytes) reassembles — the ceiling is 12-bit length + 3-byte
797    /// header, not 4096.
798    #[test]
799    fn reassembler_accepts_maximal_private_section() {
800        let mut section = vec![0x80u8, 0x7F, 0xFF]; // user-private tid, SSI=0, len 0xFFF
801        section.resize(3 + 0xFFF, 0xAB);
802
803        let mut reasm = SectionReassembler::default();
804        // First TS payload: pointer_field 0 then the section start.
805        let mut first = vec![0x00];
806        first.extend_from_slice(&section[..183]);
807        reasm.feed(&first, true);
808        for chunk in section[183..].chunks(184) {
809            reasm.feed(chunk, false);
810        }
811        let out = reasm.pop_section().expect("4098-byte section should pop");
812        assert_eq!(out.len(), 4098);
813        assert_eq!(out.as_ref(), &section[..]);
814    }
815
816    // ── adaptation field / PCR (ISO/IEC 13818-1 §2.4.3.4–2.4.3.5) ──
817
818    #[test]
819    fn pcr_as_27mhz_known_value() {
820        assert_eq!(
821            Pcr {
822                base: 10_000,
823                extension: 0
824            }
825            .as_27mhz(),
826            3_000_000
827        );
828        // base*300 + extension: 1*300 + 100 = 400.
829        assert_eq!(
830            Pcr {
831                base: 1,
832                extension: 100
833            }
834            .as_27mhz(),
835            400
836        );
837    }
838
839    #[test]
840    fn pcr_decode_from_bytes() {
841        // 6-byte PCR encoding base=10000, extension=0 (reserved bits set).
842        let af = [0x10u8, 0x00, 0x00, 0x13, 0x88, 0x7E, 0x00];
843        let pcr = Pcr::parse(&af, 1).expect("6 bytes present");
844        assert_eq!(
845            pcr,
846            Pcr {
847                base: 10_000,
848                extension: 0
849            }
850        );
851        assert_eq!(pcr.as_27mhz(), 3_000_000);
852    }
853
854    #[test]
855    fn adaptation_field_flags_and_pcr() {
856        let mut raw = [0xAAu8; TS_PACKET_SIZE];
857        raw[0] = TS_SYNC_BYTE;
858        raw[1] = 0x01; // pid 0x0100
859        raw[2] = 0x00;
860        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
861        raw[4] = 7; // adaptation_field_length: 1 flags + 6 PCR
862        raw[5] = AF_DISCONTINUITY | AF_PCR_FLAG;
863        raw[6..12].copy_from_slice(&[0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]);
864        // raw[12..] stays 0xAA = payload.
865
866        let pkt = TsPacket::parse(&raw).expect("valid packet");
867        let af = pkt
868            .adaptation_field()
869            .expect("has adaptation field")
870            .expect("adaptation field parses");
871        assert!(af.discontinuity_indicator);
872        assert!(!af.random_access_indicator);
873        assert_eq!(
874            af.pcr,
875            Some(Pcr {
876                base: 10_000,
877                extension: 0
878            })
879        );
880        assert_eq!(af.pcr.unwrap().as_27mhz(), 3_000_000);
881        assert!(af.opcr.is_none());
882        assert!(af.splice_countdown.is_none());
883        // Payload begins right after the adaptation field (cursor 4+1+7=12).
884        let payload = pkt.payload.expect("payload present");
885        assert_eq!(payload.len(), TS_PACKET_SIZE - 12);
886        assert_eq!(payload[0], 0xAA);
887    }
888
889    #[test]
890    fn no_adaptation_returns_none() {
891        let mut raw = [0x00u8; TS_PACKET_SIZE];
892        raw[0] = TS_SYNC_BYTE;
893        raw[1] = 0x01;
894        raw[3] = PAYLOAD_FLAG; // payload only
895        let pkt = TsPacket::parse(&raw).expect("valid");
896        assert!(pkt.adaptation_field().is_none());
897        assert!(pkt.adaptation.is_none());
898    }
899
900    #[test]
901    fn adaptation_field_splice_countdown_negative() {
902        let mut raw = [0xAAu8; TS_PACKET_SIZE];
903        raw[0] = TS_SYNC_BYTE;
904        raw[1] = 0x01;
905        raw[2] = 0x00;
906        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
907        raw[4] = 2; // 1 flags + 1 splice_countdown
908        raw[5] = AF_SPLICING_FLAG;
909        raw[6] = 0xFB; // -5 as i8
910        let pkt = TsPacket::parse(&raw).expect("valid");
911        let af = pkt.adaptation_field().unwrap().unwrap();
912        assert_eq!(af.splice_countdown, Some(-5));
913        assert!(af.pcr.is_none());
914    }
915}