Skip to main content

dvb_si/tables/
rct.rs

1//! Related Content Table — ETSI TS 102 323 v1.4.1 §10.4.
2//!
3//! Signals links to related material for a service. Carried in the ES whose
4//! PID is named by a `related_content_descriptor` in that service's PMT
5//! (stream_type 0x05, private sections). There is no fixed PID.
6//!
7//! The link_info loop is unfolded into [`LinkInfo`] entries (Table 110, §10.4.3)
8//! with the conditional `dvb_binary_locator` sub-structure typed as
9//! [`DvbBinaryLocator`] (Table 31, §7.3.2.3.3).
10
11use crate::descriptors::DescriptorLoop;
12
13/// Identifier type — ETSI TS 102 323 v1.4.1 §7.3.2.3.3 Table 32
14/// (`docs/ts_102_323_tva.md`, Table 32 — Identifier type).
15///
16/// 2-bit field in the DVB binary locator. Selects what kind of event identifier
17/// field (if any) is present.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20#[non_exhaustive]
21pub enum IdentifierType {
22    /// `0b00` — no event identifier field is present.
23    None,
24    /// `0b01` — event identifier is an `event_id`.
25    EventId,
26    /// `0b10` — event identifier is a `TVA_id` carried in EIT.
27    TvaIdEit,
28    /// `0b11` — event identifier is a `TVA_id` carried in PES.
29    TvaIdPes,
30}
31
32impl IdentifierType {
33    #[must_use]
34    /// Creates a value from a 2-bit wire nibble (upper bits masked off).
35    pub fn from_u8(v: u8) -> Self {
36        match v & 0x03 {
37            0x00 => Self::None,
38            0x01 => Self::EventId,
39            0x02 => Self::TvaIdEit,
40            0x03 => Self::TvaIdPes,
41            // unreachable after masking to 2 bits, but the compiler requires it
42            _ => unreachable!(),
43        }
44    }
45
46    #[must_use]
47    /// Returns the 2-bit wire nibble for this value.
48    pub fn to_u8(self) -> u8 {
49        match self {
50            Self::None => 0x00,
51            Self::EventId => 0x01,
52            Self::TvaIdEit => 0x02,
53            Self::TvaIdPes => 0x03,
54        }
55    }
56
57    #[must_use]
58    /// Returns the spec token for this value.
59    pub fn name(self) -> &'static str {
60        match self {
61            Self::None => "none",
62            Self::EventId => "event_id",
63            Self::TvaIdEit => "TVA_id (EIT)",
64            Self::TvaIdPes => "TVA_id (PES)",
65        }
66    }
67}
68dvb_common::impl_spec_display!(IdentifierType);
69use crate::error::{Error, Result};
70use crate::text::{DvbText, LangCode};
71use alloc::vec::Vec;
72use dvb_common::{Parse, Serialize};
73
74/// `table_id` for Related Content Table.
75pub const TABLE_ID: u8 = 0x76;
76
77/// Well-known PID on which RCT is carried: none (signalled via PMT).
78pub const PID: u16 = 0x0000;
79
80const MIN_HEADER_LEN: usize = 3;
81const EXTENSION_HEADER_LEN: usize = 5;
82const POST_EXT_FIXED_LEN: usize = 3;
83const LINK_ENTRY_HEADER_LEN: usize = 2;
84const DESC_LOOP_LEN_FIELD: usize = 2;
85const CRC_LEN: usize = 4;
86const MIN_SECTION_LEN: usize =
87    MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN + DESC_LOOP_LEN_FIELD + CRC_LEN;
88
89const LINK_TYPE_MASK: u8 = 0xF0;
90const LINK_TYPE_SHIFT: u8 = 4;
91const HOW_RELATED_HI_MASK: u8 = 0x03;
92const TERM_ID_HI_MASK: u8 = 0x0F;
93const TERM_ID_HI_SHIFT: u8 = 8;
94const GROUP_ID_MASK: u8 = 0xF0;
95const GROUP_ID_SHIFT: u8 = 4;
96const PRECEDENCE_MASK: u8 = 0x0F;
97
98const LOCATOR_ID_TYPE_MASK: u8 = 0xC0;
99const LOCATOR_ID_TYPE_SHIFT: u8 = 6;
100const LOCATOR_RELIABILITY_MASK: u8 = 0x20;
101const LOCATOR_INLINE_MASK: u8 = 0x10;
102const LOCATOR_START_DATE_HI_MASK: u8 = 0x07;
103const LOCATOR_START_DATE_HI_SHIFT: usize = 6;
104const LOCATOR_RESERVED_BITS: u8 = 0x08;
105
106const ITEM_RFU_MASK: u8 = 0xC0;
107const ITEM_COUNT_MASK: u8 = 0x3F;
108
109const LINK_INFO_HEADER_RFU: u8 = 0x0C;
110
111const ICON_FLAG_MASK: u8 = 0x80;
112const ICON_ID_MASK: u8 = 0x70;
113const ICON_ID_SHIFT: u8 = 4;
114const ICON_DESC_LEN_HI_MASK: u8 = 0x0F;
115
116/// Link type — ETSI TS 102 323 §10.4.3 Table 111.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize))]
119#[non_exhaustive]
120pub enum LinkType {
121    /// 0x0 — URI string only.
122    UriString,
123    /// 0x1 — Binary locator only.
124    BinaryLocator,
125    /// 0x2 — Both binary locator and URI.
126    Both,
127    /// 0x3 — Through means of a descriptor.
128    Descriptor,
129    /// 0x4..=0xF — DVB reserved.
130    DvbReserved(u8),
131}
132
133impl LinkType {
134    #[must_use]
135    /// Decode from the wire value.  Every value maps (lossless).
136    pub fn from_u8(v: u8) -> Self {
137        match v & 0x0F {
138            0x0 => Self::UriString,
139            0x1 => Self::BinaryLocator,
140            0x2 => Self::Both,
141            0x3 => Self::Descriptor,
142            v => Self::DvbReserved(v),
143        }
144    }
145
146    #[must_use]
147    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
148    pub fn to_u8(self) -> u8 {
149        match self {
150            Self::UriString => 0x0,
151            Self::BinaryLocator => 0x1,
152            Self::Both => 0x2,
153            Self::Descriptor => 0x3,
154            Self::DvbReserved(v) => v,
155        }
156    }
157
158    #[must_use]
159    /// Human-readable spec display name.
160    pub fn name(self) -> &'static str {
161        match self {
162            Self::UriString => "URI String",
163            Self::BinaryLocator => "Binary Locator",
164            Self::Both => "Both",
165            Self::Descriptor => "Descriptor",
166            Self::DvbReserved(_) => "DVB Reserved",
167        }
168    }
169}
170dvb_common::impl_spec_display!(LinkType, DvbReserved);
171
172/// How-related classification scheme ID — ETSI TS 102 323 §10.4.3 Table 112.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize))]
175#[non_exhaustive]
176pub enum HowRelated {
177    /// 0x00 — HowRelatedCS:2004.
178    Cs2004,
179    /// 0x01 — HowRelatedCS:2005.
180    Cs2005,
181    /// 0x02 — HowRelatedCS:2007.
182    Cs2007,
183    /// 0x03..=0x2F — DVB reserved.
184    DvbReserved(u8),
185    /// 0x30..=0x3F — User private.
186    UserPrivate(u8),
187}
188
189impl HowRelated {
190    #[must_use]
191    /// Decode from the wire value.  Every value maps (lossless).
192    pub fn from_u8(v: u8) -> Self {
193        match v {
194            0x00 => Self::Cs2004,
195            0x01 => Self::Cs2005,
196            0x02 => Self::Cs2007,
197            v @ 0x03..0x30 => Self::DvbReserved(v),
198            _ => Self::UserPrivate(v),
199        }
200    }
201
202    #[must_use]
203    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
204    pub fn to_u8(self) -> u8 {
205        match self {
206            Self::Cs2004 => 0x00,
207            Self::Cs2005 => 0x01,
208            Self::Cs2007 => 0x02,
209            Self::DvbReserved(v) | Self::UserPrivate(v) => v,
210        }
211    }
212
213    #[must_use]
214    /// Human-readable spec display name.
215    pub fn name(self) -> &'static str {
216        match self {
217            Self::Cs2004 => "HowRelatedCS:2004",
218            Self::Cs2005 => "HowRelatedCS:2005",
219            Self::Cs2007 => "HowRelatedCS:2007",
220            Self::DvbReserved(_) => "DVB Reserved",
221            Self::UserPrivate(_) => "User Private",
222        }
223    }
224}
225dvb_common::impl_spec_display!(HowRelated, DvbReserved, UserPrivate);
226
227/// A promotional text item within a link_info entry (Table 110, §10.4.3).
228#[derive(Debug, Clone, PartialEq, Eq)]
229#[cfg_attr(feature = "serde", derive(serde::Serialize))]
230pub struct LinkItem<'a> {
231    /// ISO 639-2 language code.
232    pub language_code: LangCode,
233    /// Promotional text (EN 300 468 Annex A).
234    pub promotional_text: DvbText<'a>,
235}
236
237fn link_item_serialized_len(item: &LinkItem) -> usize {
238    3 + 1 + item.promotional_text.len()
239}
240
241/// Service identification within a DVB binary locator (Table 31, §7.3.2.3.3).
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize))]
244#[non_exhaustive]
245pub enum DvbLocatorService {
246    /// `inline_service == 0`: 10-bit `DVB_service_triplet_ID`.
247    Triplet {
248        /// 10-bit DVB service triplet ID.
249        dvb_service_triplet_id: u16,
250    },
251    /// `inline_service == 1`: full triplet.
252    Full {
253        /// `transport_stream_id`.
254        transport_stream_id: u16,
255        /// `original_network_id`.
256        original_network_id: u16,
257        /// `service_id`.
258        service_id: u16,
259    },
260}
261
262/// Event identifier within a DVB binary locator (Table 31).
263#[derive(Debug, Clone, PartialEq, Eq)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize))]
265#[non_exhaustive]
266pub enum DvbLocatorIdentifier {
267    /// `identifier_type == 0x00`: no identifier field.
268    None,
269    /// `identifier_type == 0x01`: `event_id`.
270    EventId {
271        /// 16-bit event_id.
272        event_id: u16,
273    },
274    /// `identifier_type == 0x02`: `TVA_id` carried in EIT.
275    TvaIdEit {
276        /// 16-bit TVA_id.
277        tva_id: u16,
278    },
279    /// `identifier_type == 0x03`: `TVA_id` carried in PES + `component`.
280    TvaIdPes {
281        /// 16-bit TVA_id.
282        tva_id: u16,
283        /// 8-bit component.
284        component: u8,
285    },
286}
287
288/// Time windows within a DVB binary locator (Table 31).
289#[derive(Debug, Clone, PartialEq, Eq)]
290#[cfg_attr(feature = "serde", derive(serde::Serialize))]
291pub struct DvbLocatorWindows {
292    /// `early_start_window` (3 bits).
293    pub early_start_window: u8,
294    /// `late_end_window` (5 bits).
295    pub late_end_window: u8,
296}
297
298/// DVB binary locator (Table 31, §7.3.2.3.3).
299#[derive(Debug, Clone, PartialEq, Eq)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize))]
301pub struct DvbBinaryLocator {
302    /// `identifier_type` (2 bits, Table 32) — [`IdentifierType`].
303    pub identifier_type: IdentifierType,
304    /// `scheduled_time_reliability`.
305    pub scheduled_time_reliability: bool,
306    /// `inline_service`.
307    pub inline_service: bool,
308    /// `start_date` (9 bits).
309    pub start_date: u16,
310    /// Service identification (conditional on `inline_service`).
311    pub service: DvbLocatorService,
312    /// `start_time` (16 bits).
313    pub start_time: u16,
314    /// `duration` (16 bits).
315    pub duration: u16,
316    /// Event identifier (conditional on `identifier_type`).
317    pub identifier: DvbLocatorIdentifier,
318    /// Time windows (present iff `identifier_type == 0 && scheduled_time_reliability == 1`).
319    pub windows: Option<DvbLocatorWindows>,
320}
321
322fn locator_serialized_len(loc: &DvbBinaryLocator) -> usize {
323    let mut len = 2;
324    len += match &loc.service {
325        DvbLocatorService::Triplet { .. } => 1,
326        DvbLocatorService::Full { .. } => 6,
327    };
328    len += 4;
329    len += match &loc.identifier {
330        DvbLocatorIdentifier::None => 0,
331        DvbLocatorIdentifier::EventId { .. } => 2,
332        DvbLocatorIdentifier::TvaIdEit { .. } => 2,
333        DvbLocatorIdentifier::TvaIdPes { .. } => 3,
334    };
335    if loc.windows.is_some() {
336        len += 1;
337    }
338    len
339}
340
341/// A single link_info entry (Table 110, §10.4.3).
342#[derive(Debug, Clone, PartialEq, Eq)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize))]
344pub struct LinkInfo<'a> {
345    /// `link_type` (4 bits, Table 111).
346    pub link_type: LinkType,
347    /// `how_related_classification_scheme_id` (6 bits).
348    pub how_related: HowRelated,
349    /// `term_id` (12 bits).
350    pub term_id: u16,
351    /// `group_id` (4 bits).
352    pub group_id: u8,
353    /// `precedence` (4 bits).
354    pub precedence: u8,
355    /// Media URI bytes (present iff `link_type == 0x00 || link_type == 0x02`).
356    pub media_uri: Option<&'a [u8]>,
357    /// DVB binary locator (present iff `link_type == 0x01 || link_type == 0x02`).
358    pub dvb_binary_locator: Option<DvbBinaryLocator>,
359    /// Promotional text items.
360    pub items: Vec<LinkItem<'a>>,
361    /// `default_icon_flag`.
362    pub default_icon_flag: bool,
363    /// `icon_id` (3 bits).
364    pub icon_id: u8,
365    /// Per-link descriptor loop.
366    pub descriptors: DescriptorLoop<'a>,
367}
368
369fn link_info_serialized_len(li: &LinkInfo) -> usize {
370    let mut len = 4;
371    len += li.media_uri.map_or(0, |u| 1 + u.len());
372    len += li
373        .dvb_binary_locator
374        .as_ref()
375        .map_or(0, locator_serialized_len);
376    len += 1;
377    len += li.items.iter().map(link_item_serialized_len).sum::<usize>();
378    len += 2;
379    len += li.descriptors.len();
380    len
381}
382
383/// Related Content Table (ETSI TS 102 323 v1.4.1 §10.4.2, Table 109).
384///
385/// The link_info loop is unfolded into typed [`LinkInfo`] entries.
386#[derive(Debug, Clone, PartialEq, Eq)]
387#[cfg_attr(feature = "serde", derive(serde::Serialize))]
388#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
389pub struct RctSection<'a> {
390    /// `table_id_extension_flag` (bit 6 of byte 1).
391    pub table_id_extension_flag: bool,
392    /// `service_id` — table_id_extension field.
393    pub service_id: u16,
394    /// 5-bit `version_number`.
395    pub version_number: u8,
396    /// `current_next_indicator`.
397    pub current_next_indicator: bool,
398    /// `section_number`.
399    pub section_number: u8,
400    /// `last_section_number`.
401    pub last_section_number: u8,
402    /// `year_offset` — reference year.
403    pub year_offset: u16,
404    /// Link info entries — unfolded per Table 110.
405    pub links: Vec<LinkInfo<'a>>,
406    /// Trailing descriptor loop.
407    pub descriptors: DescriptorLoop<'a>,
408}
409
410fn parse_locator(data: &[u8]) -> Result<(DvbBinaryLocator, usize)> {
411    if data.len() < 2 {
412        return Err(Error::BufferTooShort {
413            need: 2,
414            have: data.len(),
415            what: "RctSection dvb_binary_locator header",
416        });
417    }
418    let b0 = data[0];
419    let b1 = data[1];
420    let identifier_type =
421        IdentifierType::from_u8((b0 & LOCATOR_ID_TYPE_MASK) >> LOCATOR_ID_TYPE_SHIFT);
422    let scheduled_time_reliability = (b0 & LOCATOR_RELIABILITY_MASK) != 0;
423    let inline_service = (b0 & LOCATOR_INLINE_MASK) != 0;
424    let start_date = ((b0 & LOCATOR_START_DATE_HI_MASK) as u16) << LOCATOR_START_DATE_HI_SHIFT
425        | ((b1 >> 2) as u16 & 0x3F);
426
427    let mut pos = 2;
428    let service = if inline_service {
429        if pos + 6 > data.len() {
430            return Err(Error::BufferTooShort {
431                need: pos + 6,
432                have: data.len(),
433                what: "RctSection dvb_binary_locator full triplet",
434            });
435        }
436        let triplet = data[pos..pos + 6].first_chunk::<6>().unwrap();
437        let tsid = u16::from_be_bytes(*triplet[0..].first_chunk::<2>().unwrap());
438        let onid = u16::from_be_bytes(*triplet[2..].first_chunk::<2>().unwrap());
439        let sid = u16::from_be_bytes(*triplet[4..].first_chunk::<2>().unwrap());
440        pos += 6;
441        DvbLocatorService::Full {
442            transport_stream_id: tsid,
443            original_network_id: onid,
444            service_id: sid,
445        }
446    } else {
447        if data.len() < 3 {
448            return Err(Error::BufferTooShort {
449                need: 3,
450                have: data.len(),
451                what: "RctSection dvb_binary_locator triplet",
452            });
453        }
454        let triplet_id = ((b1 as u16 & 0x03) << 8) | data[2] as u16;
455        pos = 3;
456        DvbLocatorService::Triplet {
457            dvb_service_triplet_id: triplet_id,
458        }
459    };
460
461    if pos + 4 > data.len() {
462        return Err(Error::BufferTooShort {
463            need: pos + 4,
464            have: data.len(),
465            what: "RctSection dvb_binary_locator start_time/duration",
466        });
467    }
468    let (b2, rest) = data
469        .get(pos..)
470        .and_then(|s| s.split_first_chunk::<2>())
471        .ok_or(Error::BufferTooShort {
472            need: pos + 4,
473            have: data.len(),
474            what: "RctSection dvb_binary_locator start_time/duration",
475        })?;
476    let start_time = u16::from_be_bytes(*b2);
477    let (b2, _) = rest.split_first_chunk::<2>().ok_or(Error::BufferTooShort {
478        need: pos + 4,
479        have: data.len(),
480        what: "RctSection dvb_binary_locator start_time/duration",
481    })?;
482    let duration = u16::from_be_bytes(*b2);
483    pos += 4;
484
485    let identifier = match identifier_type {
486        IdentifierType::None => DvbLocatorIdentifier::None,
487        IdentifierType::EventId => {
488            if pos + 2 > data.len() {
489                return Err(Error::BufferTooShort {
490                    need: pos + 2,
491                    have: data.len(),
492                    what: "RctSection dvb_binary_locator event_id",
493                });
494            }
495            let (b2, _) = data
496                .get(pos..)
497                .and_then(|s| s.split_first_chunk::<2>())
498                .ok_or(Error::BufferTooShort {
499                    need: pos + 2,
500                    have: data.len(),
501                    what: "RctSection dvb_binary_locator event_id",
502                })?;
503            let event_id = u16::from_be_bytes(*b2);
504            pos += 2;
505            DvbLocatorIdentifier::EventId { event_id }
506        }
507        IdentifierType::TvaIdEit => {
508            if pos + 2 > data.len() {
509                return Err(Error::BufferTooShort {
510                    need: pos + 2,
511                    have: data.len(),
512                    what: "RctSection dvb_binary_locator TVA_id (EIT)",
513                });
514            }
515            let (b2, _) = data
516                .get(pos..)
517                .and_then(|s| s.split_first_chunk::<2>())
518                .ok_or(Error::BufferTooShort {
519                    need: pos + 2,
520                    have: data.len(),
521                    what: "RctSection dvb_binary_locator TVA_id (EIT)",
522                })?;
523            let tva_id = u16::from_be_bytes(*b2);
524            pos += 2;
525            DvbLocatorIdentifier::TvaIdEit { tva_id }
526        }
527        IdentifierType::TvaIdPes => {
528            if pos + 3 > data.len() {
529                return Err(Error::BufferTooShort {
530                    need: pos + 3,
531                    have: data.len(),
532                    what: "RctSection dvb_binary_locator TVA_id (PES)",
533                });
534            }
535            let (b2, _) = data
536                .get(pos..)
537                .and_then(|s| s.split_first_chunk::<2>())
538                .ok_or(Error::BufferTooShort {
539                    need: pos + 3,
540                    have: data.len(),
541                    what: "RctSection dvb_binary_locator TVA_id (PES)",
542                })?;
543            let tva_id = u16::from_be_bytes(*b2);
544            let component = data[pos + 2];
545            pos += 3;
546            DvbLocatorIdentifier::TvaIdPes { tva_id, component }
547        }
548    };
549
550    let windows = if identifier_type == IdentifierType::None && scheduled_time_reliability {
551        if pos >= data.len() {
552            return Err(Error::BufferTooShort {
553                need: pos + 1,
554                have: data.len(),
555                what: "RctSection dvb_binary_locator windows",
556            });
557        }
558        let wb = data[pos];
559        pos += 1;
560        Some(DvbLocatorWindows {
561            early_start_window: (wb >> 5) & 0x07,
562            late_end_window: wb & 0x1F,
563        })
564    } else {
565        None
566    };
567
568    Ok((
569        DvbBinaryLocator {
570            identifier_type,
571            scheduled_time_reliability,
572            inline_service,
573            start_date,
574            service,
575            start_time,
576            duration,
577            identifier,
578            windows,
579        },
580        pos,
581    ))
582}
583
584fn serialize_locator(loc: &DvbBinaryLocator, buf: &mut [u8]) -> usize {
585    let b0 = (loc.identifier_type.to_u8() << LOCATOR_ID_TYPE_SHIFT)
586        | (u8::from(loc.scheduled_time_reliability) << 5)
587        | (u8::from(loc.inline_service) << 4)
588        | LOCATOR_RESERVED_BITS
589        | ((loc.start_date >> LOCATOR_START_DATE_HI_SHIFT) as u8 & LOCATOR_START_DATE_HI_MASK);
590    buf[0] = b0;
591
592    let mut pos: usize;
593    match &loc.service {
594        DvbLocatorService::Triplet {
595            dvb_service_triplet_id,
596        } => {
597            let sd_lo = (loc.start_date & 0x3F) as u8;
598            buf[1] = (sd_lo << 2) | ((dvb_service_triplet_id >> 8) as u8 & 0x03);
599            buf[2] = (dvb_service_triplet_id & 0xFF) as u8;
600            pos = 3;
601        }
602        DvbLocatorService::Full {
603            transport_stream_id,
604            original_network_id,
605            service_id,
606        } => {
607            let sd_lo = (loc.start_date & 0x3F) as u8;
608            buf[1] = (sd_lo << 2) | 0x03;
609            buf[2..4].copy_from_slice(&transport_stream_id.to_be_bytes());
610            buf[4..6].copy_from_slice(&original_network_id.to_be_bytes());
611            buf[6..8].copy_from_slice(&service_id.to_be_bytes());
612            pos = 8;
613        }
614    }
615    buf[pos..pos + 2].copy_from_slice(&loc.start_time.to_be_bytes());
616    buf[pos + 2..pos + 4].copy_from_slice(&loc.duration.to_be_bytes());
617    pos += 4;
618
619    match &loc.identifier {
620        DvbLocatorIdentifier::None => {}
621        DvbLocatorIdentifier::EventId { event_id } => {
622            buf[pos..pos + 2].copy_from_slice(&event_id.to_be_bytes());
623            pos += 2;
624        }
625        DvbLocatorIdentifier::TvaIdEit { tva_id } => {
626            buf[pos..pos + 2].copy_from_slice(&tva_id.to_be_bytes());
627            pos += 2;
628        }
629        DvbLocatorIdentifier::TvaIdPes { tva_id, component } => {
630            buf[pos..pos + 2].copy_from_slice(&tva_id.to_be_bytes());
631            buf[pos + 2] = *component;
632            pos += 3;
633        }
634    }
635    if let Some(w) = &loc.windows {
636        buf[pos] = ((w.early_start_window & 0x07) << 5) | (w.late_end_window & 0x1F);
637        pos += 1;
638    }
639    pos
640}
641
642fn parse_link_info(data: &[u8], link_info_length: usize) -> Result<LinkInfo<'_>> {
643    if data.len() < 4 {
644        return Err(Error::BufferTooShort {
645            need: 4,
646            have: data.len(),
647            what: "RctSection link_info header",
648        });
649    }
650    let link_type = LinkType::from_u8((data[0] & LINK_TYPE_MASK) >> LINK_TYPE_SHIFT);
651    let how_related_raw = (data[0] & HOW_RELATED_HI_MASK) << 4 | (data[1] >> 4) & 0x0F;
652    let how_related = HowRelated::from_u8(how_related_raw);
653    let term_id = ((data[1] as u16 & TERM_ID_HI_MASK as u16) << TERM_ID_HI_SHIFT) | data[2] as u16;
654    let group_id = (data[3] & GROUP_ID_MASK) >> GROUP_ID_SHIFT;
655    let precedence = data[3] & PRECEDENCE_MASK;
656
657    let mut pos = 4;
658    let end = link_info_length;
659
660    let media_uri = if link_type == LinkType::UriString || link_type == LinkType::Both {
661        if pos >= end {
662            return Err(Error::BufferTooShort {
663                need: pos + 1,
664                have: end,
665                what: "RctSection media_uri_length",
666            });
667        }
668        let uri_len = data[pos] as usize;
669        pos += 1;
670        if pos + uri_len > end {
671            return Err(Error::BufferTooShort {
672                need: pos + uri_len,
673                have: end,
674                what: "RctSection media_uri",
675            });
676        }
677        let uri = &data[pos..pos + uri_len];
678        pos += uri_len;
679        Some(uri)
680    } else {
681        None
682    };
683
684    let dvb_binary_locator = if link_type == LinkType::BinaryLocator || link_type == LinkType::Both
685    {
686        if pos >= end {
687            return Err(Error::BufferTooShort {
688                need: pos + 1,
689                have: end,
690                what: "RctSection dvb_binary_locator",
691            });
692        }
693        let (loc, consumed) = parse_locator(&data[pos..end])?;
694        pos += consumed;
695        Some(loc)
696    } else {
697        None
698    };
699
700    if pos >= end {
701        return Err(Error::BufferTooShort {
702            need: pos + 1,
703            have: end,
704            what: "RctSection number_items",
705        });
706    }
707    let ni_byte = data[pos];
708    let number_items = (ni_byte & ITEM_COUNT_MASK) as usize;
709    pos += 1;
710
711    let mut items = Vec::with_capacity(number_items);
712    for _ in 0..number_items {
713        if pos + 4 > end {
714            return Err(Error::BufferTooShort {
715                need: pos + 4,
716                have: end,
717                what: "RctSection link_item",
718            });
719        }
720        let language_code = LangCode([data[pos], data[pos + 1], data[pos + 2]]);
721        let text_len = data[pos + 3] as usize;
722        pos += 4;
723        if pos + text_len > end {
724            return Err(Error::BufferTooShort {
725                need: pos + text_len,
726                have: end,
727                what: "RctSection promotional_text",
728            });
729        }
730        let promotional_text = DvbText::new(&data[pos..pos + text_len]);
731        pos += text_len;
732        items.push(LinkItem {
733            language_code,
734            promotional_text,
735        });
736    }
737
738    if pos + 2 > end {
739        return Err(Error::BufferTooShort {
740            need: pos + 2,
741            have: end,
742            what: "RctSection icon/desc",
743        });
744    }
745    let default_icon_flag = (data[pos] & ICON_FLAG_MASK) != 0;
746    let icon_id = (data[pos] & ICON_ID_MASK) >> ICON_ID_SHIFT;
747    let desc_len = (((data[pos] & ICON_DESC_LEN_HI_MASK) as usize) << 8) | data[pos + 1] as usize;
748    pos += 2;
749    let desc_start = pos;
750    let desc_end = desc_start + desc_len;
751    if desc_end > end {
752        return Err(Error::SectionLengthOverflow {
753            declared: desc_len,
754            available: end.saturating_sub(desc_start),
755        });
756    }
757    let descriptors = DescriptorLoop::new(&data[desc_start..desc_end]);
758
759    Ok(LinkInfo {
760        link_type,
761        how_related,
762        term_id,
763        group_id,
764        precedence,
765        media_uri,
766        dvb_binary_locator,
767        items,
768        default_icon_flag,
769        icon_id,
770        descriptors,
771    })
772}
773
774fn serialize_link_info(li: &LinkInfo, buf: &mut [u8]) -> usize {
775    let lt = li.link_type.to_u8();
776    let hr = li.how_related.to_u8();
777    buf[0] =
778        ((lt & 0x0F) << LINK_TYPE_SHIFT) | LINK_INFO_HEADER_RFU | ((hr >> 4) & HOW_RELATED_HI_MASK);
779    buf[1] = ((hr & 0x0F) << 4) | ((li.term_id >> TERM_ID_HI_SHIFT) as u8 & TERM_ID_HI_MASK);
780    buf[2] = li.term_id as u8;
781    buf[3] = ((li.group_id & 0x0F) << GROUP_ID_SHIFT) | (li.precedence & PRECEDENCE_MASK);
782
783    let mut pos = 4;
784    if let Some(uri) = li.media_uri {
785        buf[pos] = uri.len() as u8;
786        pos += 1;
787        buf[pos..pos + uri.len()].copy_from_slice(uri);
788        pos += uri.len();
789    }
790    if let Some(ref loc) = li.dvb_binary_locator {
791        let n = serialize_locator(loc, &mut buf[pos..]);
792        pos += n;
793    }
794    buf[pos] = ITEM_RFU_MASK | (li.items.len() as u8 & ITEM_COUNT_MASK);
795    pos += 1;
796    for item in &li.items {
797        buf[pos..pos + 3].copy_from_slice(&item.language_code.0);
798        buf[pos + 3] = item.promotional_text.len() as u8;
799        pos += 4;
800        buf[pos..pos + item.promotional_text.len()].copy_from_slice(item.promotional_text.raw());
801        pos += item.promotional_text.len();
802    }
803    let dll = li.descriptors.len() as u16;
804    buf[pos] = u8::from(li.default_icon_flag) << 7
805        | ((li.icon_id & 0x07) << ICON_ID_SHIFT)
806        | ((dll >> 8) as u8 & ICON_DESC_LEN_HI_MASK);
807    buf[pos + 1] = (dll & 0xFF) as u8;
808    pos += 2;
809    buf[pos..pos + li.descriptors.len()].copy_from_slice(li.descriptors.raw());
810    pos += li.descriptors.len();
811    pos
812}
813
814impl<'a> Parse<'a> for RctSection<'a> {
815    type Error = crate::error::Error;
816
817    fn parse(bytes: &'a [u8]) -> Result<Self> {
818        if bytes.len() < MIN_SECTION_LEN {
819            return Err(Error::BufferTooShort {
820                need: MIN_SECTION_LEN,
821                have: bytes.len(),
822                what: "RctSection",
823            });
824        }
825        if bytes[0] != TABLE_ID {
826            return Err(Error::UnexpectedTableId {
827                table_id: bytes[0],
828                what: "RctSection",
829                expected: &[TABLE_ID],
830            });
831        }
832
833        let table_id_extension_flag = (bytes[1] & 0x40) != 0;
834        let section_length = (((bytes[1] & 0x0F) as u16) << 8) | bytes[2] as u16;
835        let total = super::check_section_length(
836            bytes.len(),
837            MIN_HEADER_LEN,
838            section_length as usize,
839            MIN_SECTION_LEN,
840        )?;
841
842        let service_id = u16::from_be_bytes(*bytes[3..].first_chunk::<2>().unwrap());
843        let version_number = (bytes[5] >> 1) & 0x1F;
844        let current_next_indicator = (bytes[5] & 0x01) != 0;
845        let section_number = bytes[6];
846        let last_section_number = bytes[7];
847        let year_offset = u16::from_be_bytes(*bytes[8..].first_chunk::<2>().unwrap());
848        let link_count = bytes[10];
849
850        let payload_end = total - CRC_LEN;
851        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN;
852
853        let mut links = Vec::with_capacity(link_count as usize);
854        for _ in 0..link_count {
855            if pos + LINK_ENTRY_HEADER_LEN > payload_end {
856                return Err(Error::BufferTooShort {
857                    need: pos + LINK_ENTRY_HEADER_LEN,
858                    have: payload_end,
859                    what: "RctSection link_entry header",
860                });
861            }
862            let link_info_length = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
863            let link_data_start = pos + LINK_ENTRY_HEADER_LEN;
864            let link_data_end = link_data_start + link_info_length;
865            if link_data_end > payload_end {
866                return Err(Error::SectionLengthOverflow {
867                    declared: link_info_length,
868                    available: payload_end.saturating_sub(link_data_start),
869                });
870            }
871            let link_info =
872                parse_link_info(&bytes[link_data_start..link_data_end], link_info_length)?;
873            links.push(link_info);
874            pos = link_data_end;
875        }
876
877        if pos + DESC_LOOP_LEN_FIELD > payload_end {
878            return Err(Error::BufferTooShort {
879                need: pos + DESC_LOOP_LEN_FIELD,
880                have: payload_end,
881                what: "RctSection descriptor_loop_length field",
882            });
883        }
884        let descriptor_loop_length =
885            (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
886        let desc_start = pos + DESC_LOOP_LEN_FIELD;
887        let desc_end = desc_start + descriptor_loop_length;
888        if desc_end > payload_end {
889            return Err(Error::SectionLengthOverflow {
890                declared: descriptor_loop_length,
891                available: payload_end.saturating_sub(desc_start),
892            });
893        }
894        let descriptors = DescriptorLoop::new(&bytes[desc_start..desc_end]);
895
896        Ok(RctSection {
897            table_id_extension_flag,
898            service_id,
899            version_number,
900            current_next_indicator,
901            section_number,
902            last_section_number,
903            year_offset,
904            links,
905            descriptors,
906        })
907    }
908}
909
910impl Serialize for RctSection<'_> {
911    type Error = crate::error::Error;
912
913    fn serialized_len(&self) -> usize {
914        MIN_HEADER_LEN
915            + EXTENSION_HEADER_LEN
916            + POST_EXT_FIXED_LEN
917            + self
918                .links
919                .iter()
920                .map(|li| LINK_ENTRY_HEADER_LEN + link_info_serialized_len(li))
921                .sum::<usize>()
922            + DESC_LOOP_LEN_FIELD
923            + self.descriptors.len()
924            + CRC_LEN
925    }
926
927    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
928        let len = self.serialized_len();
929        if buf.len() < len {
930            return Err(Error::OutputBufferTooSmall {
931                need: len,
932                have: buf.len(),
933            });
934        }
935
936        let section_length_usize = len - MIN_HEADER_LEN;
937        if section_length_usize > 0x0FFF {
938            return Err(Error::SectionLengthOverflow {
939                declared: section_length_usize,
940                available: 0x0FFF,
941            });
942        }
943        let section_length = section_length_usize as u16;
944        buf[0] = TABLE_ID;
945        let tief_bit: u8 = if self.table_id_extension_flag {
946            0x40
947        } else {
948            0x00
949        };
950        buf[1] = super::SECTION_B1_SSI
951            | tief_bit
952            | super::SECTION_B1_RESERVED_HI
953            | ((section_length >> 8) as u8 & 0x0F);
954        buf[2] = (section_length & 0xFF) as u8;
955
956        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
957        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
958        buf[6] = self.section_number;
959        buf[7] = self.last_section_number;
960        buf[8..10].copy_from_slice(&self.year_offset.to_be_bytes());
961        if self.links.len() > u8::MAX as usize {
962            return Err(Error::SectionLengthOverflow {
963                declared: self.links.len(),
964                available: u8::MAX as usize,
965            });
966        }
967        buf[10] = self.links.len() as u8;
968
969        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN;
970        for li in &self.links {
971            let li_body_len = link_info_serialized_len(li) as u16;
972            buf[pos] = 0xF0 | ((li_body_len >> 8) as u8 & 0x0F);
973            buf[pos + 1] = (li_body_len & 0xFF) as u8;
974            pos += LINK_ENTRY_HEADER_LEN;
975            pos += serialize_link_info(li, &mut buf[pos..]);
976        }
977
978        let dll = self.descriptors.len() as u16;
979        buf[pos] = 0xF0 | ((dll >> 8) as u8 & 0x0F);
980        buf[pos + 1] = (dll & 0xFF) as u8;
981        pos += DESC_LOOP_LEN_FIELD;
982        buf[pos..pos + self.descriptors.len()].copy_from_slice(self.descriptors.raw());
983        pos += self.descriptors.len();
984
985        let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
986        buf[pos..pos + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
987        Ok(len)
988    }
989}
990impl<'a> crate::traits::TableDef<'a> for RctSection<'a> {
991    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
992    const NAME: &'static str = "RELATED_CONTENT";
993}
994
995#[cfg(test)]
996mod tests {
997    use super::*;
998
999    #[test]
1000    fn parse_no_links_no_descriptors() {
1001        let rct = RctSection {
1002            table_id_extension_flag: false,
1003            service_id: 0x0064,
1004            version_number: 3,
1005            current_next_indicator: true,
1006            section_number: 0,
1007            last_section_number: 0,
1008            year_offset: 0x07D3,
1009            links: Vec::new(),
1010            descriptors: DescriptorLoop::new(&[]),
1011        };
1012        let mut buf = vec![0u8; rct.serialized_len()];
1013        rct.serialize_into(&mut buf).unwrap();
1014        let p = RctSection::parse(&buf).unwrap();
1015        assert!(!p.table_id_extension_flag);
1016        assert_eq!(p.service_id, 0x0064);
1017        assert_eq!(p.year_offset, 0x07D3);
1018        assert!(p.links.is_empty());
1019    }
1020
1021    #[test]
1022    fn parse_one_link_uri_only() {
1023        let li = LinkInfo {
1024            link_type: LinkType::UriString,
1025            how_related: HowRelated::Cs2005,
1026            term_id: 0x123,
1027            group_id: 0x5,
1028            precedence: 0x9,
1029            media_uri: Some(b"http://example.com"),
1030            dvb_binary_locator: None,
1031            items: Vec::new(),
1032            default_icon_flag: false,
1033            icon_id: 0,
1034            descriptors: DescriptorLoop::new(&[]),
1035        };
1036        let rct = RctSection {
1037            table_id_extension_flag: false,
1038            service_id: 0x1234,
1039            version_number: 7,
1040            current_next_indicator: true,
1041            section_number: 1,
1042            last_section_number: 3,
1043            year_offset: 2003,
1044            links: vec![li],
1045            descriptors: DescriptorLoop::new(&[]),
1046        };
1047        let mut buf = vec![0u8; rct.serialized_len()];
1048        rct.serialize_into(&mut buf).unwrap();
1049        let p = RctSection::parse(&buf).unwrap();
1050        assert_eq!(p.links.len(), 1);
1051        assert_eq!(p.links[0].link_type, LinkType::UriString);
1052        assert_eq!(p.links[0].how_related, HowRelated::Cs2005);
1053        assert_eq!(p.links[0].term_id, 0x123);
1054        assert_eq!(p.links[0].group_id, 0x5);
1055        assert_eq!(p.links[0].precedence, 0x9);
1056        assert_eq!(p.links[0].media_uri.unwrap(), b"http://example.com");
1057    }
1058
1059    #[test]
1060    fn parse_one_link_with_locator_and_items() {
1061        let loc = DvbBinaryLocator {
1062            identifier_type: IdentifierType::EventId,
1063            scheduled_time_reliability: false,
1064            inline_service: true,
1065            start_date: 0x0FF,
1066            service: DvbLocatorService::Full {
1067                transport_stream_id: 0x1000,
1068                original_network_id: 0x2000,
1069                service_id: 0x3000,
1070            },
1071            start_time: 0x8000,
1072            duration: 0x4000,
1073            identifier: DvbLocatorIdentifier::EventId { event_id: 0xBEEF },
1074            windows: None,
1075        };
1076        let li = LinkInfo {
1077            link_type: LinkType::BinaryLocator,
1078            how_related: HowRelated::Cs2007,
1079            term_id: 0x456,
1080            group_id: 0x1,
1081            precedence: 0x2,
1082            media_uri: None,
1083            dvb_binary_locator: Some(loc),
1084            items: vec![LinkItem {
1085                language_code: LangCode(*b"eng"),
1086                promotional_text: DvbText::new(b"Promo"),
1087            }],
1088            default_icon_flag: true,
1089            icon_id: 3,
1090            descriptors: DescriptorLoop::new(&[0x80, 0x00]),
1091        };
1092        let rct = RctSection {
1093            table_id_extension_flag: true,
1094            service_id: 0xABCD,
1095            version_number: 15,
1096            current_next_indicator: false,
1097            section_number: 2,
1098            last_section_number: 5,
1099            year_offset: 2024,
1100            links: vec![li],
1101            descriptors: DescriptorLoop::new(&[]),
1102        };
1103        let mut buf = vec![0u8; rct.serialized_len()];
1104        rct.serialize_into(&mut buf).unwrap();
1105        let mut buf2 = vec![0u8; rct.serialized_len()];
1106        rct.serialize_into(&mut buf2).unwrap();
1107        assert_eq!(buf, buf2, "byte-exact re-serialize");
1108        let p = RctSection::parse(&buf).unwrap();
1109        assert_eq!(p.links.len(), 1);
1110        assert_eq!(p.links[0].link_type, LinkType::BinaryLocator);
1111        let l = p.links[0].dvb_binary_locator.as_ref().unwrap();
1112        assert_eq!(l.identifier_type, IdentifierType::EventId);
1113        assert!(l.inline_service);
1114        assert_eq!(l.start_date, 0x0FF);
1115        assert_eq!(p.links[0].items.len(), 1);
1116        assert_eq!(p.links[0].items[0].language_code, LangCode(*b"eng"));
1117        assert!(p.links[0].default_icon_flag);
1118        assert_eq!(p.links[0].icon_id, 3);
1119    }
1120
1121    #[test]
1122    fn byte_exact_round_trip_simple() {
1123        let li = LinkInfo {
1124            link_type: LinkType::UriString,
1125            how_related: HowRelated::Cs2004,
1126            term_id: 0,
1127            group_id: 0,
1128            precedence: 0,
1129            media_uri: Some(b"uri"),
1130            dvb_binary_locator: None,
1131            items: Vec::new(),
1132            default_icon_flag: false,
1133            icon_id: 0,
1134            descriptors: DescriptorLoop::new(&[]),
1135        };
1136        let rct = RctSection {
1137            table_id_extension_flag: false,
1138            service_id: 0x0001,
1139            version_number: 0,
1140            current_next_indicator: true,
1141            section_number: 0,
1142            last_section_number: 0,
1143            year_offset: 0x07D3,
1144            links: vec![li],
1145            descriptors: DescriptorLoop::new(&[]),
1146        };
1147        let mut buf = vec![0u8; rct.serialized_len()];
1148        rct.serialize_into(&mut buf).unwrap();
1149        let parsed = RctSection::parse(&buf).unwrap();
1150        let mut buf2 = vec![0u8; parsed.serialized_len()];
1151        parsed.serialize_into(&mut buf2).unwrap();
1152        assert_eq!(buf, buf2);
1153    }
1154
1155    #[test]
1156    fn parse_rejects_wrong_table_id() {
1157        let rct = RctSection {
1158            table_id_extension_flag: false,
1159            service_id: 0x0001,
1160            version_number: 0,
1161            current_next_indicator: true,
1162            section_number: 0,
1163            last_section_number: 0,
1164            year_offset: 2024,
1165            links: Vec::new(),
1166            descriptors: DescriptorLoop::new(&[]),
1167        };
1168        let mut buf = vec![0u8; rct.serialized_len()];
1169        rct.serialize_into(&mut buf).unwrap();
1170        buf[0] = 0x4A;
1171        assert!(matches!(
1172            RctSection::parse(&buf).unwrap_err(),
1173            Error::UnexpectedTableId { table_id: 0x4A, .. }
1174        ));
1175    }
1176
1177    #[test]
1178    fn parse_rejects_buffer_too_short() {
1179        assert!(matches!(
1180            RctSection::parse(&[0x76, 0x80, 0x00]).unwrap_err(),
1181            Error::BufferTooShort { .. }
1182        ));
1183    }
1184
1185    #[test]
1186    fn serialize_rejects_output_buffer_too_small() {
1187        let rct = RctSection {
1188            table_id_extension_flag: false,
1189            service_id: 0x0001,
1190            version_number: 0,
1191            current_next_indicator: true,
1192            section_number: 0,
1193            last_section_number: 0,
1194            year_offset: 0,
1195            links: Vec::new(),
1196            descriptors: DescriptorLoop::new(&[]),
1197        };
1198        let mut buf = vec![0u8; 2];
1199        assert!(matches!(
1200            rct.serialize_into(&mut buf).unwrap_err(),
1201            Error::OutputBufferTooSmall { .. }
1202        ));
1203    }
1204
1205    #[test]
1206    fn parse_rejects_zero_section_length() {
1207        let mut buf = vec![0u8; 64];
1208        buf[0] = TABLE_ID;
1209        buf[1] = 0xF0;
1210        buf[2] = 0x00;
1211        for b in &mut buf[3..] {
1212            *b = 0xFF;
1213        }
1214        assert!(matches!(
1215            RctSection::parse(&buf).unwrap_err(),
1216            Error::SectionLengthOverflow { .. }
1217        ));
1218    }
1219
1220    #[test]
1221    fn parse_handwritten_rct_no_links() {
1222        let mut bytes: Vec<u8> = vec![
1223            0x76, 0x80, 0x0E, 0x00, 0x64, 0xC7, 0x00, 0x00, 0x07, 0xD3, 0x00, 0xF0, 0x00,
1224        ];
1225        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
1226        bytes.extend_from_slice(&crc.to_be_bytes());
1227        let rct = RctSection::parse(&bytes).unwrap();
1228        assert_eq!(rct.service_id, 0x0064);
1229        assert_eq!(rct.year_offset, 0x07D3);
1230        assert!(rct.links.is_empty());
1231    }
1232
1233    #[test]
1234    fn link_type_full_range_round_trip() {
1235        for v in 0u8..=0x0F {
1236            let lt = LinkType::from_u8(v);
1237            assert_eq!(lt.to_u8(), v, "LinkType round-trip failed for {v:#04x}");
1238        }
1239    }
1240
1241    #[test]
1242    fn how_related_full_range_round_trip() {
1243        for v in 0u8..=0x3F {
1244            let hr = HowRelated::from_u8(v);
1245            assert_eq!(hr.to_u8(), v, "HowRelated round-trip failed for {v:#04x}");
1246        }
1247    }
1248
1249    #[test]
1250    fn identifier_type_full_range_round_trip() {
1251        for v in 0u8..=0x03 {
1252            let it = IdentifierType::from_u8(v);
1253            assert_eq!(
1254                it.to_u8(),
1255                v,
1256                "IdentifierType round-trip failed for 0x{v:02X}"
1257            );
1258        }
1259    }
1260
1261    #[test]
1262    fn identifier_type_known_values() {
1263        assert_eq!(IdentifierType::from_u8(0), IdentifierType::None);
1264        assert_eq!(IdentifierType::from_u8(1), IdentifierType::EventId);
1265        assert_eq!(IdentifierType::from_u8(2), IdentifierType::TvaIdEit);
1266        assert_eq!(IdentifierType::from_u8(3), IdentifierType::TvaIdPes);
1267        assert_eq!(IdentifierType::None.name(), "none");
1268        assert_eq!(IdentifierType::EventId.name(), "event_id");
1269        assert_eq!(IdentifierType::TvaIdEit.name(), "TVA_id (EIT)");
1270    }
1271}