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 raw 188 bytes of the packet — kept for cheap forwarding.
66    #[cfg_attr(feature = "serde", serde(skip))]
67    pub raw: &'a [u8; TS_PACKET_SIZE],
68}
69
70impl TsHeader {
71    /// Parse a 4-byte TS transport header.
72    ///
73    /// Returns `None` if `raw4` is shorter than 4 bytes.
74    pub fn parse(raw4: &[u8]) -> Option<Self> {
75        if raw4.len() < 4 {
76            return None;
77        }
78        let b1 = raw4[1];
79        let b2 = raw4[2];
80        let b3 = raw4[3];
81
82        let tei = (b1 & TEI_MASK) != 0;
83        let pusi = (b1 & PUSI_MASK) != 0;
84        let pid = (((b1 & PID_MASK_HI) as u16) << 8) | (b2 as u16);
85        let scrambling = (b3 & SCRAMBLING_MASK) >> 6;
86        let has_adaptation = (b3 & ADAPTATION_FLAG) != 0;
87        let has_payload = (b3 & PAYLOAD_FLAG) != 0;
88        let continuity_counter = b3 & CC_MASK;
89
90        Some(Self {
91            tei,
92            pusi,
93            pid,
94            scrambling,
95            has_adaptation,
96            has_payload,
97            continuity_counter,
98        })
99    }
100
101    /// Serialize this header into the first 4 bytes of `buf`.
102    ///
103    /// Panics if `buf` is shorter than 4 bytes.
104    pub fn serialize_into(&self, buf: &mut [u8]) {
105        assert!(
106            buf.len() >= 4,
107            "buffer must have at least 4 bytes for TS header"
108        );
109        buf[0] = TS_SYNC_BYTE;
110        buf[1] = 0;
111        if self.tei {
112            buf[1] |= TEI_MASK;
113        }
114        if self.pusi {
115            buf[1] |= PUSI_MASK;
116        }
117        buf[1] |= ((self.pid >> 8) as u8) & PID_MASK_HI;
118        buf[2] = (self.pid & 0xFF) as u8;
119        buf[3] = (self.scrambling << 6) & SCRAMBLING_MASK;
120        if self.has_adaptation {
121            buf[3] |= ADAPTATION_FLAG;
122        }
123        if self.has_payload {
124            buf[3] |= PAYLOAD_FLAG;
125        }
126        buf[3] |= self.continuity_counter & CC_MASK;
127    }
128}
129
130impl<'a> TsPacket<'a> {
131    /// Parse a single 188-byte TS packet from a buffer.
132    ///
133    /// Returns `Err(Error::InvalidSyncByte)` if the first byte is not `0x47`,
134    /// `Err(Error::BufferTooShort)` if fewer than 188 bytes, or `Ok` with
135    /// the parsed packet otherwise.
136    pub fn parse(buf: &'a [u8]) -> Result<Self> {
137        if buf.len() < TS_PACKET_SIZE {
138            return Err(Error::BufferTooShort {
139                need: TS_PACKET_SIZE,
140                have: buf.len(),
141                what: "TsPacket::parse",
142            });
143        }
144        if buf[0] != TS_SYNC_BYTE {
145            return Err(Error::InvalidSyncByte { found: buf[0] });
146        }
147
148        let raw: &[u8; TS_PACKET_SIZE] =
149            buf[..TS_PACKET_SIZE]
150                .try_into()
151                .map_err(|_| Error::BufferTooShort {
152                    need: TS_PACKET_SIZE,
153                    have: buf.len(),
154                    what: "TsPacket::parse (array conversion)",
155                })?;
156
157        let header = TsHeader::parse(&raw[..4])
158            .expect("raw is 188 bytes so first 4 bytes are always present");
159
160        let mut cursor = 4usize;
161        let mut payload = None;
162
163        // Skip adaptation field if present (not parsed in detail — not needed for sections).
164        if header.has_adaptation && cursor < TS_PACKET_SIZE {
165            let af_len = raw[cursor] as usize;
166            cursor += 1 + af_len;
167        }
168
169        if header.has_payload && cursor < TS_PACKET_SIZE {
170            payload = Some(&raw[cursor..]);
171        }
172
173        Ok(TsPacket {
174            header,
175            payload,
176            raw,
177        })
178    }
179}
180
181/// Reassembles PSI/SI sections from TS packets on a single PID.
182///
183/// Feed each TS packet's payload with `feed`. Complete sections are
184/// appended to an internal queue; drain them with `pop_section`.
185#[derive(Default)]
186pub struct SectionReassembler {
187    buf: bytes::BytesMut,
188    expected: usize,
189    ready: std::collections::VecDeque<bytes::Bytes>,
190}
191
192impl SectionReassembler {
193    /// Feed a TS payload and whether its packet had PUSI set.
194    ///
195    /// Extracts complete SI sections into the internal queue. A single call
196    /// can produce zero, one, or **several** sections — a payload may
197    /// concatenate multiple complete sections after the `pointer_field`
198    /// (EN 300 468 §5.1.4; common on EMM PIDs). Drain with a
199    /// `while let Some(s) = r.pop_section()` loop, not a single `if let`.
200    pub fn feed(&mut self, payload: &[u8], pusi: bool) {
201        if pusi {
202            // A PUSI packet whose adaptation field consumed the whole body is
203            // malformed but constructible — drop sync rather than panic.
204            if payload.is_empty() {
205                self.buf.clear();
206                self.expected = 0;
207                return;
208            }
209            let pointer = payload[0] as usize;
210
211            // The `pointer_field` counts bytes that belong to a section still
212            // in progress from a previous packet (ISO/IEC 13818-1 §2.4.4): the
213            // `pointer` bytes immediately after it are that section's tail and
214            // must complete it BEFORE new sections begin at `1 + pointer`.
215            // Skipping them (or clearing `buf` first) drops any section that
216            // spans into a PUSI packet — silent loss biased toward whichever
217            // section happens to straddle a packet boundary.
218            if !self.buf.is_empty() && pointer > 0 {
219                let avail = payload.len() - 1;
220                let tail_len = pointer.min(avail);
221                if self.buf.len() + tail_len > MAX_SECTION_SIZE {
222                    self.buf.clear();
223                    self.expected = 0;
224                } else {
225                    self.buf.extend_from_slice(&payload[1..1 + tail_len]);
226                    self.drain_complete_sections();
227                }
228            }
229
230            // New sections start at `1 + pointer`; anything still buffered is
231            // an incomplete (corrupt / lost-packet) section — discard it.
232            self.buf.clear();
233            self.expected = 0;
234
235            let start = 1 + pointer;
236            if start >= payload.len() {
237                // Pointer spans to (or past) the end — no new section here.
238                return;
239            }
240            let new_data = &payload[start..];
241            if new_data.len() > MAX_SECTION_SIZE {
242                return;
243            }
244            self.buf.extend_from_slice(new_data);
245        } else {
246            if self.buf.is_empty() {
247                return;
248            }
249            if self.buf.len() + payload.len() > MAX_SECTION_SIZE {
250                self.buf.clear();
251                self.expected = 0;
252                return;
253            }
254            self.buf.extend_from_slice(payload);
255        }
256
257        self.drain_complete_sections();
258    }
259
260    /// Queue every complete section the buffer currently holds.
261    ///
262    /// A single TS payload may concatenate multiple complete sections after
263    /// the `pointer_field` (legal per ETSI EN 300 468 §5.1.4 and common on
264    /// EMM PIDs, which pack several short messages into one payload). We must
265    /// keep extracting until the buffer holds only a partial (multi-packet
266    /// spanning) section, which is stashed as `expected` for the next
267    /// continuation. A `0xFF` where a `table_id` is expected marks the rest of
268    /// the payload as stuffing.
269    fn drain_complete_sections(&mut self) {
270        loop {
271            if self.buf.len() < 3 {
272                // Not enough for a section header yet; keep the partial bytes
273                // and wait for the next packet to complete the header.
274                self.expected = 0;
275                break;
276            }
277            if self.buf[0] == 0xFF {
278                // Stuffing where a table_id is expected — payload tail is fill.
279                self.buf.clear();
280                self.expected = 0;
281                break;
282            }
283            let exp = 3 + (((self.buf[1] & 0x0F) as usize) << 8 | self.buf[2] as usize);
284            if self.buf.len() >= exp {
285                // split_to returns the first `exp` bytes as an owned BytesMut,
286                // leaving the remainder in self.buf — cheap (shifts pointers).
287                let section = self.buf.split_to(exp).freeze();
288                self.ready.push_back(section);
289                self.expected = 0;
290            } else {
291                // Partial section spanning into later packets.
292                self.expected = exp;
293                break;
294            }
295        }
296    }
297
298    /// Pop one complete section. Returns `None` when the queue is empty.
299    pub fn pop_section(&mut self) -> Option<bytes::Bytes> {
300        self.ready.pop_front()
301    }
302
303    /// Number of bytes currently buffered (incomplete section).
304    pub fn len(&self) -> usize {
305        self.buf.len()
306    }
307
308    /// True if no bytes are currently buffered.
309    pub fn is_empty(&self) -> bool {
310        self.buf.is_empty()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    /// Helper: construct a minimal 188-byte TS packet buffer with given header flags and payload.
319    fn make_packet(b1: u8, b2: u8, b3: u8, payload_data: &[u8]) -> [u8; TS_PACKET_SIZE] {
320        let mut pkt = [0u8; TS_PACKET_SIZE];
321        pkt[0] = TS_SYNC_BYTE;
322        pkt[1] = b1;
323        pkt[2] = b2;
324        pkt[3] = b3;
325        let payload_start = 4;
326        let end = (payload_start + payload_data.len()).min(TS_PACKET_SIZE);
327        let len = (end - payload_start).min(payload_data.len());
328        pkt[payload_start..payload_start + len].copy_from_slice(&payload_data[..len]);
329        pkt
330    }
331
332    #[test]
333    fn parse_rejects_non_0x47_sync_byte() {
334        let mut pkt = [0u8; TS_PACKET_SIZE];
335        pkt[0] = 0x46; // wrong sync byte
336        let err = TsPacket::parse(&pkt).unwrap_err();
337        match err {
338            Error::InvalidSyncByte { found } => assert_eq!(found, 0x46),
339            other => panic!("expected InvalidSyncByte, got {other:?}"),
340        }
341    }
342
343    #[test]
344    fn parse_extracts_pid_and_continuity_counter() {
345        // PID = 0x1234 → upper 5 bits = 0x12, lower 8 bits = 0x34
346        // CC = 5 → 0x05
347        // b1 = 0x47 (sync=0, tei=0, pusi=0) | (0x12) = 0x47 & 0xE0 | 0x12 = 0x47 & 0xE0 = 0x40 | 0x12 = 0x52
348        // Actually: b1 bits: [tei:1][pusi:1][pid_hi:5]
349        // pid_hi = 0x12 = 0b00100_10 → bits 5..=1 = 0x12
350        // b1 = 0b00_010010 = 0x12 (no tei, no pusi)
351        let pkt = make_packet(0x12, 0x34, 0x05, &[]);
352        let pkt = TsPacket::parse(&pkt).unwrap();
353        assert_eq!(pkt.header.pid, 0x1234);
354        assert_eq!(pkt.header.continuity_counter, 5);
355    }
356
357    #[test]
358    fn payload_unit_start_indicator_flag_extracted() {
359        // b1 = 0x40 → pusi = true (bit 6 set, no tei, no pid bits)
360        let pkt1 = make_packet(0x40, 0x00, 0x00, &[]);
361        let pkt1 = TsPacket::parse(&pkt1).unwrap();
362        assert!(pkt1.header.pusi);
363
364        // b1 = 0x00 → pusi = false
365        let pkt2 = make_packet(0x00, 0x00, 0x00, &[]);
366        let pkt2 = TsPacket::parse(&pkt2).unwrap();
367        assert!(!pkt2.header.pusi);
368    }
369
370    /// Build a PSI-carrying TS payload: `pointer_field` byte followed by
371    /// (optionally) some tail of a previous section, followed by a fresh
372    /// section. `pointer_field` is the number of bytes of the previous
373    /// section that precede the new one (per ETSI EN 300 468 §5.1.4).
374    fn build_pusi_payload(pointer_field: u8, previous_tail: &[u8], section: &[u8]) -> Vec<u8> {
375        assert_eq!(pointer_field as usize, previous_tail.len());
376        let mut v = Vec::with_capacity(1 + previous_tail.len() + section.len());
377        v.push(pointer_field);
378        v.extend_from_slice(previous_tail);
379        v.extend_from_slice(section);
380        v
381    }
382
383    /// Build a long-form section with the given table_id and body bytes.
384    /// Returns the full section including its 3-byte + 5-byte header and a
385    /// placeholder CRC — for reassembler testing we don't validate the CRC.
386    fn build_section(table_id: u8, body_after_length: &[u8]) -> Vec<u8> {
387        let section_length = body_after_length.len() as u16;
388        let mut v = Vec::with_capacity(3 + section_length as usize);
389        v.push(table_id);
390        // ssi=1, pi=0, reserved=11, length hi 4 bits
391        v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
392        v.push((section_length & 0xFF) as u8);
393        v.extend_from_slice(body_after_length);
394        v
395    }
396
397    // The reassembler tests below feed raw payload slices directly to
398    // `feed()` rather than wrapping them in 188-byte TS packets. This avoids
399    // the TS stuffing-byte tail (0xFF padding) bleeding into the reassembled
400    // section and keeps the assertions exact.
401
402    #[test]
403    fn reassembler_accumulates_multi_packet_section() {
404        // 200-byte section that spans two payload slices.
405        let body = vec![0xAAu8; 197];
406        let section = build_section(0x02, &body);
407        assert_eq!(section.len(), 200);
408
409        let first_chunk = 100;
410        let payload1 = build_pusi_payload(0, &[], &section[..first_chunk]);
411        let payload2 = section[first_chunk..].to_vec();
412
413        let mut reasm = SectionReassembler::default();
414        reasm.feed(&payload1, true);
415        reasm.feed(&payload2, false);
416
417        let out = reasm.pop_section().expect("section should be ready");
418        assert_eq!(out.len(), 200);
419        assert_eq!(out.as_ref(), &section[..]);
420    }
421
422    #[test]
423    fn reassembler_yields_complete_section_once_length_satisfied() {
424        // 1-byte-body section: table_id=0x42, section_length=1, total=4 bytes.
425        let section = build_section(0x42, &[0xAA]);
426        assert_eq!(section.len(), 4);
427        let payload = build_pusi_payload(0, &[], &section);
428
429        let mut reasm = SectionReassembler::default();
430        reasm.feed(&payload, true);
431
432        let out = reasm
433            .pop_section()
434            .expect("single-packet section should pop");
435        assert_eq!(out.as_ref(), &section[..]);
436    }
437
438    #[test]
439    fn reassembler_extracts_all_concatenated_sections_in_one_payload() {
440        // Issue #29: a single PUSI payload packing three complete short
441        // sections after the pointer_field. All three must be queued — the
442        // old `feed` stopped after the first and the rest were silently lost
443        // (the CAS/EMM data-loss bug: SHARED EMMs landing as the 2nd+ section).
444        let s1 = build_section(0x42, &[0x11, 0x22]); // 5 bytes
445        let s2 = build_section(0x46, &[0x33]); // 4 bytes
446        let s3 = build_section(0x4A, &[0x44, 0x55, 0x66]); // 6 bytes
447
448        let mut concat = Vec::new();
449        concat.extend_from_slice(&s1);
450        concat.extend_from_slice(&s2);
451        concat.extend_from_slice(&s3);
452        let payload = build_pusi_payload(0, &[], &concat);
453
454        let mut reasm = SectionReassembler::default();
455        reasm.feed(&payload, true);
456
457        // Consumers must drain with a loop, not a single `if let`.
458        let got: Vec<_> = std::iter::from_fn(|| reasm.pop_section()).collect();
459        assert_eq!(got.len(), 3, "all three concatenated sections must pop");
460        assert_eq!(got[0].as_ref(), &s1[..]);
461        assert_eq!(got[1].as_ref(), &s2[..]);
462        assert_eq!(got[2].as_ref(), &s3[..]);
463    }
464
465    #[test]
466    fn reassembler_stops_at_stuffing_after_concatenated_sections() {
467        // Two sections then 0xFF stuffing fill — the stuffing must not be
468        // mistaken for a section header (0xFF table_id) nor leak into a
469        // section; both real sections still pop.
470        let s1 = build_section(0x42, &[0xAA]); // 4 bytes
471        let s2 = build_section(0x46, &[0xBB, 0xCC]); // 5 bytes
472        let mut concat = Vec::new();
473        concat.extend_from_slice(&s1);
474        concat.extend_from_slice(&s2);
475        concat.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); // stuffing tail
476        let payload = build_pusi_payload(0, &[], &concat);
477
478        let mut reasm = SectionReassembler::default();
479        reasm.feed(&payload, true);
480
481        let got: Vec<_> = std::iter::from_fn(|| reasm.pop_section()).collect();
482        assert_eq!(got.len(), 2);
483        assert_eq!(got[0].as_ref(), &s1[..]);
484        assert_eq!(got[1].as_ref(), &s2[..]);
485        assert!(
486            reasm.is_empty(),
487            "stuffing tail must be discarded, not buffered"
488        );
489    }
490
491    #[test]
492    fn reassembler_concatenated_then_spanning_tail() {
493        // One complete section followed by the head of a second that spans
494        // into a continuation packet: first pops immediately, second pops
495        // once the continuation arrives.
496        let s1 = build_section(0x42, &[0x01, 0x02]); // 5 bytes
497        let s2 = build_section(0x46, &[0x09u8; 60]); // 63 bytes
498        let split = 30;
499
500        let mut head = Vec::new();
501        head.extend_from_slice(&s1);
502        head.extend_from_slice(&s2[..split]);
503        let payload1 = build_pusi_payload(0, &[], &head);
504        let payload2 = s2[split..].to_vec();
505
506        let mut reasm = SectionReassembler::default();
507        reasm.feed(&payload1, true);
508        let first = reasm.pop_section().expect("first section pops at once");
509        assert_eq!(first.as_ref(), &s1[..]);
510        assert!(reasm.pop_section().is_none(), "second is still partial");
511
512        reasm.feed(&payload2, false);
513        let second = reasm.pop_section().expect("second pops after continuation");
514        assert_eq!(second.as_ref(), &s2[..]);
515    }
516
517    #[test]
518    fn reassembler_completes_section_spanning_into_pusi_packet() {
519        // Issue #29 (second case): a section starts late in packet A and spills
520        // into packet B, but B is itself PUSI=1 because new sections begin in it.
521        // B's pointer_field = the count of leading tail bytes belonging to the
522        // section from A. Those bytes MUST complete A's section before new
523        // sections start. 3.1.1 cleared buf + skipped them → the spanning
524        // section was lost (the SHARED EMM the smartcard needed).
525        let spanning = build_section(0x42, &[0x5Au8; 62]); // 65 bytes
526        let head = 41;
527        let tail = &spanning[head..]; // 24 bytes — lands in packet B
528        assert_eq!(tail.len(), 24);
529
530        // New section that begins in packet B after the spanning tail.
531        let next = build_section(0x46, &[0x77, 0x88]); // 5 bytes
532
533        // Packet A (PUSI): pointer 0, then the 41-byte head (incomplete).
534        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
535        // Packet B (PUSI): pointer = 24 (tail of A's section), then `next`.
536        let payload_b = build_pusi_payload(24, tail, &next);
537
538        let mut reasm = SectionReassembler::default();
539        reasm.feed(&payload_a, true);
540        assert!(reasm.pop_section().is_none(), "head alone is incomplete");
541
542        reasm.feed(&payload_b, true);
543        let got: Vec<_> = std::iter::from_fn(|| reasm.pop_section()).collect();
544        assert_eq!(got.len(), 2, "spanning section + new section must both pop");
545        assert_eq!(
546            got[0].as_ref(),
547            &spanning[..],
548            "spanning section completed from B's pointer tail"
549        );
550        assert_eq!(got[1].as_ref(), &next[..]);
551    }
552
553    #[test]
554    fn reassembler_pusi_pointer_spans_whole_payload() {
555        // A section spans into a PUSI packet whose pointer covers the ENTIRE
556        // remaining payload (no new section starts here) — the tail must be
557        // appended and the section completed once the count is satisfied.
558        let spanning = build_section(0x42, &[0x33u8; 40]); // 43 bytes
559        let head = 20;
560        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
561        let tail = &spanning[head..]; // 23 bytes — exactly the rest of payload B
562
563        let mut reasm = SectionReassembler::default();
564        reasm.feed(&payload_a, true);
565        // Packet B: pointer = 23 = all remaining bytes; no new section follows.
566        reasm.feed(&payload_b_pointer_only(tail), true);
567
568        let out = reasm.pop_section().expect("spanning section completes");
569        assert_eq!(out.as_ref(), &spanning[..]);
570        assert!(reasm.pop_section().is_none());
571    }
572
573    /// Build a PUSI payload whose `pointer_field` equals the whole tail (so the
574    /// pointer spans to the end of the payload and no new section starts).
575    fn payload_b_pointer_only(tail: &[u8]) -> Vec<u8> {
576        let mut v = Vec::with_capacity(1 + tail.len());
577        v.push(tail.len() as u8);
578        v.extend_from_slice(tail);
579        v
580    }
581
582    #[test]
583    fn reassembler_discards_on_buffer_overflow() {
584        // Declare section_length larger than a single payload can carry. No
585        // pop happens until continuations arrive; if continuations push the
586        // buffer past MAX_SECTION_SIZE the reassembler must reset, not panic.
587        let mut section = Vec::with_capacity(3 + 4095);
588        section.push(0x00); // table_id
589        section.push(0xB0 | ((4095u16 >> 8) as u8 & 0x0F));
590        section.push(0xFF);
591        section.extend_from_slice(&[0u8; 160]);
592        let payload1 = build_pusi_payload(0, &[], &section);
593
594        let mut reasm = SectionReassembler::default();
595        reasm.feed(&payload1, true);
596        assert!(reasm.pop_section().is_none());
597
598        // Push enough continuation data to cross MAX_SECTION_SIZE.
599        let filler = vec![0u8; 180];
600        for _ in 0..(MAX_SECTION_SIZE / 180 + 1) {
601            reasm.feed(&filler, false);
602        }
603        assert!(
604            reasm.pop_section().is_none(),
605            "no section should pop after overflow reset"
606        );
607
608        // State must be resettable — a fresh valid PUSI section works.
609        let valid_section = build_section(0x00, &[0xAA]);
610        let payload2 = build_pusi_payload(0, &[], &valid_section);
611        reasm.feed(&payload2, true);
612        let out = reasm
613            .pop_section()
614            .expect("fresh section should pop after reset");
615        assert_eq!(out.as_ref(), &valid_section[..]);
616    }
617
618    #[test]
619    fn reassembler_handles_pusi_with_nonzero_pointer_field() {
620        // payload = pointer_field=3, 3 bytes of prior-section tail, then new section.
621        let prior_tail = vec![0x11, 0x22, 0x33];
622        let new_section = build_section(0x02, &[0xBB]);
623        assert_eq!(new_section.len(), 4);
624        let payload = build_pusi_payload(3, &prior_tail, &new_section);
625
626        let mut reasm = SectionReassembler::default();
627        reasm.feed(&payload, true);
628
629        let out = reasm
630            .pop_section()
631            .expect("section after pointer_field skip should pop");
632        assert_eq!(out.as_ref(), &new_section[..]);
633    }
634
635    #[test]
636    fn reassembler_ignores_continuation_before_pusi() {
637        // Feed a non-PUSI payload first (no prior PUSI seen).
638        // SectionReassembler should discard it and stay empty.
639        let pkt = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xAA, 0xBB, 0xCC]);
640
641        let mut reasm = SectionReassembler::default();
642        reasm.feed(&pkt[4..], false); // no PUSI
643
644        assert!(
645            reasm.pop_section().is_none(),
646            "no section should appear without prior PUSI"
647        );
648        assert!(
649            reasm.pop_section().is_none(),
650            "second pop should also be none"
651        );
652    }
653
654    /// A PUSI packet with an empty payload (adaptation field ate the body)
655    /// is malformed but must not panic — it drops sync.
656    #[test]
657    fn reassembler_empty_pusi_payload_does_not_panic() {
658        let mut reasm = SectionReassembler::default();
659        reasm.feed(&[], true);
660        assert!(reasm.pop_section().is_none());
661        // Recovers on the next clean PUSI.
662        let mut payload = vec![0x00u8, 0x72, 0x70, 0x01, 0x00];
663        payload.resize(5, 0);
664        reasm.feed(&payload, true);
665        assert!(reasm.pop_section().is_some());
666    }
667
668    /// A maximal short-form private section (section_length 0xFFF, total
669    /// 4098 bytes) reassembles — the ceiling is 12-bit length + 3-byte
670    /// header, not 4096.
671    #[test]
672    fn reassembler_accepts_maximal_private_section() {
673        let mut section = vec![0x80u8, 0x7F, 0xFF]; // user-private tid, SSI=0, len 0xFFF
674        section.resize(3 + 0xFFF, 0xAB);
675
676        let mut reasm = SectionReassembler::default();
677        // First TS payload: pointer_field 0 then the section start.
678        let mut first = vec![0x00];
679        first.extend_from_slice(&section[..183]);
680        reasm.feed(&first, true);
681        for chunk in section[183..].chunks(184) {
682            reasm.feed(chunk, false);
683        }
684        let out = reasm.pop_section().expect("4098-byte section should pop");
685        assert_eq!(out.len(), 4098);
686        assert_eq!(out.as_ref(), &section[..]);
687    }
688}