Skip to main content

dvb_si/tables/
eit.rs

1//! Event Information Table — ETSI EN 300 468 §5.2.4.
2//!
3//! EIT carries programme event metadata. Four variants distinguished by
4//! table_id:
5//! - `0x4E` — Present/Following for the actual TS
6//! - `0x4F` — Present/Following for another TS
7//! - `0x50..=0x5F` — Schedule sub-tables for the actual TS
8//! - `0x60..=0x6F` — Schedule sub-tables for another TS
9
10use crate::descriptors::DescriptorLoop;
11use crate::error::{Error, Result};
12use dvb_common::{Parse, Serialize};
13
14/// table_id for present/following on the actual TS.
15pub const TABLE_ID_PF_ACTUAL: u8 = 0x4E;
16/// table_id for present/following on other TSes.
17pub const TABLE_ID_PF_OTHER: u8 = 0x4F;
18/// First table_id in the schedule range for the actual TS.
19pub const TABLE_ID_SCHEDULE_ACTUAL_FIRST: u8 = 0x50;
20/// Last table_id in the schedule range for the actual TS (inclusive).
21pub const TABLE_ID_SCHEDULE_ACTUAL_LAST: u8 = 0x5F;
22/// First table_id in the schedule range for other TSes.
23pub const TABLE_ID_SCHEDULE_OTHER_FIRST: u8 = 0x60;
24/// Last table_id in the schedule range for other TSes (inclusive).
25pub const TABLE_ID_SCHEDULE_OTHER_LAST: u8 = 0x6F;
26/// Well-known PID on which EIT is carried.
27pub const PID: u16 = 0x0012;
28
29const MIN_HEADER_LEN: usize = 3;
30const EXTENSION_HEADER_LEN: usize = 5;
31/// Bytes after the extension header: transport_stream_id(2) + original_network_id(2) + segment_last_section_number(1)
32/// + last_table_id(1) = 6 bytes between the section header and the first event.
33const POST_EXTENSION_LEN: usize = 6;
34const CRC_LEN: usize = 4;
35const MIN_SECTION_LEN: usize = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + CRC_LEN;
36const EVENT_HEADER_LEN: usize = 12;
37
38/// EIT variant distinguished by table_id range.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize))]
41pub enum EitKind {
42    /// Present/Following, actual TS.
43    PresentFollowingActual,
44    /// Present/Following, other TS.
45    PresentFollowingOther,
46    /// Schedule, actual TS — table_id `0x50..=0x5F`.
47    ScheduleActual,
48    /// Schedule, other TS — table_id `0x60..=0x6F`.
49    ScheduleOther,
50}
51
52impl EitKind {
53    /// Classify a table_id byte into a kind, if recognised.
54    #[must_use]
55    pub fn from_table_id(table_id: u8) -> Option<Self> {
56        match table_id {
57            TABLE_ID_PF_ACTUAL => Some(Self::PresentFollowingActual),
58            TABLE_ID_PF_OTHER => Some(Self::PresentFollowingOther),
59            TABLE_ID_SCHEDULE_ACTUAL_FIRST..=TABLE_ID_SCHEDULE_ACTUAL_LAST => {
60                Some(Self::ScheduleActual)
61            }
62            TABLE_ID_SCHEDULE_OTHER_FIRST..=TABLE_ID_SCHEDULE_OTHER_LAST => {
63                Some(Self::ScheduleOther)
64            }
65            _ => None,
66        }
67    }
68}
69
70/// One event in the EIT.
71#[derive(Debug, Clone, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize))]
73#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
74pub struct EitEvent<'a> {
75    /// 16-bit event_id.
76    pub event_id: u16,
77    /// 40-bit start_time: 16-bit MJD followed by 24-bit BCD UTC (HHMMSS).
78    pub start_time_raw: [u8; 5],
79    /// 24-bit BCD duration HHMMSS.
80    pub duration_raw: [u8; 3],
81    /// 3-bit running_status.
82    pub running_status: u8,
83    /// free_CA_mode flag.
84    pub free_ca_mode: bool,
85    /// Descriptor loop for this event. Serializes as the typed descriptor
86    /// sequence; `.raw()` yields the wire bytes.
87    pub descriptors: DescriptorLoop<'a>,
88}
89
90/// Event Information Table.
91#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
94pub struct EitSection<'a> {
95    /// Variant based on table_id.
96    pub kind: EitKind,
97    /// Raw table_id byte as parsed (for schedule sub-tables, identifies the slot).
98    pub table_id: u8,
99    /// service_id the events belong to (table_id_extension).
100    pub service_id: u16,
101    /// 5-bit version_number.
102    pub version_number: u8,
103    /// current_next_indicator bit.
104    pub current_next_indicator: bool,
105    /// section_number.
106    pub section_number: u8,
107    /// last_section_number.
108    pub last_section_number: u8,
109    /// transport_stream_id the events are carried on.
110    pub transport_stream_id: u16,
111    /// original_network_id.
112    pub original_network_id: u16,
113    /// segment_last_section_number.
114    pub segment_last_section_number: u8,
115    /// last_table_id (for schedule sub-table grouping).
116    pub last_table_id: u8,
117    /// Events in wire order.
118    pub events: Vec<EitEvent<'a>>,
119}
120
121impl<'a> Parse<'a> for EitSection<'a> {
122    type Error = crate::error::Error;
123    fn parse(bytes: &'a [u8]) -> Result<Self> {
124        let min_len = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + CRC_LEN;
125        if bytes.len() < min_len {
126            return Err(Error::BufferTooShort {
127                need: min_len,
128                have: bytes.len(),
129                what: "EitSection",
130            });
131        }
132
133        let table_id = bytes[0];
134        let kind = EitKind::from_table_id(table_id).ok_or(Error::UnexpectedTableId {
135            table_id,
136            what: "EitSection",
137            expected: &[
138                TABLE_ID_PF_ACTUAL,
139                TABLE_ID_PF_OTHER,
140                TABLE_ID_SCHEDULE_ACTUAL_FIRST,
141                TABLE_ID_SCHEDULE_OTHER_FIRST,
142            ],
143        })?;
144
145        let section_length = ((bytes[1] & 0x0F) as u16) << 8 | bytes[2] as u16;
146        let total = super::check_section_length(
147            bytes.len(),
148            MIN_HEADER_LEN,
149            section_length as usize,
150            MIN_SECTION_LEN,
151        )?;
152
153        let service_id = u16::from_be_bytes([bytes[3], bytes[4]]);
154        let version_number = (bytes[5] >> 1) & 0x1F;
155        let current_next_indicator = (bytes[5] & 0x01) != 0;
156        let section_number = bytes[6];
157        let last_section_number = bytes[7];
158
159        let transport_stream_id = u16::from_be_bytes([bytes[8], bytes[9]]);
160        let original_network_id = u16::from_be_bytes([bytes[10], bytes[11]]);
161        let segment_last_section_number = bytes[12];
162        let last_table_id = bytes[13];
163
164        let events_start = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN;
165        let events_end = total - CRC_LEN;
166        let mut events = Vec::new();
167        let mut pos = events_start;
168        while pos + EVENT_HEADER_LEN <= events_end {
169            let event_id = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]);
170            let start_time_raw = [
171                bytes[pos + 2],
172                bytes[pos + 3],
173                bytes[pos + 4],
174                bytes[pos + 5],
175                bytes[pos + 6],
176            ];
177            let duration_raw = [bytes[pos + 7], bytes[pos + 8], bytes[pos + 9]];
178            let status_and_len_hi = bytes[pos + 10];
179            let running_status = (status_and_len_hi >> 5) & 0x07;
180            let free_ca_mode = (status_and_len_hi & 0x10) != 0;
181            let descriptors_loop_length =
182                (((status_and_len_hi & 0x0F) as usize) << 8) | bytes[pos + 11] as usize;
183            let desc_start = pos + EVENT_HEADER_LEN;
184            let desc_end = desc_start + descriptors_loop_length;
185            if desc_end > events_end {
186                return Err(Error::SectionLengthOverflow {
187                    declared: descriptors_loop_length,
188                    available: events_end.saturating_sub(desc_start),
189                });
190            }
191            events.push(EitEvent {
192                event_id,
193                start_time_raw,
194                duration_raw,
195                running_status,
196                free_ca_mode,
197                descriptors: DescriptorLoop::new(&bytes[desc_start..desc_end]),
198            });
199            pos = desc_end;
200        }
201
202        if pos != events_end {
203            return Err(Error::BufferTooShort {
204                need: events_end - pos,
205                have: 0,
206                what: "EitSection trailing event bytes",
207            });
208        }
209
210        Ok(EitSection {
211            kind,
212            table_id,
213            service_id,
214            version_number,
215            current_next_indicator,
216            section_number,
217            last_section_number,
218            transport_stream_id,
219            original_network_id,
220            segment_last_section_number,
221            last_table_id,
222            events,
223        })
224    }
225}
226
227impl Serialize for EitSection<'_> {
228    type Error = crate::error::Error;
229    fn serialized_len(&self) -> usize {
230        let ev_bytes: usize = self
231            .events
232            .iter()
233            .map(|e| EVENT_HEADER_LEN + e.descriptors.len())
234            .sum();
235        MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + ev_bytes + CRC_LEN
236    }
237
238    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
239        let len = self.serialized_len();
240        if buf.len() < len {
241            return Err(Error::OutputBufferTooSmall {
242                need: len,
243                have: buf.len(),
244            });
245        }
246        let section_length_usize = len - MIN_HEADER_LEN;
247        if section_length_usize > 0x0FFF {
248            return Err(Error::SectionLengthOverflow {
249                declared: section_length_usize,
250                available: 0x0FFF,
251            });
252        }
253        let section_length: u16 = section_length_usize as u16;
254        buf[0] = self.table_id;
255        buf[1] = super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F);
256        buf[2] = (section_length & 0xFF) as u8;
257        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
258        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
259        buf[6] = self.section_number;
260        buf[7] = self.last_section_number;
261        buf[8..10].copy_from_slice(&self.transport_stream_id.to_be_bytes());
262        buf[10..12].copy_from_slice(&self.original_network_id.to_be_bytes());
263        buf[12] = self.segment_last_section_number;
264        buf[13] = self.last_table_id;
265
266        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXTENSION_LEN;
267        for ev in &self.events {
268            buf[pos..pos + 2].copy_from_slice(&ev.event_id.to_be_bytes());
269            buf[pos + 2..pos + 7].copy_from_slice(&ev.start_time_raw);
270            buf[pos + 7..pos + 10].copy_from_slice(&ev.duration_raw);
271            let dll = ev.descriptors.len() as u16;
272            buf[pos + 10] = ((ev.running_status & 0x07) << 5)
273                | (u8::from(ev.free_ca_mode) << 4)
274                | ((dll >> 8) as u8 & 0x0F);
275            buf[pos + 11] = (dll & 0xFF) as u8;
276            let desc_start = pos + EVENT_HEADER_LEN;
277            buf[desc_start..desc_start + ev.descriptors.len()]
278                .copy_from_slice(ev.descriptors.raw());
279            pos = desc_start + ev.descriptors.len();
280        }
281
282        let crc_pos = len - CRC_LEN;
283        let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
284        buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
285        Ok(len)
286    }
287}
288impl<'a> crate::traits::TableDef<'a> for EitSection<'a> {
289    const TABLE_ID_RANGES: &'static [(u8, u8)] =
290        &[(TABLE_ID_PF_ACTUAL, TABLE_ID_SCHEDULE_OTHER_LAST)];
291    const NAME: &'static str = "EVENT_INFORMATION";
292}
293
294impl EitEvent<'_> {
295    /// Decode the 24-bit BCD `duration` (HHMMSS) to a [`core::time::Duration`].
296    ///
297    /// Returns `None` if the BCD nibbles are out of range. Available without the
298    /// `chrono` feature — a duration is a plain elapsed-seconds value.
299    #[must_use]
300    pub fn duration(&self) -> Option<core::time::Duration> {
301        dvb_common::time::decode_bcd_duration(self.duration_raw)
302    }
303
304    /// Set the event duration, encoding it into the 24-bit BCD `duration` field.
305    ///
306    /// # Errors
307    /// [`ValueOutOfRange`](crate::Error::ValueOutOfRange) if the duration
308    /// is 100 hours or longer (the `HH` field holds only two BCD digits).
309    pub fn set_duration(&mut self, duration: core::time::Duration) -> crate::Result<()> {
310        self.duration_raw = dvb_common::time::encode_bcd_duration(duration).ok_or(
311            crate::Error::ValueOutOfRange {
312                field: "EitEvent::duration",
313                reason: "duration must be < 100 hours",
314            },
315        )?;
316        Ok(())
317    }
318}
319
320#[cfg(feature = "chrono")]
321impl EitEvent<'_> {
322    /// Decode `start_time_raw` (16-bit MJD + 24-bit BCD UTC) to a UTC datetime.
323    ///
324    /// Returns `None` if the date/time fields are out of range. MJD→calendar
325    /// conversion per ETSI EN 300 468 Annex C.
326    #[must_use]
327    pub fn start_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
328        dvb_common::time::decode_mjd_bcd_utc(self.start_time_raw)
329    }
330
331    /// Set the event start time, encoding it into the 40-bit `start_time` field.
332    ///
333    /// # Errors
334    /// [`ValueOutOfRange`](crate::Error::ValueOutOfRange) if the date is
335    /// outside the representable 16-bit MJD range.
336    pub fn set_start_time(
337        &mut self,
338        start_time: chrono::DateTime<chrono::Utc>,
339    ) -> crate::Result<()> {
340        self.start_time_raw = dvb_common::time::encode_mjd_bcd_utc(start_time).ok_or(
341            crate::Error::ValueOutOfRange {
342                field: "EitEvent::start_time",
343                reason: "date not representable in 16-bit MJD",
344            },
345        )?;
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    type TestEvent = (u16, [u8; 5], [u8; 3], u8, bool, Vec<u8>);
355
356    fn build_eit(
357        table_id: u8,
358        service_id: u16,
359        version: u8,
360        tsid: u16,
361        onid: u16,
362        events: &[TestEvent],
363    ) -> Vec<u8> {
364        let ev_bytes: usize = events
365            .iter()
366            .map(|(_, _, _, _, _, d)| EVENT_HEADER_LEN + d.len())
367            .sum();
368        let section_length: u16 =
369            (EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + ev_bytes + CRC_LEN) as u16;
370        let mut v = Vec::new();
371        v.push(table_id);
372        v.push(super::super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F));
373        v.push((section_length & 0xFF) as u8);
374        v.extend_from_slice(&service_id.to_be_bytes());
375        v.push(0xC0 | ((version & 0x1F) << 1) | 0x01);
376        v.push(0);
377        v.push(0);
378        v.extend_from_slice(&tsid.to_be_bytes());
379        v.extend_from_slice(&onid.to_be_bytes());
380        v.push(0);
381        v.push(table_id);
382        for (eid, start, dur, rs, fca, desc) in events {
383            v.extend_from_slice(&eid.to_be_bytes());
384            v.extend_from_slice(start);
385            v.extend_from_slice(dur);
386            let dll = desc.len() as u16;
387            v.push(((*rs & 0x07) << 5) | (u8::from(*fca) << 4) | ((dll >> 8) as u8 & 0x0F));
388            v.push((dll & 0xFF) as u8);
389            v.extend_from_slice(desc);
390        }
391        v.extend_from_slice(&[0, 0, 0, 0]);
392        v
393    }
394
395    #[test]
396    fn parse_pf_actual_and_other_map_to_correct_kind() {
397        for (tid, expected) in [
398            (TABLE_ID_PF_ACTUAL, EitKind::PresentFollowingActual),
399            (TABLE_ID_PF_OTHER, EitKind::PresentFollowingOther),
400        ] {
401            let bytes = build_eit(tid, 1, 0, 0x20, 0x30, &[]);
402            assert_eq!(EitSection::parse(&bytes).unwrap().kind, expected);
403        }
404    }
405
406    #[test]
407    fn schedule_tables_0x50_through_0x5f_all_decode_as_schedule_actual() {
408        for tid in TABLE_ID_SCHEDULE_ACTUAL_FIRST..=TABLE_ID_SCHEDULE_ACTUAL_LAST {
409            let bytes = build_eit(tid, 1, 0, 0x20, 0x30, &[]);
410            assert_eq!(
411                EitSection::parse(&bytes).unwrap().kind,
412                EitKind::ScheduleActual
413            );
414        }
415    }
416
417    #[test]
418    fn schedule_tables_0x60_through_0x6f_all_decode_as_schedule_other() {
419        for tid in TABLE_ID_SCHEDULE_OTHER_FIRST..=TABLE_ID_SCHEDULE_OTHER_LAST {
420            let bytes = build_eit(tid, 1, 0, 0x20, 0x30, &[]);
421            assert_eq!(
422                EitSection::parse(&bytes).unwrap().kind,
423                EitKind::ScheduleOther
424            );
425        }
426    }
427
428    #[test]
429    fn event_loop_with_descriptor_bytes_preserved() {
430        let desc = vec![0x4D, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
431        let bytes = build_eit(
432            TABLE_ID_PF_ACTUAL,
433            1,
434            0,
435            0x20,
436            0x30,
437            &[(
438                42,
439                [0xDF, 0xA1, 0x12, 0x34, 0x56],
440                [0x00, 0x30, 0x00],
441                4,
442                false,
443                desc.clone(),
444            )],
445        );
446        let eit = EitSection::parse(&bytes).unwrap();
447        assert_eq!(eit.events.len(), 1);
448        assert_eq!(eit.events[0].event_id, 42);
449        assert_eq!(eit.events[0].descriptors.raw(), &desc[..]);
450    }
451
452    #[test]
453    fn running_status_extracted() {
454        let bytes = build_eit(
455            TABLE_ID_PF_ACTUAL,
456            1,
457            0,
458            0x20,
459            0x30,
460            &[(1, [0; 5], [0; 3], 2, false, vec![])],
461        );
462        assert_eq!(
463            EitSection::parse(&bytes).unwrap().events[0].running_status,
464            2
465        );
466    }
467
468    #[test]
469    fn free_ca_mode_flag_extracted() {
470        let bytes = build_eit(
471            TABLE_ID_PF_ACTUAL,
472            1,
473            0,
474            0x20,
475            0x30,
476            &[(1, [0; 5], [0; 3], 0, true, vec![])],
477        );
478        assert!(EitSection::parse(&bytes).unwrap().events[0].free_ca_mode);
479    }
480
481    #[test]
482    fn serialize_round_trip_preserves_all_events() {
483        let desc1: [u8; 2] = [0x54, 0x00];
484        let eit = EitSection {
485            kind: EitKind::PresentFollowingActual,
486            table_id: TABLE_ID_PF_ACTUAL,
487            service_id: 0x0100,
488            version_number: 3,
489            current_next_indicator: true,
490            section_number: 0,
491            last_section_number: 0,
492            transport_stream_id: 0x1234,
493            original_network_id: 0x0020,
494            segment_last_section_number: 0,
495            last_table_id: TABLE_ID_PF_ACTUAL,
496            events: vec![
497                EitEvent {
498                    event_id: 1,
499                    start_time_raw: [0xDF, 0xA1, 0x12, 0x34, 0x56],
500                    duration_raw: [0x00, 0x30, 0x00],
501                    running_status: 4,
502                    free_ca_mode: false,
503                    descriptors: DescriptorLoop::new(&desc1),
504                },
505                EitEvent {
506                    event_id: 2,
507                    start_time_raw: [0xDF, 0xA1, 0x13, 0x00, 0x00],
508                    duration_raw: [0x01, 0x00, 0x00],
509                    running_status: 1,
510                    free_ca_mode: true,
511                    descriptors: DescriptorLoop::new(&[]),
512                },
513            ],
514        };
515        let mut buf = vec![0u8; eit.serialized_len()];
516        eit.serialize_into(&mut buf).unwrap();
517        let re = EitSection::parse(&buf).unwrap();
518        assert_eq!(eit, re);
519    }
520
521    #[test]
522    fn zero_events_is_valid() {
523        let bytes = build_eit(TABLE_ID_PF_ACTUAL, 1, 0, 0x20, 0x30, &[]);
524        let eit = EitSection::parse(&bytes).unwrap();
525        assert_eq!(eit.events.len(), 0);
526    }
527
528    #[test]
529    #[cfg(feature = "chrono")]
530    fn event_start_time_decodes_to_utc_datetime() {
531        // MJD 59945 is 2023-01-01 per ETSI EN 300 468 Annex C; BCD time 12:34:56.
532        let mjd: u16 = 59945;
533        let ev = EitEvent {
534            event_id: 1,
535            start_time_raw: [(mjd >> 8) as u8, (mjd & 0xFF) as u8, 0x12, 0x34, 0x56],
536            duration_raw: [0, 0, 0],
537            running_status: 0,
538            free_ca_mode: false,
539            descriptors: DescriptorLoop::new(&[]),
540        };
541        let dt = ev.start_time().unwrap();
542        use chrono::Datelike;
543        assert_eq!(dt.year(), 2023);
544        assert_eq!(dt.month(), 1);
545        assert_eq!(dt.day(), 1);
546        use chrono::Timelike;
547        assert_eq!(dt.hour(), 12);
548        assert_eq!(dt.minute(), 34);
549        assert_eq!(dt.second(), 56);
550    }
551
552    #[test]
553    fn parse_rejects_wrong_tag() {
554        let bytes = build_eit(0x00, 1, 0, 0x20, 0x30, &[]);
555        let err = EitSection::parse(&bytes).unwrap_err();
556        assert!(matches!(
557            err,
558            Error::UnexpectedTableId { table_id: 0x00, .. }
559        ));
560    }
561
562    #[test]
563    fn parse_rejects_truncated_header() {
564        let bytes = [0x4Eu8, 0xF0, 0x00];
565        let err = EitSection::parse(&bytes).unwrap_err();
566        assert!(matches!(
567            err,
568            Error::BufferTooShort {
569                need: 18,
570                have: 3,
571                ..
572            }
573        ));
574    }
575
576    #[test]
577    fn parse_rejects_event_descriptor_loop_overflow() {
578        let section_length: u16 =
579            (EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + EVENT_HEADER_LEN + CRC_LEN) as u16;
580        let mut v = Vec::new();
581        v.push(TABLE_ID_PF_ACTUAL);
582        v.push(super::super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F));
583        v.push((section_length & 0xFF) as u8);
584        v.extend_from_slice(&1u16.to_be_bytes());
585        v.push(0xC1);
586        v.push(0);
587        v.push(0);
588        v.extend_from_slice(&0x0020u16.to_be_bytes());
589        v.extend_from_slice(&0x0030u16.to_be_bytes());
590        v.push(0);
591        v.push(TABLE_ID_PF_ACTUAL);
592        v.extend_from_slice(&1u16.to_be_bytes());
593        v.extend_from_slice(&[0u8; 5]);
594        v.extend_from_slice(&[0u8; 3]);
595        v.push(0x00);
596        v.push(0x0A);
597        v.extend_from_slice(&[0u8; 4]);
598        // descriptor_loop_length=10 but events_end is at the CRC start:
599        // the declared 10 bytes overflow past the CRC boundary.
600        let err = EitSection::parse(&v).unwrap_err();
601        assert!(matches!(
602            err,
603            Error::SectionLengthOverflow { declared: 10, .. }
604        ));
605    }
606
607    #[test]
608    fn structured_fields_segment_and_last_table_id_preserved() {
609        let desc: [u8; 2] = [0x54, 0x00];
610        let bytes = build_eit(
611            TABLE_ID_SCHEDULE_ACTUAL_FIRST,
612            0x0100,
613            7,
614            0x0020,
615            0x0030,
616            &[(
617                42,
618                [0xDF, 0xA1, 0x12, 0x34, 0x56],
619                [0x00, 0x30, 0x00],
620                4,
621                false,
622                desc.to_vec(),
623            )],
624        );
625        let eit = EitSection::parse(&bytes).unwrap();
626        assert_eq!(eit.kind, EitKind::ScheduleActual);
627        assert_eq!(eit.table_id, TABLE_ID_SCHEDULE_ACTUAL_FIRST);
628        assert_eq!(eit.service_id, 0x0100);
629        assert_eq!(eit.version_number, 7);
630        assert!(eit.current_next_indicator);
631        assert_eq!(eit.section_number, 0);
632        assert_eq!(eit.last_section_number, 0);
633        assert_eq!(eit.transport_stream_id, 0x0020);
634        assert_eq!(eit.original_network_id, 0x0030);
635        assert_eq!(eit.segment_last_section_number, 0);
636        assert_eq!(eit.last_table_id, TABLE_ID_SCHEDULE_ACTUAL_FIRST);
637        assert_eq!(eit.events.len(), 1);
638        assert_eq!(eit.events[0].event_id, 42);
639        assert_eq!(eit.events[0].running_status, 4);
640        assert!(!eit.events[0].free_ca_mode);
641        // 12-bit descriptor loop length decoded correctly: 2 bytes of desc.
642        assert_eq!(eit.events[0].descriptors.raw(), &desc[..]);
643    }
644
645    #[test]
646    fn parse_rejects_zero_section_length() {
647        let mut buf = vec![0u8; 64];
648        buf[0] = TABLE_ID_PF_ACTUAL;
649        buf[1] = 0xF0;
650        buf[2] = 0x00;
651        for b in &mut buf[3..] {
652            *b = 0xFF;
653        }
654        assert!(matches!(
655            EitSection::parse(&buf).unwrap_err(),
656            Error::SectionLengthOverflow { .. }
657        ));
658    }
659
660    #[test]
661    fn parse_rejects_trailing_slack_bytes() {
662        let section_length: u16 =
663            (EXTENSION_HEADER_LEN + POST_EXTENSION_LEN + EVENT_HEADER_LEN + 1 + CRC_LEN) as u16;
664        let mut v = Vec::new();
665        v.push(TABLE_ID_PF_ACTUAL);
666        v.push(super::super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F));
667        v.push((section_length & 0xFF) as u8);
668        v.extend_from_slice(&1u16.to_be_bytes());
669        v.push(0xC1);
670        v.push(0);
671        v.push(0);
672        v.extend_from_slice(&0x0020u16.to_be_bytes());
673        v.extend_from_slice(&0x0030u16.to_be_bytes());
674        v.push(0);
675        v.push(TABLE_ID_PF_ACTUAL);
676        v.extend_from_slice(&1u16.to_be_bytes());
677        v.extend_from_slice(&[0u8; 5]);
678        v.extend_from_slice(&[0u8; 3]);
679        v.push(0x00);
680        v.push(0x00);
681        v.push(0xFF);
682        v.extend_from_slice(&[0u8; 4]);
683        let err = EitSection::parse(&v).unwrap_err();
684        assert!(matches!(
685            err,
686            Error::BufferTooShort {
687                what: "EitSection trailing event bytes",
688                ..
689            }
690        ));
691    }
692}