Skip to main content

dvb_si/
collect.rs

1//! Multi-section table collection.
2//!
3//! Section parsers in [`crate::tables`] describe one wire section. This module
4//! adds the next layer up: collect all sections in `0..=last_section_number`
5//! for one logical version, then expose a complete table view.
6//!
7//! Collectors validate long-form section CRCs before retaining bytes. If the
8//! input already came from [`crate::demux::SiDemux`], that validation has
9//! already happened; direct section-byte callers get the same guard here.
10//!
11//! A collector error describes the section that was just pushed, not the whole
12//! stream. Long-running consumers should normally log/drop that section and
13//! continue feeding later sections; previous valid collector state is retained.
14
15use std::collections::{BTreeMap, HashMap};
16use std::sync::Arc;
17
18use crate::descriptors::{AnyDescriptor, DescriptorLoop, DescriptorRegistry};
19use crate::section::Section;
20use crate::tables::{bat, eit, nit, sdt};
21use dvb_common::Parse;
22
23/// Result alias for collection operations.
24pub type CollectResult<T> = core::result::Result<T, CollectError>;
25
26/// Errors returned by multi-section collectors.
27///
28/// These errors are scoped to the current input section. They usually mean
29/// "skip this section and keep going", especially on live streams where a
30/// broadcaster may mutate section bytes without bumping `version_number`.
31#[derive(Debug, thiserror::Error)]
32#[non_exhaustive]
33pub enum CollectError {
34    /// The section bytes did not parse as a generic PSI/SI section.
35    #[error("section parse failed: {0}")]
36    Section(#[from] crate::Error),
37
38    /// A short-form section was fed to a multi-section collector.
39    #[error(
40        "table_id {table_id:#04x} is a short-form section and cannot be multi-section collected"
41    )]
42    ShortFormSection {
43        /// Raw table_id byte.
44        table_id: u8,
45    },
46
47    /// `section_number` was outside the advertised section range.
48    #[error(
49        "section_number {section_number} exceeds last_section_number {last_section_number} for table_id {table_id:#04x}"
50    )]
51    SectionNumberOutOfRange {
52        /// Raw table_id byte.
53        table_id: u8,
54        /// Section number carried by the section.
55        section_number: u8,
56        /// Last section number carried by the section.
57        last_section_number: u8,
58    },
59
60    /// A slot already contained different bytes for the same version.
61    #[error("conflicting bytes for table_id {table_id:#04x} section {section_number}")]
62    ConflictingSection {
63        /// Raw table_id byte.
64        table_id: u8,
65        /// Section slot that conflicted.
66        section_number: u8,
67    },
68
69    /// An EIT schedule section advertised an impossible table-id range.
70    #[error(
71        "EIT schedule table_id {table_id:#04x} is outside advertised range {first_table_id:#04x}..={last_table_id:#04x}"
72    )]
73    EitTableIdOutOfRange {
74        /// Incoming EIT schedule table_id.
75        table_id: u8,
76        /// First table_id for this schedule kind.
77        first_table_id: u8,
78        /// Advertised last_table_id.
79        last_table_id: u8,
80    },
81}
82
83/// Logical key for one section sequence.
84///
85/// The key deliberately excludes `version_number` and `section_number`. Version
86/// changes reset a collection; section numbers index into that collection.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88pub struct SectionSetKey {
89    /// Optional PID context supplied by the caller.
90    pub pid: Option<u16>,
91    /// Raw `table_id`.
92    pub table_id: u8,
93    /// Long-form `table_id_extension`.
94    pub extension_id: u16,
95    /// `current_next_indicator`.
96    pub current_next_indicator: bool,
97}
98
99/// Metadata shared by every section in a complete section set.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct SectionSetMeta {
102    /// Logical section-set key.
103    pub key: SectionSetKey,
104    /// 5-bit `version_number`.
105    pub version_number: u8,
106    /// Last section number for this set.
107    pub last_section_number: u8,
108}
109
110#[derive(Debug)]
111struct PartialSectionSet {
112    meta: SectionSetMeta,
113    slots: Vec<Option<Arc<[u8]>>>,
114    filled: usize,
115    emitted: bool,
116}
117
118impl PartialSectionSet {
119    fn new(meta: SectionSetMeta) -> Self {
120        let len = meta.last_section_number as usize + 1;
121        Self {
122            meta,
123            slots: vec![None; len],
124            filled: 0,
125            emitted: false,
126        }
127    }
128
129    fn reset(&mut self, meta: SectionSetMeta) {
130        *self = Self::new(meta);
131    }
132
133    fn insert(&mut self, section_number: u8, bytes: Arc<[u8]>) -> CollectResult<bool> {
134        let index = section_number as usize;
135        if let Some(existing) = &self.slots[index] {
136            if existing.as_ref() == bytes.as_ref() {
137                return Ok(false);
138            }
139            return Err(CollectError::ConflictingSection {
140                table_id: self.meta.key.table_id,
141                section_number,
142            });
143        }
144
145        self.slots[index] = Some(bytes);
146        self.filled += 1;
147        self.emitted = false;
148        Ok(true)
149    }
150
151    fn complete(&self) -> bool {
152        self.filled == self.slots.len()
153    }
154
155    fn to_complete(&self) -> Option<CompleteSectionSet> {
156        if !self.complete() || self.emitted {
157            return None;
158        }
159
160        let sections = self
161            .slots
162            .iter()
163            .map(|slot| slot.as_ref().expect("complete set has no holes").clone())
164            .collect();
165        Some(CompleteSectionSet {
166            meta: self.meta,
167            sections,
168        })
169    }
170}
171
172/// Generic collector for long-form `section_number`/`last_section_number`
173/// sequences.
174#[derive(Debug, Default)]
175pub struct SectionSetCollector {
176    partial: HashMap<SectionSetKey, PartialSectionSet>,
177}
178
179/// EIT-specific collector.
180///
181/// Present/following EITs complete as one normal section set. Schedule EITs
182/// complete only when every schedule table_id from the kind's first table_id
183/// through the advertised `last_table_id` has completed its own section set.
184#[derive(Debug, Default)]
185pub struct EitCollector {
186    sections: HashMap<EitSectionSetKey, PartialEitSectionSet>,
187    schedules: HashMap<EitLogicalKey, PartialEitSchedule>,
188}
189
190impl EitCollector {
191    /// Create an empty EIT collector.
192    #[must_use]
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    /// Push one complete EIT section.
198    ///
199    /// Returns `Some` for a completed present/following table or a completed
200    /// schedule table-id range.
201    ///
202    /// # Errors
203    ///
204    /// Returns a [`CollectError`] if the incoming section is malformed,
205    /// inconsistent with already retained bytes, or not an EIT section. Treat
206    /// the error as applying to this section only unless your application wants
207    /// strict stream-fail behavior.
208    pub fn push_section(&mut self, bytes: impl AsRef<[u8]>) -> CollectResult<Option<CompletedEit>> {
209        self.push_section_with_pid(None, bytes)
210    }
211
212    /// Push one complete EIT section with PID context.
213    pub fn push_section_with_pid(
214        &mut self,
215        pid: Option<u16>,
216        bytes: impl AsRef<[u8]>,
217    ) -> CollectResult<Option<CompletedEit>> {
218        let raw = bytes.as_ref();
219        let section = Section::parse(raw)?;
220        if !section.section_syntax_indicator {
221            return Err(CollectError::ShortFormSection {
222                table_id: section.table_id,
223            });
224        }
225        if section.section_number > section.last_section_number {
226            return Err(CollectError::SectionNumberOutOfRange {
227                table_id: section.table_id,
228                section_number: section.section_number,
229                last_section_number: section.last_section_number,
230            });
231        }
232        section.validate_crc(raw)?;
233
234        let eit = eit::EitSection::parse(raw)?;
235        let logical_key = EitLogicalKey {
236            pid,
237            kind: eit.kind,
238            service_id: eit.service_id,
239            transport_stream_id: eit.transport_stream_id,
240            original_network_id: eit.original_network_id,
241            current_next_indicator: eit.current_next_indicator,
242        };
243        let key = EitSectionSetKey {
244            logical_key,
245            table_id: eit.table_id,
246        };
247        let meta = EitSectionSetMeta {
248            key,
249            version_number: eit.version_number,
250            last_section_number: eit.last_section_number,
251        };
252        let bytes: Arc<[u8]> = Arc::from(raw);
253
254        let partial = self
255            .sections
256            .entry(key)
257            .or_insert_with(|| PartialEitSectionSet::new(meta));
258        if partial.meta.version_number != meta.version_number
259            || partial.meta.last_section_number != meta.last_section_number
260        {
261            partial.reset(meta);
262        }
263
264        partial.insert(eit.section_number, bytes)?;
265        let complete = match partial.to_complete() {
266            Some(complete) => complete,
267            None => return Ok(None),
268        };
269        partial.emitted = true;
270
271        match eit.kind {
272            eit::EitKind::PresentFollowingActual | eit::EitKind::PresentFollowingOther => {
273                Ok(Some(CompletedEit::PresentFollowing(complete)))
274            }
275            eit::EitKind::ScheduleActual | eit::EitKind::ScheduleOther => {
276                let first_table_id = match eit.kind {
277                    eit::EitKind::ScheduleActual => eit::TABLE_ID_SCHEDULE_ACTUAL_FIRST,
278                    eit::EitKind::ScheduleOther => eit::TABLE_ID_SCHEDULE_OTHER_FIRST,
279                    _ => unreachable!("matched schedule kind above"),
280                };
281                if eit.table_id < first_table_id || eit.table_id > eit.last_table_id {
282                    return Err(CollectError::EitTableIdOutOfRange {
283                        table_id: eit.table_id,
284                        first_table_id,
285                        last_table_id: eit.last_table_id,
286                    });
287                }
288                let meta = EitScheduleMeta {
289                    key: logical_key,
290                    first_table_id,
291                    last_table_id: eit.last_table_id,
292                };
293                let schedule = self
294                    .schedules
295                    .entry(logical_key)
296                    .or_insert_with(|| PartialEitSchedule::new(meta));
297                if schedule.meta.last_table_id != meta.last_table_id {
298                    schedule.reset(meta);
299                }
300                schedule.insert(eit.table_id, complete);
301                if let Some(complete) = schedule.to_complete() {
302                    schedule.emitted = true;
303                    Ok(Some(CompletedEit::Schedule(complete)))
304                } else {
305                    Ok(None)
306                }
307            }
308        }
309    }
310
311    /// Drop all retained EIT partial and completed schedule state.
312    ///
313    /// Long-running receivers that collect EPG data continuously can call this
314    /// at an application-defined carousel boundary if they do not need older
315    /// schedule state.
316    pub fn clear(&mut self) {
317        self.sections.clear();
318        self.schedules.clear();
319    }
320
321    /// Retain only logical EIT keys accepted by `keep`.
322    ///
323    /// This is the explicit pruning hook for long-running EIT schedule
324    /// collection. Both in-progress section sets and completed schedule ranges
325    /// for rejected keys are removed.
326    pub fn retain_logical<F>(&mut self, mut keep: F)
327    where
328        F: FnMut(&EitLogicalKey) -> bool,
329    {
330        self.sections.retain(|key, _| keep(&key.logical_key));
331        self.schedules.retain(|key, _| keep(key));
332    }
333
334    /// Number of retained EIT section-set states.
335    #[must_use]
336    pub fn section_set_len(&self) -> usize {
337        self.sections.len()
338    }
339
340    /// Number of retained EIT logical schedule states.
341    #[must_use]
342    pub fn schedule_len(&self) -> usize {
343        self.schedules.len()
344    }
345}
346
347/// Completed EIT collection result.
348#[derive(Debug, Clone)]
349pub enum CompletedEit {
350    /// One completed present/following EIT section set.
351    PresentFollowing(CompleteSectionSet),
352    /// A completed schedule EIT range spanning one or more table IDs.
353    Schedule(CompleteEitSchedule),
354}
355
356impl CompletedEit {
357    /// Parse the completed EIT table(s) without a descriptor registry.
358    pub fn tables(&self) -> crate::Result<Vec<CompleteEit<'_>>> {
359        self.tables_with_registry(None)
360    }
361
362    /// Parse the completed EIT table(s) with an optional descriptor registry.
363    pub fn tables_with_registry<'a>(
364        &'a self,
365        registry: Option<&'a DescriptorRegistry>,
366    ) -> crate::Result<Vec<CompleteEit<'a>>> {
367        match self {
368            Self::PresentFollowing(set) => Ok(vec![CompleteEit::parse(set, registry)?]),
369            Self::Schedule(schedule) => schedule.tables_with_registry(registry),
370        }
371    }
372}
373
374/// Logical EIT table key used by [`EitCollector`].
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
376pub struct EitLogicalKey {
377    /// Optional PID context supplied by the caller.
378    pub pid: Option<u16>,
379    /// EIT kind derived from table_id.
380    pub kind: eit::EitKind,
381    /// service_id.
382    pub service_id: u16,
383    /// transport_stream_id.
384    pub transport_stream_id: u16,
385    /// original_network_id.
386    pub original_network_id: u16,
387    /// current_next_indicator.
388    pub current_next_indicator: bool,
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
392struct EitSectionSetKey {
393    logical_key: EitLogicalKey,
394    table_id: u8,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398struct EitScheduleMeta {
399    key: EitLogicalKey,
400    first_table_id: u8,
401    last_table_id: u8,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405struct EitSectionSetMeta {
406    key: EitSectionSetKey,
407    version_number: u8,
408    last_section_number: u8,
409}
410
411#[derive(Debug)]
412struct PartialEitSectionSet {
413    meta: EitSectionSetMeta,
414    slots: Vec<Option<Arc<[u8]>>>,
415    filled: usize,
416    emitted: bool,
417}
418
419impl PartialEitSectionSet {
420    fn new(meta: EitSectionSetMeta) -> Self {
421        let len = meta.last_section_number as usize + 1;
422        Self {
423            meta,
424            slots: vec![None; len],
425            filled: 0,
426            emitted: false,
427        }
428    }
429
430    fn reset(&mut self, meta: EitSectionSetMeta) {
431        *self = Self::new(meta);
432    }
433
434    fn insert(&mut self, section_number: u8, bytes: Arc<[u8]>) -> CollectResult<bool> {
435        let index = section_number as usize;
436        if let Some(existing) = &self.slots[index] {
437            if existing.as_ref() == bytes.as_ref() {
438                return Ok(false);
439            }
440            return Err(CollectError::ConflictingSection {
441                table_id: self.meta.key.table_id,
442                section_number,
443            });
444        }
445
446        self.slots[index] = Some(bytes);
447        self.filled += 1;
448        self.emitted = false;
449        Ok(true)
450    }
451
452    fn complete(&self) -> bool {
453        self.filled == self.slots.len()
454    }
455
456    fn to_complete(&self) -> Option<CompleteSectionSet> {
457        if !self.complete() || self.emitted {
458            return None;
459        }
460
461        let sections = self
462            .slots
463            .iter()
464            .map(|slot| {
465                slot.as_ref()
466                    .expect("complete EIT set has no holes")
467                    .clone()
468            })
469            .collect();
470        Some(CompleteSectionSet {
471            meta: SectionSetMeta {
472                key: SectionSetKey {
473                    pid: self.meta.key.logical_key.pid,
474                    table_id: self.meta.key.table_id,
475                    extension_id: self.meta.key.logical_key.service_id,
476                    current_next_indicator: self.meta.key.logical_key.current_next_indicator,
477                },
478                version_number: self.meta.version_number,
479                last_section_number: self.meta.last_section_number,
480            },
481            sections,
482        })
483    }
484}
485
486#[derive(Debug)]
487struct PartialEitSchedule {
488    meta: EitScheduleMeta,
489    table_sets: BTreeMap<u8, CompleteSectionSet>,
490    emitted: bool,
491}
492
493impl PartialEitSchedule {
494    fn new(meta: EitScheduleMeta) -> Self {
495        Self {
496            meta,
497            table_sets: BTreeMap::new(),
498            emitted: false,
499        }
500    }
501
502    fn reset(&mut self, meta: EitScheduleMeta) {
503        *self = Self::new(meta);
504    }
505
506    fn insert(&mut self, table_id: u8, set: CompleteSectionSet) {
507        self.table_sets.insert(table_id, set);
508        self.emitted = false;
509    }
510
511    fn complete(&self) -> bool {
512        (self.meta.first_table_id..=self.meta.last_table_id)
513            .all(|table_id| self.table_sets.contains_key(&table_id))
514    }
515
516    fn to_complete(&self) -> Option<CompleteEitSchedule> {
517        if !self.complete() || self.emitted {
518            return None;
519        }
520        let table_sets = (self.meta.first_table_id..=self.meta.last_table_id)
521            .map(|table_id| {
522                self.table_sets
523                    .get(&table_id)
524                    .expect("complete EIT schedule has no missing table IDs")
525                    .clone()
526            })
527            .collect();
528        Some(CompleteEitSchedule {
529            first_table_id: self.meta.first_table_id,
530            last_table_id: self.meta.last_table_id,
531            table_sets,
532        })
533    }
534}
535
536/// Completed EIT schedule spanning all schedule table IDs through
537/// `last_table_id`.
538#[derive(Debug, Clone)]
539pub struct CompleteEitSchedule {
540    first_table_id: u8,
541    last_table_id: u8,
542    table_sets: Vec<CompleteSectionSet>,
543}
544
545impl CompleteEitSchedule {
546    /// First schedule table_id in this range.
547    #[must_use]
548    pub const fn first_table_id(&self) -> u8 {
549        self.first_table_id
550    }
551
552    /// Last schedule table_id in this range.
553    #[must_use]
554    pub const fn last_table_id(&self) -> u8 {
555        self.last_table_id
556    }
557
558    /// Completed section sets, one per schedule table_id in order.
559    #[must_use]
560    pub fn table_sets(&self) -> &[CompleteSectionSet] {
561        &self.table_sets
562    }
563
564    /// Per-table_id 5-bit version numbers in schedule table_id order.
565    ///
566    /// DVB EIT schedule sub-tables version independently, so there is no single
567    /// schedule-wide version number.
568    pub fn table_versions(&self) -> impl ExactSizeIterator<Item = (u8, u8)> + '_ {
569        self.table_sets
570            .iter()
571            .map(|set| (set.meta.key.table_id, set.meta.version_number))
572    }
573
574    /// Parse each completed schedule table-id set.
575    pub fn tables(&self) -> crate::Result<Vec<CompleteEit<'_>>> {
576        self.tables_with_registry(None)
577    }
578
579    /// Parse each completed schedule table-id set with an optional descriptor
580    /// registry.
581    pub fn tables_with_registry<'a>(
582        &'a self,
583        registry: Option<&'a DescriptorRegistry>,
584    ) -> crate::Result<Vec<CompleteEit<'a>>> {
585        self.table_sets
586            .iter()
587            .map(|set| CompleteEit::parse(set, registry))
588            .collect()
589    }
590}
591
592impl SectionSetCollector {
593    /// Create an empty collector.
594    #[must_use]
595    pub fn new() -> Self {
596        Self::default()
597    }
598
599    /// Push one complete section. Returns `Some` only when the logical section
600    /// set has become complete for the first time at this version.
601    ///
602    /// # Errors
603    ///
604    /// Returns a [`CollectError`] if the bytes are not a valid long-form
605    /// section or if the section set becomes internally inconsistent. Treat the
606    /// error as applying to this section only unless your application wants
607    /// strict stream-fail behavior.
608    pub fn push_section(
609        &mut self,
610        bytes: impl AsRef<[u8]>,
611    ) -> CollectResult<Option<CompleteSectionSet>> {
612        self.push_section_with_pid(None, bytes)
613    }
614
615    /// Push one complete section with PID context.
616    ///
617    /// The PID is folded into the section-set key so tables with identical
618    /// table id/extension on different PIDs do not collide.
619    pub fn push_section_with_pid(
620        &mut self,
621        pid: Option<u16>,
622        bytes: impl AsRef<[u8]>,
623    ) -> CollectResult<Option<CompleteSectionSet>> {
624        let raw = bytes.as_ref();
625        let section = Section::parse(raw)?;
626        if !section.section_syntax_indicator {
627            return Err(CollectError::ShortFormSection {
628                table_id: section.table_id,
629            });
630        }
631        if section.section_number > section.last_section_number {
632            return Err(CollectError::SectionNumberOutOfRange {
633                table_id: section.table_id,
634                section_number: section.section_number,
635                last_section_number: section.last_section_number,
636            });
637        }
638        section.validate_crc(raw)?;
639
640        let key = SectionSetKey {
641            pid,
642            table_id: section.table_id,
643            extension_id: section.extension_id,
644            current_next_indicator: section.current_next_indicator,
645        };
646        let meta = SectionSetMeta {
647            key,
648            version_number: section.version_number,
649            last_section_number: section.last_section_number,
650        };
651        let bytes: Arc<[u8]> = Arc::from(raw);
652
653        let partial = self
654            .partial
655            .entry(key)
656            .or_insert_with(|| PartialSectionSet::new(meta));
657
658        if partial.meta.version_number != meta.version_number
659            || partial.meta.last_section_number != meta.last_section_number
660        {
661            partial.reset(meta);
662        }
663
664        partial.insert(section.section_number, bytes)?;
665        let complete = partial.to_complete();
666        if complete.is_some() {
667            partial.emitted = true;
668        }
669        Ok(complete)
670    }
671
672    /// Drop all retained partial section sets.
673    pub fn clear(&mut self) {
674        self.partial.clear();
675    }
676
677    /// Number of retained partial section-set states.
678    #[must_use]
679    pub fn len(&self) -> usize {
680        self.partial.len()
681    }
682
683    /// Whether the collector currently has no retained state.
684    #[must_use]
685    pub fn is_empty(&self) -> bool {
686        self.partial.is_empty()
687    }
688}
689
690/// A complete owned set of original section bytes for one logical section
691/// sequence.
692#[derive(Debug, Clone)]
693pub struct CompleteSectionSet {
694    meta: SectionSetMeta,
695    sections: Vec<Arc<[u8]>>,
696}
697
698/// Generic complete table view for one collected section set.
699///
700/// This is the all-table escape hatch: every long-form PSI/SI table with
701/// `section_number`/`last_section_number` can be collected into a
702/// [`CompleteSectionSet`] and parsed as `CompleteTable<T>`. Table-specific
703/// complete views such as [`CompleteNit`] add flattened convenience fields where
704/// the logical table shape is useful.
705#[derive(Debug)]
706pub struct CompleteTable<T> {
707    meta: SectionSetMeta,
708    sections: Vec<T>,
709}
710
711impl<T> CompleteTable<T> {
712    /// Metadata shared by the section set.
713    #[must_use]
714    pub const fn meta(&self) -> SectionSetMeta {
715        self.meta
716    }
717
718    /// Parsed sections in section-number order.
719    #[must_use]
720    pub fn sections(&self) -> &[T] {
721        &self.sections
722    }
723
724    /// Consume the complete table and return the parsed sections.
725    #[must_use]
726    pub fn into_sections(self) -> Vec<T> {
727        self.sections
728    }
729}
730
731impl CompleteSectionSet {
732    /// Metadata shared by the section set.
733    #[must_use]
734    pub const fn meta(&self) -> SectionSetMeta {
735        self.meta
736    }
737
738    /// Complete section bytes in section-number order.
739    #[must_use]
740    pub fn section_bytes(&self) -> impl ExactSizeIterator<Item = &[u8]> {
741        self.sections.iter().map(AsRef::as_ref)
742    }
743
744    /// Parse every section in this set as `T`.
745    ///
746    /// The parsed values borrow from this [`CompleteSectionSet`], so callers can
747    /// retain the set and use borrowed typed views without copying table loops.
748    pub fn parse_sections<'a, T>(&'a self) -> crate::Result<Vec<T>>
749    where
750        T: Parse<'a, Error = crate::Error>,
751    {
752        self.section_bytes().map(T::parse).collect()
753    }
754
755    /// Parse this set as a generic complete table.
756    ///
757    /// Use this for any long-form table that does not need a specialised
758    /// flattened logical view.
759    pub fn table<'a, T>(&'a self) -> crate::Result<CompleteTable<T>>
760    where
761        T: Parse<'a, Error = crate::Error>,
762    {
763        Ok(CompleteTable {
764            meta: self.meta,
765            sections: self.parse_sections()?,
766        })
767    }
768
769    /// Build a complete NIT view from this section set.
770    pub fn nit(&self) -> crate::Result<CompleteNit<'_>> {
771        CompleteNit::parse(self, None)
772    }
773
774    /// Build a complete NIT view using a descriptor registry.
775    pub fn nit_with_registry<'a>(
776        &'a self,
777        registry: &'a DescriptorRegistry,
778    ) -> crate::Result<CompleteNit<'a>> {
779        CompleteNit::parse(self, Some(registry))
780    }
781
782    /// Build a complete BAT view from this section set.
783    pub fn bat(&self) -> crate::Result<CompleteBat<'_>> {
784        CompleteBat::parse(self, None)
785    }
786
787    /// Build a complete BAT view using a descriptor registry.
788    pub fn bat_with_registry<'a>(
789        &'a self,
790        registry: &'a DescriptorRegistry,
791    ) -> crate::Result<CompleteBat<'a>> {
792        CompleteBat::parse(self, Some(registry))
793    }
794
795    /// Build a complete SDT view from this section set.
796    pub fn sdt(&self) -> crate::Result<CompleteSdt<'_>> {
797        CompleteSdt::parse(self, None)
798    }
799
800    /// Build a complete SDT view using a descriptor registry.
801    pub fn sdt_with_registry<'a>(
802        &'a self,
803        registry: &'a DescriptorRegistry,
804    ) -> crate::Result<CompleteSdt<'a>> {
805        CompleteSdt::parse(self, Some(registry))
806    }
807
808    /// Build a complete EIT view from this section set.
809    pub fn eit(&self) -> crate::Result<CompleteEit<'_>> {
810        CompleteEit::parse(self, None)
811    }
812
813    /// Build a complete EIT view using a descriptor registry.
814    pub fn eit_with_registry<'a>(
815        &'a self,
816        registry: &'a DescriptorRegistry,
817    ) -> crate::Result<CompleteEit<'a>> {
818        CompleteEit::parse(self, Some(registry))
819    }
820}
821
822/// Parsed descriptor loop retaining the raw bytes and the typed descriptor
823/// results.
824#[derive(Debug)]
825pub struct ParsedDescriptorLoop<'a> {
826    raw: DescriptorLoop<'a>,
827    descriptors: Vec<crate::Result<AnyDescriptor<'a>>>,
828}
829
830impl<'a> ParsedDescriptorLoop<'a> {
831    fn parse(raw: DescriptorLoop<'a>, registry: Option<&'a DescriptorRegistry>) -> Self {
832        let descriptors = match registry {
833            Some(registry) => registry.parse_loop(raw.raw()).collect(),
834            None => raw.iter().collect(),
835        };
836        Self { raw, descriptors }
837    }
838
839    /// Raw descriptor-loop bytes.
840    #[must_use]
841    pub const fn raw(&self) -> DescriptorLoop<'a> {
842        self.raw
843    }
844
845    /// Typed descriptor parse results in wire order.
846    pub fn descriptors(&self) -> &[crate::Result<AnyDescriptor<'a>>] {
847        &self.descriptors
848    }
849}
850
851/// Transport-stream entry in a complete NIT.
852#[derive(Debug)]
853pub struct CompleteNitTransportStream<'a> {
854    /// transport_stream_id of the described TS.
855    pub transport_stream_id: u16,
856    /// original_network_id of the described TS.
857    pub original_network_id: u16,
858    /// Typed descriptor loop for this transport stream.
859    pub descriptors: ParsedDescriptorLoop<'a>,
860}
861
862/// Complete logical Network Information Table.
863#[derive(Debug)]
864pub struct CompleteNit<'a> {
865    /// Variant discriminator.
866    pub kind: nit::NitKind,
867    /// Network identifier.
868    pub network_id: u16,
869    /// 5-bit version_number.
870    pub version_number: u8,
871    /// current_next_indicator bit.
872    pub current_next_indicator: bool,
873    /// Network-wide descriptors from section 0.
874    pub network_descriptors: ParsedDescriptorLoop<'a>,
875    /// Transport-stream loop entries from all sections in wire order.
876    pub transport_streams: Vec<CompleteNitTransportStream<'a>>,
877}
878
879impl<'a> CompleteNit<'a> {
880    fn parse(
881        set: &'a CompleteSectionSet,
882        registry: Option<&'a DescriptorRegistry>,
883    ) -> crate::Result<Self> {
884        let sections: Vec<nit::NitSection<'a>> = set.parse_sections()?;
885        let first = sections.first().ok_or(crate::Error::BufferTooShort {
886            need: 1,
887            have: 0,
888            what: "CompleteNit sections",
889        })?;
890        let mut transport_streams = Vec::new();
891        for section in &sections {
892            transport_streams.extend(section.transport_streams.iter().map(|ts| {
893                CompleteNitTransportStream {
894                    transport_stream_id: ts.transport_stream_id,
895                    original_network_id: ts.original_network_id,
896                    descriptors: ParsedDescriptorLoop::parse(ts.descriptors, registry),
897                }
898            }));
899        }
900        Ok(Self {
901            kind: first.kind,
902            network_id: first.network_id,
903            version_number: first.version_number,
904            current_next_indicator: first.current_next_indicator,
905            // The network descriptor loop is carried in section 0; completed
906            // sets are stored in section-number order, so `first` is
907            // authoritative for table-wide descriptors.
908            network_descriptors: ParsedDescriptorLoop::parse(first.network_descriptors, registry),
909            transport_streams,
910        })
911    }
912}
913
914/// Transport-stream entry in a complete BAT.
915#[derive(Debug)]
916pub struct CompleteBatTransportStream<'a> {
917    /// transport_stream_id of the described TS.
918    pub transport_stream_id: u16,
919    /// original_network_id of the described TS.
920    pub original_network_id: u16,
921    /// Typed descriptor loop for this transport stream.
922    pub descriptors: ParsedDescriptorLoop<'a>,
923}
924
925/// Complete logical Bouquet Association Table.
926#[derive(Debug)]
927pub struct CompleteBat<'a> {
928    /// Bouquet identifier.
929    pub bouquet_id: u16,
930    /// 5-bit version_number.
931    pub version_number: u8,
932    /// current_next_indicator bit.
933    pub current_next_indicator: bool,
934    /// Bouquet descriptors from section 0.
935    pub bouquet_descriptors: ParsedDescriptorLoop<'a>,
936    /// Transport-stream loop entries from all sections in wire order.
937    pub transport_streams: Vec<CompleteBatTransportStream<'a>>,
938}
939
940impl<'a> CompleteBat<'a> {
941    fn parse(
942        set: &'a CompleteSectionSet,
943        registry: Option<&'a DescriptorRegistry>,
944    ) -> crate::Result<Self> {
945        let sections: Vec<bat::BatSection<'a>> = set.parse_sections()?;
946        let first = sections.first().ok_or(crate::Error::BufferTooShort {
947            need: 1,
948            have: 0,
949            what: "CompleteBat sections",
950        })?;
951        let mut transport_streams = Vec::new();
952        for section in &sections {
953            transport_streams.extend(section.transport_streams.iter().map(|ts| {
954                CompleteBatTransportStream {
955                    transport_stream_id: ts.transport_stream_id,
956                    original_network_id: ts.original_network_id,
957                    descriptors: ParsedDescriptorLoop::parse(ts.descriptors, registry),
958                }
959            }));
960        }
961        Ok(Self {
962            bouquet_id: first.bouquet_id,
963            version_number: first.version_number,
964            current_next_indicator: first.current_next_indicator,
965            // The bouquet descriptor loop is carried in section 0; completed
966            // sets are stored in section-number order, so `first` is
967            // authoritative for table-wide descriptors.
968            bouquet_descriptors: ParsedDescriptorLoop::parse(first.bouquet_descriptors, registry),
969            transport_streams,
970        })
971    }
972}
973
974/// Service entry in a complete SDT.
975#[derive(Debug)]
976pub struct CompleteSdtService<'a> {
977    /// service_id.
978    pub service_id: u16,
979    /// EIT schedule flag.
980    pub eit_schedule_flag: bool,
981    /// EIT present/following flag.
982    pub eit_present_following_flag: bool,
983    /// 3-bit running status.
984    pub running_status: u8,
985    /// free_CA_mode.
986    pub free_ca_mode: bool,
987    /// Typed descriptor loop for this service.
988    pub descriptors: ParsedDescriptorLoop<'a>,
989}
990
991/// Complete logical Service Description Table.
992#[derive(Debug)]
993pub struct CompleteSdt<'a> {
994    /// Variant discriminator.
995    pub kind: sdt::SdtKind,
996    /// transport_stream_id of the described TS.
997    pub transport_stream_id: u16,
998    /// 5-bit version_number.
999    pub version_number: u8,
1000    /// current_next_indicator bit.
1001    pub current_next_indicator: bool,
1002    /// original_network_id of the described TS.
1003    pub original_network_id: u16,
1004    /// Services from all sections in wire order.
1005    pub services: Vec<CompleteSdtService<'a>>,
1006}
1007
1008impl<'a> CompleteSdt<'a> {
1009    fn parse(
1010        set: &'a CompleteSectionSet,
1011        registry: Option<&'a DescriptorRegistry>,
1012    ) -> crate::Result<Self> {
1013        let sections: Vec<sdt::SdtSection<'a>> = set.parse_sections()?;
1014        let first = sections.first().ok_or(crate::Error::BufferTooShort {
1015            need: 1,
1016            have: 0,
1017            what: "CompleteSdt sections",
1018        })?;
1019        let mut services = Vec::new();
1020        for section in &sections {
1021            services.extend(section.services.iter().map(|svc| CompleteSdtService {
1022                service_id: svc.service_id,
1023                eit_schedule_flag: svc.eit_schedule_flag,
1024                eit_present_following_flag: svc.eit_present_following_flag,
1025                running_status: svc.running_status,
1026                free_ca_mode: svc.free_ca_mode,
1027                descriptors: ParsedDescriptorLoop::parse(svc.descriptors, registry),
1028            }));
1029        }
1030        Ok(Self {
1031            kind: first.kind,
1032            transport_stream_id: first.transport_stream_id,
1033            version_number: first.version_number,
1034            current_next_indicator: first.current_next_indicator,
1035            original_network_id: first.original_network_id,
1036            services,
1037        })
1038    }
1039}
1040
1041/// Event entry in a complete EIT.
1042#[derive(Debug)]
1043pub struct CompleteEitEvent<'a> {
1044    /// 16-bit event_id.
1045    pub event_id: u16,
1046    /// 40-bit start time.
1047    pub start_time_raw: [u8; 5],
1048    /// 24-bit duration.
1049    pub duration_raw: [u8; 3],
1050    /// 3-bit running status.
1051    pub running_status: u8,
1052    /// free_CA_mode.
1053    pub free_ca_mode: bool,
1054    /// Typed descriptor loop for this event.
1055    pub descriptors: ParsedDescriptorLoop<'a>,
1056}
1057
1058impl CompleteEitEvent<'_> {
1059    /// Decode the 24-bit BCD `duration` (HHMMSS) to a [`core::time::Duration`].
1060    ///
1061    /// Returns `None` if the BCD nibbles are out of range.
1062    #[must_use]
1063    pub fn duration(&self) -> Option<core::time::Duration> {
1064        dvb_common::time::decode_bcd_duration(self.duration_raw)
1065    }
1066
1067    /// Decode `start_time_raw` (16-bit MJD + 24-bit BCD UTC) to a UTC datetime.
1068    ///
1069    /// Returns `None` if the date/time fields are out of range. MJD→calendar
1070    /// conversion per ETSI EN 300 468 Annex C.
1071    #[cfg(feature = "chrono")]
1072    #[must_use]
1073    pub fn start_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
1074        dvb_common::time::decode_mjd_bcd_utc(self.start_time_raw)
1075    }
1076}
1077
1078/// Complete EIT for one exact table_id/extension section sequence.
1079///
1080/// EIT schedule collection across `last_table_id` is intentionally represented
1081/// as multiple complete section sets: one per schedule table_id. That preserves
1082/// the DVB schedule sub-table structure while still exposing flattened events.
1083#[derive(Debug)]
1084pub struct CompleteEit<'a> {
1085    /// Variant based on table_id.
1086    pub kind: eit::EitKind,
1087    /// Raw table_id byte.
1088    pub table_id: u8,
1089    /// service_id.
1090    pub service_id: u16,
1091    /// 5-bit version_number.
1092    pub version_number: u8,
1093    /// current_next_indicator bit.
1094    pub current_next_indicator: bool,
1095    /// transport_stream_id.
1096    pub transport_stream_id: u16,
1097    /// original_network_id.
1098    pub original_network_id: u16,
1099    /// segment_last_section_number from section 0.
1100    pub segment_last_section_number: u8,
1101    /// last_table_id.
1102    pub last_table_id: u8,
1103    /// Events from all sections in wire order.
1104    pub events: Vec<CompleteEitEvent<'a>>,
1105}
1106
1107impl<'a> CompleteEit<'a> {
1108    fn parse(
1109        set: &'a CompleteSectionSet,
1110        registry: Option<&'a DescriptorRegistry>,
1111    ) -> crate::Result<Self> {
1112        let sections: Vec<eit::EitSection<'a>> = set.parse_sections()?;
1113        let first = sections.first().ok_or(crate::Error::BufferTooShort {
1114            need: 1,
1115            have: 0,
1116            what: "CompleteEit sections",
1117        })?;
1118        let mut events = Vec::new();
1119        for section in &sections {
1120            events.extend(section.events.iter().map(|event| CompleteEitEvent {
1121                event_id: event.event_id,
1122                start_time_raw: event.start_time_raw,
1123                duration_raw: event.duration_raw,
1124                running_status: event.running_status,
1125                free_ca_mode: event.free_ca_mode,
1126                descriptors: ParsedDescriptorLoop::parse(event.descriptors, registry),
1127            }));
1128        }
1129        Ok(Self {
1130            kind: first.kind,
1131            table_id: first.table_id,
1132            service_id: first.service_id,
1133            version_number: first.version_number,
1134            current_next_indicator: first.current_next_indicator,
1135            transport_stream_id: first.transport_stream_id,
1136            original_network_id: first.original_network_id,
1137            segment_last_section_number: first.segment_last_section_number,
1138            last_table_id: first.last_table_id,
1139            events,
1140        })
1141    }
1142}