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