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