Skip to main content

dvb_si/carousel/biop/
ior.rs

1//! IOP::IOR and its component types for the DVB object-carousel profile.
2//!
3//! All wire layouts are from `docs/iso_13818_6_biop.md` (ETSI TR 101 202 §4.7.3).
4//!
5//! # Layout overview
6//!
7//! `IOP::IOR` (Table 4.3) → one or more `TaggedProfile` variants:
8//! - [`TaggedProfile::Biop`] — BIOP Profile Body (Table 4.5): contains an
9//!   [`ObjectLocation`] and a [`ConnBinder`] (each a [`LiteComponent`]).
10//! - [`TaggedProfile::LiteOptions`] — Lite Options Profile Body (Table 4.7):
11//!   contains a [`ServiceLocation`] (with an [`NsapAddress`]).
12//! - [`TaggedProfile::Unknown`] — any other tag; data preserved raw.
13
14use super::{
15    BIOP_DELIVERY_PARA_USE, BYTE_ORDER_BIG_ENDIAN, TAG_BIOP, TAG_CONN_BINDER, TAG_LITE_OPTIONS,
16    TAG_OBJECT_LOCATION, TAG_SERVICE_LOCATION,
17};
18use crate::error::{Error, Result};
19use alloc::vec::Vec;
20use broadcast_common::{Parse, Serialize};
21
22// ── wire-layout byte counts ────────────────────────────────────────────────────
23
24/// IOR: type_id_length (4) + taggedProfiles_count (4).
25const IOR_FIXED_LEN: usize = 8;
26/// Per-profile: profileId_tag (4) + profile_data_length (4).
27const PROFILE_HEADER_LEN: usize = 8;
28/// BIOP Profile Body: byte_order (1) + liteComponents_count (1).
29const BIOP_BODY_FIXED_LEN: usize = 2;
30/// Per liteComponent: componentId_tag (4) + component_data_length (1).
31const COMPONENT_HEADER_LEN: usize = 5;
32/// ObjectLocation fixed: carousel_id(4)+module_id(2)+version_major(1)+version_minor(1)+key_len(1) = 9.
33const OBJECT_LOCATION_FIXED_LEN: usize = 9;
34/// ConnBinder fixed: taps_count(1).
35const CONN_BINDER_FIXED_LEN: usize = 1;
36/// Tap fixed: id(2)+use(2)+association_tag(2)+selector_length(1) = 7.
37const TAP_FIXED_LEN: usize = 7;
38/// LiteOptions Profile Body: byte_order(1) + component_count(1).
39const LITE_OPTIONS_BODY_FIXED_LEN: usize = 2;
40/// ServiceLocation component header: componentId_tag(4)+component_data_length(4).
41const SERVICE_LOCATION_COMP_HEADER_LEN: usize = 8;
42/// serviceDomain_length(1) = 1 prefix before the 20-byte NSAP address.
43const SERVICE_DOMAIN_LEN_FIELD: usize = 1;
44/// DVB Carousel NSAP address: always 20 bytes.  See Table 4.8.
45const NSAP_ADDRESS_LEN: usize = 20;
46/// NSAP: AFI(1)+Type(1)+carouselId(4)+specifierType(1)+specifierData(3)+tsid(2)+onid(2)+sid(2)+reserved(4) = 20.
47const _NSAP_FIELDS_LEN: usize = NSAP_ADDRESS_LEN; // same constant, alias for clarity
48/// CosNaming nameComponents_count in ServiceLocation: 32-bit.
49const NAMING_COUNT_LEN: usize = 4;
50/// id_length / kind_length in ServiceLocation CosNaming: 32-bit each.
51const NAMING_FIELD_LEN: usize = 4;
52/// initialContext_length in ServiceLocation: 32-bit.
53const INITIAL_CONTEXT_LEN_FIELD: usize = 4;
54
55// ── ObjectKind ────────────────────────────────────────────────────────────────
56
57/// DVB alias `type_id` values (4 bytes) that appear in IOR `type_id` and
58/// BIOP object `objectKind` fields.  TR 101 202 §4.7.3.1, Table 4.4.
59#[derive(Debug, Clone, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize))]
61#[non_exhaustive]
62pub enum ObjectKind {
63    /// `"dir\0"` (0x64697200) — DSM::Directory.
64    Directory,
65    /// `"fil\0"` (0x66696C00) — DSM::File.
66    File,
67    /// `"str\0"` (0x73747200) — DSM::Stream.
68    Stream,
69    /// `"srg\0"` (0x73726700) — DSM::ServiceGateway.
70    ServiceGateway,
71    /// `"ste\0"` (0x73746500) — BIOP::StreamEvent.
72    StreamEvent,
73    /// Unknown 4-byte kind.
74    Unknown([u8; 4]),
75}
76
77impl ObjectKind {
78    /// Parse 4 bytes into an `ObjectKind`.
79    pub fn from_bytes(b: [u8; 4]) -> Self {
80        match &b {
81            b"dir\0" => Self::Directory,
82            b"fil\0" => Self::File,
83            b"str\0" => Self::Stream,
84            b"srg\0" => Self::ServiceGateway,
85            b"ste\0" => Self::StreamEvent,
86            _ => Self::Unknown(b),
87        }
88    }
89
90    /// Serialize this kind to its 4-byte wire representation.
91    pub fn to_bytes(&self) -> [u8; 4] {
92        match self {
93            Self::Directory => *b"dir\0",
94            Self::File => *b"fil\0",
95            Self::Stream => *b"str\0",
96            Self::ServiceGateway => *b"srg\0",
97            Self::StreamEvent => *b"ste\0",
98            Self::Unknown(b) => *b,
99        }
100    }
101}
102
103// ── Tap ───────────────────────────────────────────────────────────────────────
104
105/// BIOP::Tap — one delivery-parameter or object-use tap.
106/// TR 101 202 §4.7.3.2, Table 4.5 (ConnBinder Tap list).
107#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize))]
109pub struct Tap<'a> {
110    /// `id` field — user private; typically 0.
111    pub id: u16,
112    /// `use` field — e.g. [`BIOP_DELIVERY_PARA_USE`].
113    pub use_: u16,
114    /// `association_tag` — ES on which this tap is broadcast.
115    pub association_tag: u16,
116    /// Raw selector bytes.
117    #[cfg_attr(feature = "serde", serde(borrow))]
118    pub selector: &'a [u8],
119}
120
121impl<'a> Tap<'a> {
122    /// Returns the `transactionId` decoded from the selector when this is a
123    /// `BIOP_DELIVERY_PARA_USE` tap with a 10-byte MESSAGE selector.
124    ///
125    /// Selector layout (when `use == 0x0016` and `selector.len() >= 10`):
126    /// selector_type(2) | transactionId(4) | timeout(4).
127    pub fn transaction_id(&self) -> Option<u32> {
128        if self.use_ == BIOP_DELIVERY_PARA_USE && self.selector.len() >= 10 {
129            let (chunk, _) = self.selector[2..].split_first_chunk::<4>()?;
130            Some(u32::from_be_bytes(*chunk))
131        } else {
132            None
133        }
134    }
135
136    /// Returns the `timeout` (µs) decoded from the selector when this is a
137    /// `BIOP_DELIVERY_PARA_USE` tap with a 10-byte MESSAGE selector.
138    pub fn timeout(&self) -> Option<u32> {
139        if self.use_ == BIOP_DELIVERY_PARA_USE && self.selector.len() >= 10 {
140            let (chunk, _) = self.selector[6..].split_first_chunk::<4>()?;
141            Some(u32::from_be_bytes(*chunk))
142        } else {
143            None
144        }
145    }
146
147    pub(crate) fn serialized_len(&self) -> usize {
148        TAP_FIXED_LEN + self.selector.len()
149    }
150
151    pub(crate) fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
152        let (tap_hdr, _) =
153            bytes[pos..end]
154                .split_first_chunk::<TAP_FIXED_LEN>()
155                .ok_or(Error::BufferTooShort {
156                    need: pos + TAP_FIXED_LEN,
157                    have: end,
158                    what: "BIOP Tap fixed fields",
159                })?;
160        let id = u16::from_be_bytes([tap_hdr[0], tap_hdr[1]]);
161        let use_ = u16::from_be_bytes([tap_hdr[2], tap_hdr[3]]);
162        let association_tag = u16::from_be_bytes([tap_hdr[4], tap_hdr[5]]);
163        let selector_length = tap_hdr[6] as usize;
164        let data_start = pos + TAP_FIXED_LEN;
165        if data_start + selector_length > end {
166            return Err(Error::SectionLengthOverflow {
167                declared: selector_length,
168                available: end - data_start,
169            });
170        }
171        let selector = &bytes[data_start..data_start + selector_length];
172        Ok((
173            Tap {
174                id,
175                use_,
176                association_tag,
177                selector,
178            },
179            data_start + selector_length,
180        ))
181    }
182
183    pub(crate) fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
184        let len = self.serialized_len();
185        if buf.len() < len {
186            return Err(Error::OutputBufferTooSmall {
187                need: len,
188                have: buf.len(),
189            });
190        }
191        if self.selector.len() > u8::MAX as usize {
192            return Err(Error::SectionLengthOverflow {
193                declared: self.selector.len(),
194                available: u8::MAX as usize,
195            });
196        }
197        buf[0..2].copy_from_slice(&self.id.to_be_bytes());
198        buf[2..4].copy_from_slice(&self.use_.to_be_bytes());
199        buf[4..6].copy_from_slice(&self.association_tag.to_be_bytes());
200        buf[6] = self.selector.len() as u8;
201        buf[7..7 + self.selector.len()].copy_from_slice(self.selector);
202        Ok(len)
203    }
204}
205
206// ── ObjectLocation ────────────────────────────────────────────────────────────
207
208/// BIOP::ObjectLocation — first mandatory liteComponent in the BIOP Profile Body.
209/// TR 101 202 §4.7.3.2, Table 4.5.
210#[derive(Debug, Clone, PartialEq, Eq)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize))]
212pub struct ObjectLocation<'a> {
213    /// `carouselId` — identifies the carousel delivering this object.
214    pub carousel_id: u32,
215    /// `moduleId` — module within the carousel.
216    pub module_id: u16,
217    /// `version.major` — always 1 for DVB.
218    pub version_major: u8,
219    /// `version.minor` — always 0 for DVB.
220    pub version_minor: u8,
221    /// `objectKey_data` — key for this object within the module.
222    #[cfg_attr(feature = "serde", serde(borrow))]
223    pub object_key: &'a [u8],
224}
225
226impl<'a> ObjectLocation<'a> {
227    fn serialized_len(&self) -> usize {
228        OBJECT_LOCATION_FIXED_LEN + self.object_key.len()
229    }
230
231    fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
232        let (ol_hdr, _) = bytes[pos..end]
233            .split_first_chunk::<OBJECT_LOCATION_FIXED_LEN>()
234            .ok_or(Error::BufferTooShort {
235                need: pos + OBJECT_LOCATION_FIXED_LEN,
236                have: end,
237                what: "BIOP ObjectLocation fixed fields",
238            })?;
239        let carousel_id = u32::from_be_bytes([ol_hdr[0], ol_hdr[1], ol_hdr[2], ol_hdr[3]]);
240        let module_id = u16::from_be_bytes([ol_hdr[4], ol_hdr[5]]);
241        let version_major = ol_hdr[6];
242        let version_minor = ol_hdr[7];
243        let object_key_length = ol_hdr[8] as usize;
244        let data_start = pos + OBJECT_LOCATION_FIXED_LEN;
245        if data_start + object_key_length > end {
246            return Err(Error::SectionLengthOverflow {
247                declared: object_key_length,
248                available: end - data_start,
249            });
250        }
251        Ok((
252            ObjectLocation {
253                carousel_id,
254                module_id,
255                version_major,
256                version_minor,
257                object_key: &bytes[data_start..data_start + object_key_length],
258            },
259            data_start + object_key_length,
260        ))
261    }
262
263    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
264        let len = self.serialized_len();
265        if buf.len() < len {
266            return Err(Error::OutputBufferTooSmall {
267                need: len,
268                have: buf.len(),
269            });
270        }
271        if self.object_key.len() > u8::MAX as usize {
272            return Err(Error::SectionLengthOverflow {
273                declared: self.object_key.len(),
274                available: u8::MAX as usize,
275            });
276        }
277        buf[0..4].copy_from_slice(&self.carousel_id.to_be_bytes());
278        buf[4..6].copy_from_slice(&self.module_id.to_be_bytes());
279        buf[6] = self.version_major;
280        buf[7] = self.version_minor;
281        buf[8] = self.object_key.len() as u8;
282        buf[9..9 + self.object_key.len()].copy_from_slice(self.object_key);
283        Ok(len)
284    }
285}
286
287// ── ConnBinder ────────────────────────────────────────────────────────────────
288
289/// DSM::ConnBinder — second mandatory liteComponent in the BIOP Profile Body.
290/// TR 101 202 §4.7.3.2, Table 4.5.
291#[derive(Debug, Clone, PartialEq, Eq)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize))]
293pub struct ConnBinder<'a> {
294    /// Taps list.
295    #[cfg_attr(feature = "serde", serde(borrow))]
296    pub taps: Vec<Tap<'a>>,
297}
298
299impl<'a> ConnBinder<'a> {
300    fn serialized_len(&self) -> usize {
301        CONN_BINDER_FIXED_LEN + self.taps.iter().map(|t| t.serialized_len()).sum::<usize>()
302    }
303
304    fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
305        if pos + CONN_BINDER_FIXED_LEN > end {
306            return Err(Error::BufferTooShort {
307                need: pos + CONN_BINDER_FIXED_LEN,
308                have: end,
309                what: "BIOP ConnBinder taps_count",
310            });
311        }
312        let taps_count = bytes[pos] as usize;
313        let mut cur = pos + CONN_BINDER_FIXED_LEN;
314        let mut taps = Vec::with_capacity(taps_count.min(16));
315        for _ in 0..taps_count {
316            let (tap, next) = Tap::parse_from(bytes, cur, end)?;
317            taps.push(tap);
318            cur = next;
319        }
320        Ok((ConnBinder { taps }, cur))
321    }
322
323    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
324        let len = self.serialized_len();
325        if buf.len() < len {
326            return Err(Error::OutputBufferTooSmall {
327                need: len,
328                have: buf.len(),
329            });
330        }
331        if self.taps.len() > u8::MAX as usize {
332            return Err(Error::SectionLengthOverflow {
333                declared: self.taps.len(),
334                available: u8::MAX as usize,
335            });
336        }
337        buf[0] = self.taps.len() as u8;
338        let mut pos = CONN_BINDER_FIXED_LEN;
339        for tap in &self.taps {
340            let written = tap.serialize_into_buf(&mut buf[pos..])?;
341            pos += written;
342        }
343        Ok(len)
344    }
345}
346
347// ── LiteComponent ─────────────────────────────────────────────────────────────
348
349/// An unknown extra liteComponent in a profile body — tag + raw data.
350/// TR 101 202 §4.7.3.2, Table 4.5 (extra components beyond ObjectLocation + ConnBinder).
351#[derive(Debug, Clone, PartialEq, Eq)]
352#[cfg_attr(feature = "serde", derive(serde::Serialize))]
353pub struct LiteComponent<'a> {
354    /// `componentId_tag` (32-bit).
355    pub tag: u32,
356    /// Raw component data bytes.
357    #[cfg_attr(feature = "serde", serde(borrow))]
358    pub data: &'a [u8],
359}
360
361impl LiteComponent<'_> {
362    fn serialized_len(&self) -> usize {
363        COMPONENT_HEADER_LEN + self.data.len()
364    }
365}
366
367// ── BiopProfileBody ───────────────────────────────────────────────────────────
368
369/// BIOP Profile Body — decoded contents of a `TAG_BIOP` profile.
370/// TR 101 202 §4.7.3.2, Table 4.5.
371#[derive(Debug, Clone, PartialEq, Eq)]
372#[cfg_attr(feature = "serde", derive(serde::Serialize))]
373pub struct BiopProfileBody<'a> {
374    /// First mandatory component: BIOP::ObjectLocation.
375    #[cfg_attr(feature = "serde", serde(borrow))]
376    pub object_location: ObjectLocation<'a>,
377    /// Second mandatory component: DSM::ConnBinder.
378    pub conn_binder: ConnBinder<'a>,
379    /// Extra liteComponents beyond the mandatory two (N−2).
380    pub extra: Vec<LiteComponent<'a>>,
381}
382
383impl<'a> BiopProfileBody<'a> {
384    /// Parse from profile_data bytes (after the 4+4 profileId_tag+length fields).
385    fn parse_from(bytes: &'a [u8]) -> Result<Self> {
386        let end = bytes.len();
387        if end < BIOP_BODY_FIXED_LEN {
388            return Err(Error::BufferTooShort {
389                need: BIOP_BODY_FIXED_LEN,
390                have: end,
391                what: "BIOP Profile Body fixed fields",
392            });
393        }
394        let byte_order = bytes[0];
395        if byte_order != BYTE_ORDER_BIG_ENDIAN {
396            return Err(Error::ReservedBitsViolation {
397                field: "profile_data_byte_order",
398                reason: "must be 0x00 (big-endian) per DVB mandatory constraint (TR 101 202 §4.7.3.2)",
399            });
400        }
401        let lite_components_count = bytes[1] as usize;
402        if lite_components_count < 2 {
403            return Err(Error::ValueOutOfRange {
404                field: "liteComponents_count",
405                reason: "BIOP Profile Body must have at least 2 components (ObjectLocation + ConnBinder)",
406            });
407        }
408        let mut pos = BIOP_BODY_FIXED_LEN;
409
410        // First component: ObjectLocation
411        let (ch0, _) = bytes[pos..end]
412            .split_first_chunk::<COMPONENT_HEADER_LEN>()
413            .ok_or(Error::BufferTooShort {
414                need: pos + COMPONENT_HEADER_LEN,
415                have: end,
416                what: "BIOP ObjectLocation component header",
417            })?;
418        let comp0_tag = u32::from_be_bytes([ch0[0], ch0[1], ch0[2], ch0[3]]);
419        if comp0_tag != TAG_OBJECT_LOCATION {
420            return Err(Error::ReservedBitsViolation {
421                field: "componentId_tag[0]",
422                reason: "first liteComponent must be TAG_ObjectLocation (0x49534F50)",
423            });
424        }
425        let comp0_len = ch0[4] as usize;
426        pos += COMPONENT_HEADER_LEN;
427        let comp0_end = pos + comp0_len;
428        if comp0_end > end {
429            return Err(Error::SectionLengthOverflow {
430                declared: comp0_len,
431                available: end - pos,
432            });
433        }
434        let (object_location, _) = ObjectLocation::parse_from(bytes, pos, comp0_end)?;
435        pos = comp0_end;
436
437        // Second component: ConnBinder
438        let (ch1, _) = bytes[pos..end]
439            .split_first_chunk::<COMPONENT_HEADER_LEN>()
440            .ok_or(Error::BufferTooShort {
441                need: pos + COMPONENT_HEADER_LEN,
442                have: end,
443                what: "BIOP ConnBinder component header",
444            })?;
445        let comp1_tag = u32::from_be_bytes([ch1[0], ch1[1], ch1[2], ch1[3]]);
446        if comp1_tag != TAG_CONN_BINDER {
447            return Err(Error::ReservedBitsViolation {
448                field: "componentId_tag[1]",
449                reason: "second liteComponent must be TAG_ConnBinder (0x49534F40)",
450            });
451        }
452        let comp1_len = ch1[4] as usize;
453        pos += COMPONENT_HEADER_LEN;
454        let comp1_end = pos + comp1_len;
455        if comp1_end > end {
456            return Err(Error::SectionLengthOverflow {
457                declared: comp1_len,
458                available: end - pos,
459            });
460        }
461        let (conn_binder, _) = ConnBinder::parse_from(bytes, pos, comp1_end)?;
462        pos = comp1_end;
463
464        // Remaining extra components
465        let extra_count = lite_components_count - 2;
466        let mut extra = Vec::with_capacity(extra_count.min(8));
467        for _ in 0..extra_count {
468            let (ch_ex, _) = bytes[pos..end]
469                .split_first_chunk::<COMPONENT_HEADER_LEN>()
470                .ok_or(Error::BufferTooShort {
471                    need: pos + COMPONENT_HEADER_LEN,
472                    have: end,
473                    what: "BIOP extra liteComponent header",
474                })?;
475            let tag = u32::from_be_bytes([ch_ex[0], ch_ex[1], ch_ex[2], ch_ex[3]]);
476            let data_len = ch_ex[4] as usize;
477            pos += COMPONENT_HEADER_LEN;
478            if pos + data_len > end {
479                return Err(Error::SectionLengthOverflow {
480                    declared: data_len,
481                    available: end - pos,
482                });
483            }
484            extra.push(LiteComponent {
485                tag,
486                data: &bytes[pos..pos + data_len],
487            });
488            pos += data_len;
489        }
490
491        Ok(BiopProfileBody {
492            object_location,
493            conn_binder,
494            extra,
495        })
496    }
497
498    fn serialized_len(&self) -> usize {
499        let ol_len = self.object_location.serialized_len();
500        let cb_len = self.conn_binder.serialized_len();
501        let extra_len: usize = self.extra.iter().map(|c| c.serialized_len()).sum();
502        BIOP_BODY_FIXED_LEN
503            + COMPONENT_HEADER_LEN
504            + ol_len
505            + COMPONENT_HEADER_LEN
506            + cb_len
507            + extra_len
508    }
509
510    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
511        let len = self.serialized_len();
512        if buf.len() < len {
513            return Err(Error::OutputBufferTooSmall {
514                need: len,
515                have: buf.len(),
516            });
517        }
518        let total_components = 2 + self.extra.len();
519        if total_components > u8::MAX as usize {
520            return Err(Error::SectionLengthOverflow {
521                declared: total_components,
522                available: u8::MAX as usize,
523            });
524        }
525        buf[0] = BYTE_ORDER_BIG_ENDIAN;
526        buf[1] = total_components as u8;
527        let mut pos = BIOP_BODY_FIXED_LEN;
528
529        // ObjectLocation component
530        let ol_data_len = self.object_location.serialized_len();
531        if ol_data_len > u8::MAX as usize {
532            return Err(Error::SectionLengthOverflow {
533                declared: ol_data_len,
534                available: u8::MAX as usize,
535            });
536        }
537        buf[pos..pos + 4].copy_from_slice(&TAG_OBJECT_LOCATION.to_be_bytes());
538        buf[pos + 4] = ol_data_len as u8;
539        pos += COMPONENT_HEADER_LEN;
540        let written = self.object_location.serialize_into_buf(&mut buf[pos..])?;
541        pos += written;
542
543        // ConnBinder component
544        let cb_data_len = self.conn_binder.serialized_len();
545        if cb_data_len > u8::MAX as usize {
546            return Err(Error::SectionLengthOverflow {
547                declared: cb_data_len,
548                available: u8::MAX as usize,
549            });
550        }
551        buf[pos..pos + 4].copy_from_slice(&TAG_CONN_BINDER.to_be_bytes());
552        buf[pos + 4] = cb_data_len as u8;
553        pos += COMPONENT_HEADER_LEN;
554        let written = self.conn_binder.serialize_into_buf(&mut buf[pos..])?;
555        pos += written;
556
557        // Extra components
558        for comp in &self.extra {
559            let data_len = comp.data.len();
560            if data_len > u8::MAX as usize {
561                return Err(Error::SectionLengthOverflow {
562                    declared: data_len,
563                    available: u8::MAX as usize,
564                });
565            }
566            buf[pos..pos + 4].copy_from_slice(&comp.tag.to_be_bytes());
567            buf[pos + 4] = data_len as u8;
568            pos += COMPONENT_HEADER_LEN;
569            buf[pos..pos + data_len].copy_from_slice(comp.data);
570            pos += data_len;
571        }
572
573        Ok(len)
574    }
575}
576
577// ── NsapAddress ───────────────────────────────────────────────────────────────
578
579/// DVB Carousel NSAP Address — 20 bytes.  TR 101 202 §4.7.3.4, Table 4.8.
580///
581/// Fixed layout: AFI(1)=0x00, Type(1)=0x00, carouselId(4), specifierType(1)=0x01,
582/// specifierData/IEEE OUI(3), tsid(2), onid(2), sid(2), reserved(4)=0xFFFFFFFF.
583#[derive(Debug, Clone, PartialEq, Eq)]
584#[cfg_attr(feature = "serde", derive(serde::Serialize))]
585pub struct NsapAddress {
586    /// `carouselId` field.
587    pub carousel_id: u32,
588    /// `specifierData` — 3-byte IEEE OUI (e.g. DVB OUI).
589    pub specifier_data: [u8; 3],
590    /// `transport_stream_id`.
591    pub transport_stream_id: u16,
592    /// `original_network_id`.
593    pub original_network_id: u16,
594    /// `service_id` — equals MPEG-2 `program_number`.
595    pub service_id: u16,
596}
597
598impl NsapAddress {
599    /// Expected AFI value: 0x00 (NSAP for private use).
600    const AFI: u8 = 0x00;
601    /// Expected Type value: 0x00 (Object carousel NSAP address).
602    const NSAP_TYPE: u8 = 0x00;
603    /// Expected specifierType value: 0x01 (IEEE OUI).
604    const SPECIFIER_TYPE: u8 = 0x01;
605    /// Reserved field value: 0xFFFFFFFF.
606    const RESERVED: u32 = 0xFFFF_FFFF;
607
608    fn parse_from(bytes: &[u8], pos: usize) -> Result<Self> {
609        let end = pos + NSAP_ADDRESS_LEN;
610        let (nsap, _) = bytes
611            .get(pos..)
612            .and_then(|s| s.split_first_chunk::<NSAP_ADDRESS_LEN>())
613            .ok_or(Error::BufferTooShort {
614                need: end,
615                have: bytes.len(),
616                what: "NSAP address (20 bytes)",
617            })?;
618        // AFI and Type are fixed per DVB profile; do not reject if they differ
619        // (be tolerant, but documented).
620        let carousel_id = u32::from_be_bytes([nsap[2], nsap[3], nsap[4], nsap[5]]);
621        // specifierType at nsap[6]
622        let specifier_data = [nsap[7], nsap[8], nsap[9]];
623        let transport_stream_id = u16::from_be_bytes([nsap[10], nsap[11]]);
624        let original_network_id = u16::from_be_bytes([nsap[12], nsap[13]]);
625        let service_id = u16::from_be_bytes([nsap[14], nsap[15]]);
626        // reserved nsap[16..20]
627        Ok(NsapAddress {
628            carousel_id,
629            specifier_data,
630            transport_stream_id,
631            original_network_id,
632            service_id,
633        })
634    }
635
636    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
637        if buf.len() < NSAP_ADDRESS_LEN {
638            return Err(Error::OutputBufferTooSmall {
639                need: NSAP_ADDRESS_LEN,
640                have: buf.len(),
641            });
642        }
643        buf[0] = Self::AFI;
644        buf[1] = Self::NSAP_TYPE;
645        buf[2..6].copy_from_slice(&self.carousel_id.to_be_bytes());
646        buf[6] = Self::SPECIFIER_TYPE;
647        buf[7..10].copy_from_slice(&self.specifier_data);
648        buf[10..12].copy_from_slice(&self.transport_stream_id.to_be_bytes());
649        buf[12..14].copy_from_slice(&self.original_network_id.to_be_bytes());
650        buf[14..16].copy_from_slice(&self.service_id.to_be_bytes());
651        buf[16..20].copy_from_slice(&Self::RESERVED.to_be_bytes());
652        Ok(NSAP_ADDRESS_LEN)
653    }
654}
655
656// ── NameComponent ─────────────────────────────────────────────────────────────
657
658/// CosNaming name component — used in `ServiceLocation` path.
659/// TR 101 202 §4.7.3.3, Table 4.7.  id and kind lengths are 32-bit.
660#[derive(Debug, Clone, PartialEq, Eq)]
661#[cfg_attr(feature = "serde", derive(serde::Serialize))]
662pub struct NameComponent<'a> {
663    /// Component id bytes.
664    #[cfg_attr(feature = "serde", serde(borrow))]
665    pub id: &'a [u8],
666    /// Component kind bytes — typically a 4-byte alias type_id.
667    #[cfg_attr(feature = "serde", serde(borrow))]
668    pub kind: &'a [u8],
669}
670
671impl<'a> NameComponent<'a> {
672    /// Wire size for a CosNaming component (32-bit lengths).
673    pub(crate) fn serialized_len_32bit(&self) -> usize {
674        NAMING_FIELD_LEN + self.id.len() + NAMING_FIELD_LEN + self.kind.len()
675    }
676
677    /// Parse one CosNaming name component with 32-bit length prefixes.
678    pub(crate) fn parse_32bit(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
679        let (bid, _) = bytes[pos..end]
680            .split_first_chunk::<4>()
681            .ok_or(Error::BufferTooShort {
682                need: pos + NAMING_FIELD_LEN,
683                have: end,
684                what: "CosNaming id_length",
685            })?;
686        let id_len = u32::from_be_bytes(*bid) as usize;
687        let id_start = pos + NAMING_FIELD_LEN;
688        if id_start + id_len > end {
689            return Err(Error::SectionLengthOverflow {
690                declared: id_len,
691                available: end - id_start,
692            });
693        }
694        let id = &bytes[id_start..id_start + id_len];
695        let kind_pos = id_start + id_len;
696        let (bkind, _) =
697            bytes[kind_pos..end]
698                .split_first_chunk::<4>()
699                .ok_or(Error::BufferTooShort {
700                    need: kind_pos + NAMING_FIELD_LEN,
701                    have: end,
702                    what: "CosNaming kind_length",
703                })?;
704        let kind_len = u32::from_be_bytes(*bkind) as usize;
705        let kind_start = kind_pos + NAMING_FIELD_LEN;
706        if kind_start + kind_len > end {
707            return Err(Error::SectionLengthOverflow {
708                declared: kind_len,
709                available: end - kind_start,
710            });
711        }
712        let kind = &bytes[kind_start..kind_start + kind_len];
713        Ok((NameComponent { id, kind }, kind_start + kind_len))
714    }
715
716    /// Serialize one CosNaming name component with 32-bit length prefixes.
717    pub(crate) fn serialize_32bit(&self, buf: &mut [u8]) -> Result<usize> {
718        let len = self.serialized_len_32bit();
719        if buf.len() < len {
720            return Err(Error::OutputBufferTooSmall {
721                need: len,
722                have: buf.len(),
723            });
724        }
725        buf[0..4].copy_from_slice(&(self.id.len() as u32).to_be_bytes());
726        buf[4..4 + self.id.len()].copy_from_slice(self.id);
727        let kind_pos = 4 + self.id.len();
728        buf[kind_pos..kind_pos + 4].copy_from_slice(&(self.kind.len() as u32).to_be_bytes());
729        buf[kind_pos + 4..kind_pos + 4 + self.kind.len()].copy_from_slice(self.kind);
730        Ok(len)
731    }
732
733    /// Wire size for a DVB Directory BIOP name component (8-bit lengths).
734    pub(crate) fn serialized_len_8bit(&self) -> usize {
735        1 + self.id.len() + 1 + self.kind.len()
736    }
737
738    /// Parse one BIOP Directory name component with 8-bit length prefixes.
739    pub(crate) fn parse_8bit(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
740        if pos + 1 > end {
741            return Err(Error::BufferTooShort {
742                need: pos + 1,
743                have: end,
744                what: "BIOP NameComponent id_length (8-bit)",
745            });
746        }
747        let id_len = bytes[pos] as usize;
748        let id_start = pos + 1;
749        if id_start + id_len > end {
750            return Err(Error::SectionLengthOverflow {
751                declared: id_len,
752                available: end - id_start,
753            });
754        }
755        let id = &bytes[id_start..id_start + id_len];
756        let kind_pos = id_start + id_len;
757        if kind_pos + 1 > end {
758            return Err(Error::BufferTooShort {
759                need: kind_pos + 1,
760                have: end,
761                what: "BIOP NameComponent kind_length (8-bit)",
762            });
763        }
764        let kind_len = bytes[kind_pos] as usize;
765        let kind_start = kind_pos + 1;
766        if kind_start + kind_len > end {
767            return Err(Error::SectionLengthOverflow {
768                declared: kind_len,
769                available: end - kind_start,
770            });
771        }
772        let kind = &bytes[kind_start..kind_start + kind_len];
773        Ok((NameComponent { id, kind }, kind_start + kind_len))
774    }
775
776    /// Serialize one BIOP Directory name component with 8-bit length prefixes.
777    pub(crate) fn serialize_8bit(&self, buf: &mut [u8]) -> Result<usize> {
778        let len = self.serialized_len_8bit();
779        if buf.len() < len {
780            return Err(Error::OutputBufferTooSmall {
781                need: len,
782                have: buf.len(),
783            });
784        }
785        if self.id.len() > u8::MAX as usize {
786            return Err(Error::SectionLengthOverflow {
787                declared: self.id.len(),
788                available: u8::MAX as usize,
789            });
790        }
791        if self.kind.len() > u8::MAX as usize {
792            return Err(Error::SectionLengthOverflow {
793                declared: self.kind.len(),
794                available: u8::MAX as usize,
795            });
796        }
797        buf[0] = self.id.len() as u8;
798        buf[1..1 + self.id.len()].copy_from_slice(self.id);
799        let kind_pos = 1 + self.id.len();
800        buf[kind_pos] = self.kind.len() as u8;
801        buf[kind_pos + 1..kind_pos + 1 + self.kind.len()].copy_from_slice(self.kind);
802        Ok(len)
803    }
804}
805
806// ── ServiceLocation ───────────────────────────────────────────────────────────
807
808/// DSM::ServiceLocation — first mandatory component of Lite Options Profile Body.
809/// TR 101 202 §4.7.3.3, Table 4.7.
810#[derive(Debug, Clone, PartialEq, Eq)]
811#[cfg_attr(feature = "serde", derive(serde::Serialize))]
812pub struct ServiceLocation<'a> {
813    /// DVB carousel NSAP address (always 20 bytes, serviceDomain_length=0x14).
814    pub service_domain: NsapAddress,
815    /// CosNaming path components (nameComponents_count, 32-bit-length fields).
816    #[cfg_attr(feature = "serde", serde(borrow))]
817    pub path: Vec<NameComponent<'a>>,
818    /// `InitialContext_data`.
819    #[cfg_attr(feature = "serde", serde(borrow))]
820    pub initial_context: &'a [u8],
821}
822
823impl<'a> ServiceLocation<'a> {
824    fn serialized_len(&self) -> usize {
825        SERVICE_DOMAIN_LEN_FIELD
826            + NSAP_ADDRESS_LEN
827            + NAMING_COUNT_LEN
828            + self
829                .path
830                .iter()
831                .map(|c| c.serialized_len_32bit())
832                .sum::<usize>()
833            + INITIAL_CONTEXT_LEN_FIELD
834            + self.initial_context.len()
835    }
836
837    fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
838        if pos + SERVICE_DOMAIN_LEN_FIELD > end {
839            return Err(Error::BufferTooShort {
840                need: pos + SERVICE_DOMAIN_LEN_FIELD,
841                have: end,
842                what: "ServiceLocation serviceDomain_length",
843            });
844        }
845        let sd_len = bytes[pos] as usize;
846        if sd_len != NSAP_ADDRESS_LEN {
847            return Err(Error::ValueOutOfRange {
848                field: "serviceDomain_length",
849                reason: "DVB Carousel NSAP address must be exactly 20 bytes (0x14)",
850            });
851        }
852        let sd_start = pos + SERVICE_DOMAIN_LEN_FIELD;
853        if sd_start + NSAP_ADDRESS_LEN > end {
854            return Err(Error::BufferTooShort {
855                need: sd_start + NSAP_ADDRESS_LEN,
856                have: end,
857                what: "ServiceLocation serviceDomain_data",
858            });
859        }
860        let service_domain = NsapAddress::parse_from(bytes, sd_start)?;
861        let mut cur = sd_start + NSAP_ADDRESS_LEN;
862
863        let (bnc, _) = bytes[cur..end]
864            .split_first_chunk::<4>()
865            .ok_or(Error::BufferTooShort {
866                need: cur + NAMING_COUNT_LEN,
867                have: end,
868                what: "ServiceLocation nameComponents_count",
869            })?;
870        let name_count = u32::from_be_bytes(*bnc) as usize;
871        cur += NAMING_COUNT_LEN;
872        let mut path = Vec::with_capacity(name_count.min(16));
873        for _ in 0..name_count {
874            let (nc, next) = NameComponent::parse_32bit(bytes, cur, end)?;
875            path.push(nc);
876            cur = next;
877        }
878
879        let (bic, _) = bytes[cur..end]
880            .split_first_chunk::<4>()
881            .ok_or(Error::BufferTooShort {
882                need: cur + INITIAL_CONTEXT_LEN_FIELD,
883                have: end,
884                what: "ServiceLocation initialContext_length",
885            })?;
886        let ic_len = u32::from_be_bytes(*bic) as usize;
887        cur += INITIAL_CONTEXT_LEN_FIELD;
888        if cur + ic_len > end {
889            return Err(Error::SectionLengthOverflow {
890                declared: ic_len,
891                available: end - cur,
892            });
893        }
894        let initial_context = &bytes[cur..cur + ic_len];
895        cur += ic_len;
896        Ok((
897            ServiceLocation {
898                service_domain,
899                path,
900                initial_context,
901            },
902            cur,
903        ))
904    }
905
906    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
907        let len = self.serialized_len();
908        if buf.len() < len {
909            return Err(Error::OutputBufferTooSmall {
910                need: len,
911                have: buf.len(),
912            });
913        }
914        buf[0] = NSAP_ADDRESS_LEN as u8; // serviceDomain_length = 0x14
915        self.service_domain
916            .serialize_into_buf(&mut buf[1..1 + NSAP_ADDRESS_LEN])?;
917        let mut pos = SERVICE_DOMAIN_LEN_FIELD + NSAP_ADDRESS_LEN;
918        buf[pos..pos + 4].copy_from_slice(&(self.path.len() as u32).to_be_bytes());
919        pos += NAMING_COUNT_LEN;
920        for nc in &self.path {
921            let written = nc.serialize_32bit(&mut buf[pos..])?;
922            pos += written;
923        }
924        let ic_len = self.initial_context.len();
925        buf[pos..pos + 4].copy_from_slice(&(ic_len as u32).to_be_bytes());
926        pos += INITIAL_CONTEXT_LEN_FIELD;
927        buf[pos..pos + ic_len].copy_from_slice(self.initial_context);
928        pos += ic_len;
929        Ok(pos)
930    }
931}
932
933// ── LiteOptionsProfileBody ────────────────────────────────────────────────────
934
935/// Lite Options Profile Body — decoded contents of a `TAG_LITE_OPTIONS` profile.
936/// TR 101 202 §4.7.3.3, Table 4.7.
937#[derive(Debug, Clone, PartialEq, Eq)]
938#[cfg_attr(feature = "serde", derive(serde::Serialize))]
939pub struct LiteOptionsProfileBody<'a> {
940    /// First mandatory component: DSM::ServiceLocation.
941    #[cfg_attr(feature = "serde", serde(borrow))]
942    pub service_location: ServiceLocation<'a>,
943    /// Extra components beyond the mandatory one (tag + raw data, 8-bit length prefix).
944    pub extra: Vec<LiteComponent<'a>>,
945}
946
947impl<'a> LiteOptionsProfileBody<'a> {
948    fn parse_from(bytes: &'a [u8]) -> Result<Self> {
949        let end = bytes.len();
950        if end < LITE_OPTIONS_BODY_FIXED_LEN {
951            return Err(Error::BufferTooShort {
952                need: LITE_OPTIONS_BODY_FIXED_LEN,
953                have: end,
954                what: "LiteOptions Profile Body fixed fields",
955            });
956        }
957        let byte_order = bytes[0];
958        if byte_order != BYTE_ORDER_BIG_ENDIAN {
959            return Err(Error::ReservedBitsViolation {
960                field: "profile_data_byte_order (LiteOptions)",
961                reason: "must be 0x00 (big-endian) per DVB mandatory constraint",
962            });
963        }
964        let component_count = bytes[1] as usize;
965        if component_count < 1 {
966            return Err(Error::ValueOutOfRange {
967                field: "component_count (LiteOptions)",
968                reason: "must have at least 1 component (ServiceLocation)",
969            });
970        }
971        let mut pos = LITE_OPTIONS_BODY_FIXED_LEN;
972
973        // First component: ServiceLocation (32-bit component_data_length)
974        let (slch, _) = bytes[pos..end]
975            .split_first_chunk::<SERVICE_LOCATION_COMP_HEADER_LEN>()
976            .ok_or(Error::BufferTooShort {
977                need: pos + SERVICE_LOCATION_COMP_HEADER_LEN,
978                have: end,
979                what: "LiteOptions ServiceLocation component header",
980            })?;
981        let comp0_tag = u32::from_be_bytes([slch[0], slch[1], slch[2], slch[3]]);
982        if comp0_tag != TAG_SERVICE_LOCATION {
983            return Err(Error::ReservedBitsViolation {
984                field: "componentId_tag[0] (LiteOptions)",
985                reason: "first component must be TAG_ServiceLocation (0x49534F46)",
986            });
987        }
988        let comp0_len = u32::from_be_bytes([slch[4], slch[5], slch[6], slch[7]]) as usize;
989        pos += SERVICE_LOCATION_COMP_HEADER_LEN;
990        let comp0_end = pos + comp0_len;
991        if comp0_end > end {
992            return Err(Error::SectionLengthOverflow {
993                declared: comp0_len,
994                available: end - pos,
995            });
996        }
997        let (service_location, _) = ServiceLocation::parse_from(bytes, pos, comp0_end)?;
998        pos = comp0_end;
999
1000        // Extra components (8-bit length)
1001        let extra_count = component_count - 1;
1002        let mut extra = Vec::with_capacity(extra_count.min(8));
1003        for _ in 0..extra_count {
1004            let (ech, _) = bytes[pos..end]
1005                .split_first_chunk::<COMPONENT_HEADER_LEN>()
1006                .ok_or(Error::BufferTooShort {
1007                    need: pos + COMPONENT_HEADER_LEN,
1008                    have: end,
1009                    what: "LiteOptions extra component header",
1010                })?;
1011            let tag = u32::from_be_bytes([ech[0], ech[1], ech[2], ech[3]]);
1012            let data_len = ech[4] as usize;
1013            pos += COMPONENT_HEADER_LEN;
1014            if pos + data_len > end {
1015                return Err(Error::SectionLengthOverflow {
1016                    declared: data_len,
1017                    available: end - pos,
1018                });
1019            }
1020            extra.push(LiteComponent {
1021                tag,
1022                data: &bytes[pos..pos + data_len],
1023            });
1024            pos += data_len;
1025        }
1026
1027        Ok(LiteOptionsProfileBody {
1028            service_location,
1029            extra,
1030        })
1031    }
1032
1033    fn serialized_len(&self) -> usize {
1034        let sl_data_len = self.service_location.serialized_len();
1035        let extra_len: usize = self.extra.iter().map(|c| c.serialized_len()).sum();
1036        LITE_OPTIONS_BODY_FIXED_LEN + SERVICE_LOCATION_COMP_HEADER_LEN + sl_data_len + extra_len
1037    }
1038
1039    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
1040        let len = self.serialized_len();
1041        if buf.len() < len {
1042            return Err(Error::OutputBufferTooSmall {
1043                need: len,
1044                have: buf.len(),
1045            });
1046        }
1047        let total_components = 1 + self.extra.len();
1048        if total_components > u8::MAX as usize {
1049            return Err(Error::SectionLengthOverflow {
1050                declared: total_components,
1051                available: u8::MAX as usize,
1052            });
1053        }
1054        buf[0] = BYTE_ORDER_BIG_ENDIAN;
1055        buf[1] = total_components as u8;
1056        let mut pos = LITE_OPTIONS_BODY_FIXED_LEN;
1057
1058        // ServiceLocation component (32-bit data length)
1059        let sl_data_len = self.service_location.serialized_len();
1060        buf[pos..pos + 4].copy_from_slice(&TAG_SERVICE_LOCATION.to_be_bytes());
1061        buf[pos + 4..pos + 8].copy_from_slice(&(sl_data_len as u32).to_be_bytes());
1062        pos += SERVICE_LOCATION_COMP_HEADER_LEN;
1063        let written = self.service_location.serialize_into_buf(&mut buf[pos..])?;
1064        pos += written;
1065
1066        // Extra components (8-bit data length)
1067        for comp in &self.extra {
1068            let data_len = comp.data.len();
1069            if data_len > u8::MAX as usize {
1070                return Err(Error::SectionLengthOverflow {
1071                    declared: data_len,
1072                    available: u8::MAX as usize,
1073                });
1074            }
1075            buf[pos..pos + 4].copy_from_slice(&comp.tag.to_be_bytes());
1076            buf[pos + 4] = data_len as u8;
1077            pos += COMPONENT_HEADER_LEN;
1078            buf[pos..pos + data_len].copy_from_slice(comp.data);
1079            pos += data_len;
1080        }
1081
1082        Ok(len)
1083    }
1084}
1085
1086// ── TaggedProfile ─────────────────────────────────────────────────────────────
1087
1088/// A tagged profile entry in an `IOP::IOR`.
1089/// TR 101 202 §4.7.3, Table 4.3.
1090#[derive(Debug, Clone, PartialEq, Eq)]
1091#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1092#[non_exhaustive]
1093pub enum TaggedProfile<'a> {
1094    /// `TAG_BIOP` (0x49534F06) — BIOP Profile Body.
1095    Biop(BiopProfileBody<'a>),
1096    /// `TAG_LITE_OPTIONS` (0x49534F05) — Lite Options Profile Body.
1097    LiteOptions(LiteOptionsProfileBody<'a>),
1098    /// Any other `profileId_tag`.
1099    Unknown {
1100        /// The raw `profileId_tag` value.
1101        tag: u32,
1102        /// Raw `profile_data` bytes.
1103        #[cfg_attr(feature = "serde", serde(borrow))]
1104        data: &'a [u8],
1105    },
1106}
1107
1108impl TaggedProfile<'_> {
1109    fn serialized_len(&self) -> usize {
1110        let data_len = match self {
1111            Self::Biop(b) => b.serialized_len(),
1112            Self::LiteOptions(l) => l.serialized_len(),
1113            Self::Unknown { data, .. } => data.len(),
1114        };
1115        PROFILE_HEADER_LEN + data_len
1116    }
1117
1118    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
1119        let len = self.serialized_len();
1120        if buf.len() < len {
1121            return Err(Error::OutputBufferTooSmall {
1122                need: len,
1123                have: buf.len(),
1124            });
1125        }
1126        let (tag, data_len) = match self {
1127            Self::Biop(b) => (TAG_BIOP, b.serialized_len()),
1128            Self::LiteOptions(l) => (TAG_LITE_OPTIONS, l.serialized_len()),
1129            Self::Unknown { tag, data } => (*tag, data.len()),
1130        };
1131        buf[0..4].copy_from_slice(&tag.to_be_bytes());
1132        buf[4..8].copy_from_slice(&(data_len as u32).to_be_bytes());
1133        let pos = PROFILE_HEADER_LEN;
1134        match self {
1135            Self::Biop(b) => {
1136                b.serialize_into_buf(&mut buf[pos..])?;
1137            }
1138            Self::LiteOptions(l) => {
1139                l.serialize_into_buf(&mut buf[pos..])?;
1140            }
1141            Self::Unknown { data, .. } => {
1142                buf[pos..pos + data.len()].copy_from_slice(data);
1143            }
1144        }
1145        Ok(len)
1146    }
1147}
1148
1149// ── Ior ───────────────────────────────────────────────────────────────────────
1150
1151/// `IOP::IOR` — Interoperable Object Reference.
1152/// TR 101 202 §4.7.3.1, Table 4.3.
1153///
1154/// Carries the `type_id` (object kind) and one or more tagged profiles that
1155/// describe where to find the object in the carousel.
1156#[derive(Debug, Clone, PartialEq, Eq)]
1157#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1158pub struct Ior<'a> {
1159    /// `type_id` bytes — DVB: always a 4-byte alias (see [`ObjectKind`]).
1160    #[cfg_attr(feature = "serde", serde(borrow))]
1161    pub type_id: &'a [u8],
1162    /// One or more tagged profiles.  The first must be BIOP or LiteOptions.
1163    pub profiles: Vec<TaggedProfile<'a>>,
1164}
1165
1166impl<'a> Ior<'a> {
1167    /// Decode the `type_id` as an [`ObjectKind`].
1168    pub fn object_kind(&self) -> ObjectKind {
1169        if self.type_id.len() == 4 {
1170            let mut arr = [0u8; 4];
1171            arr.copy_from_slice(self.type_id);
1172            ObjectKind::from_bytes(arr)
1173        } else {
1174            ObjectKind::Unknown([0; 4])
1175        }
1176    }
1177
1178    /// Return the first BIOP Profile Body from this IOR, if present.
1179    pub fn biop_profile(&self) -> Option<&BiopProfileBody<'a>> {
1180        for p in &self.profiles {
1181            if let TaggedProfile::Biop(b) = p {
1182                return Some(b);
1183            }
1184        }
1185        None
1186    }
1187}
1188
1189impl<'a> Parse<'a> for Ior<'a> {
1190    type Error = crate::error::Error;
1191
1192    fn parse(bytes: &'a [u8]) -> Result<Self> {
1193        let end = bytes.len();
1194        let (ior_hdr, _) =
1195            bytes
1196                .split_first_chunk::<IOR_FIXED_LEN>()
1197                .ok_or(Error::BufferTooShort {
1198                    need: IOR_FIXED_LEN,
1199                    have: end,
1200                    what: "IOP::IOR fixed fields",
1201                })?;
1202        let type_id_length =
1203            u32::from_be_bytes([ior_hdr[0], ior_hdr[1], ior_hdr[2], ior_hdr[3]]) as usize;
1204        // DVB: only alias type_ids (N%4==0); reject non-conformant.
1205        if type_id_length % 4 != 0 {
1206            return Err(Error::ValueOutOfRange {
1207                field: "IOR.type_id_length",
1208                reason: "type_id_length must be a multiple of 4 (DVB alias type_ids only — \
1209                         non-aligned type_ids are not supported per TR 101 202 §4.7.3.1)",
1210            });
1211        }
1212        let mut pos = 4;
1213        if pos + type_id_length > end {
1214            return Err(Error::SectionLengthOverflow {
1215                declared: type_id_length,
1216                available: end - pos,
1217            });
1218        }
1219        let type_id = &bytes[pos..pos + type_id_length];
1220        pos += type_id_length;
1221
1222        let (bpc, _) = bytes[pos..end]
1223            .split_first_chunk::<4>()
1224            .ok_or(Error::BufferTooShort {
1225                need: pos + 4,
1226                have: end,
1227                what: "IOR taggedProfiles_count",
1228            })?;
1229        let profiles_count = u32::from_be_bytes(*bpc) as usize;
1230        pos += 4;
1231
1232        let mut profiles = Vec::with_capacity(profiles_count.min(8));
1233        for _ in 0..profiles_count {
1234            let (phdr, _) = bytes[pos..end]
1235                .split_first_chunk::<PROFILE_HEADER_LEN>()
1236                .ok_or(Error::BufferTooShort {
1237                    need: pos + PROFILE_HEADER_LEN,
1238                    have: end,
1239                    what: "TaggedProfile header",
1240                })?;
1241            let tag = u32::from_be_bytes([phdr[0], phdr[1], phdr[2], phdr[3]]);
1242            let data_len = u32::from_be_bytes([phdr[4], phdr[5], phdr[6], phdr[7]]) as usize;
1243            pos += PROFILE_HEADER_LEN;
1244            if pos + data_len > end {
1245                return Err(Error::SectionLengthOverflow {
1246                    declared: data_len,
1247                    available: end - pos,
1248                });
1249            }
1250            let profile_data = &bytes[pos..pos + data_len];
1251            let profile = match tag {
1252                TAG_BIOP => TaggedProfile::Biop(BiopProfileBody::parse_from(profile_data)?),
1253                TAG_LITE_OPTIONS => {
1254                    TaggedProfile::LiteOptions(LiteOptionsProfileBody::parse_from(profile_data)?)
1255                }
1256                _ => TaggedProfile::Unknown {
1257                    tag,
1258                    data: profile_data,
1259                },
1260            };
1261            profiles.push(profile);
1262            pos += data_len;
1263        }
1264
1265        Ok(Ior { type_id, profiles })
1266    }
1267}
1268
1269impl Serialize for Ior<'_> {
1270    type Error = crate::error::Error;
1271
1272    fn serialized_len(&self) -> usize {
1273        let type_id_len = self.type_id.len();
1274        let profiles_len: usize = self.profiles.iter().map(|p| p.serialized_len()).sum();
1275        4 // type_id_length field
1276            + type_id_len
1277            + 4 // taggedProfiles_count field
1278            + profiles_len
1279    }
1280
1281    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1282        let len = self.serialized_len();
1283        if buf.len() < len {
1284            return Err(Error::OutputBufferTooSmall {
1285                need: len,
1286                have: buf.len(),
1287            });
1288        }
1289        if self.type_id.len() % 4 != 0 {
1290            return Err(Error::ValueOutOfRange {
1291                field: "IOR.type_id_length",
1292                reason: "type_id_length must be a multiple of 4 (DVB alias type_ids only)",
1293            });
1294        }
1295        buf[0..4].copy_from_slice(&(self.type_id.len() as u32).to_be_bytes());
1296        buf[4..4 + self.type_id.len()].copy_from_slice(self.type_id);
1297        let mut pos = 4 + self.type_id.len();
1298        buf[pos..pos + 4].copy_from_slice(&(self.profiles.len() as u32).to_be_bytes());
1299        pos += 4;
1300        for profile in &self.profiles {
1301            let written = profile.serialize_into_buf(&mut buf[pos..])?;
1302            pos += written;
1303        }
1304        Ok(len)
1305    }
1306}
1307
1308// ── Tests ─────────────────────────────────────────────────────────────────────
1309
1310#[cfg(test)]
1311mod tests {
1312    use super::*;
1313    use broadcast_common::Parse;
1314
1315    fn sample_ior() -> Vec<u8> {
1316        // IOR for a ServiceGateway object in the m6 fixture format:
1317        // type_id = "srg\0", 1 profile TAG_BIOP
1318        // ObjectLocation: carousel_id=0xAB, module_id=1, v1.0, key=[0x01]
1319        // ConnBinder: 1 tap, use=0x0016, assoc=0x47, selector=[0x00,0x01,0x80,0x00,0x00,0x02,0xFF,0xFF,0xFF,0xFF]
1320        #[rustfmt::skip]
1321        let bytes: &[u8] = &[
1322            // type_id_length=4
1323            0x00, 0x00, 0x00, 0x04,
1324            // type_id = "srg\0"
1325            0x73, 0x72, 0x67, 0x00,
1326            // taggedProfiles_count=1
1327            0x00, 0x00, 0x00, 0x01,
1328            // profileId_tag = TAG_BIOP
1329            0x49, 0x53, 0x4F, 0x06,
1330            // profile_data_length = 40
1331            0x00, 0x00, 0x00, 0x28,
1332            // byte_order=0, liteComponents_count=2
1333            0x00, 0x02,
1334            // ObjectLocation: tag(4)+len(1)
1335            0x49, 0x53, 0x4F, 0x50,  0x0A,
1336            // carouselId=0xAB, moduleId=1, v1.0, key_len=1, key=0x01
1337            0x00, 0x00, 0x00, 0xAB,  0x00, 0x01,  0x01, 0x00,  0x01,  0x01,
1338            // ConnBinder: tag(4)+len(1)
1339            0x49, 0x53, 0x4F, 0x40,  0x12,
1340            // taps_count=1, tap: id=0, use=0x0016, assoc=0x47, sel_len=10, selector
1341            0x01,  0x00, 0x00,  0x00, 0x16,  0x00, 0x47,  0x0A,
1342            0x00, 0x01, 0x80, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF,
1343        ];
1344        bytes.to_vec()
1345    }
1346
1347    #[test]
1348    fn ior_round_trip() {
1349        let raw = sample_ior();
1350        let ior = Ior::parse(&raw).unwrap();
1351        let mut out = vec![0u8; ior.serialized_len()];
1352        ior.serialize_into(&mut out).unwrap();
1353        assert_eq!(out, raw, "IOR round-trip byte-exact");
1354    }
1355
1356    #[test]
1357    fn ior_byte_anchor_m6_sgw() {
1358        let raw = sample_ior();
1359        let ior = Ior::parse(&raw).unwrap();
1360
1361        assert_eq!(ior.type_id, b"srg\0");
1362        assert_eq!(ior.object_kind(), ObjectKind::ServiceGateway);
1363        assert_eq!(ior.profiles.len(), 1);
1364
1365        let bp = ior.biop_profile().unwrap();
1366        assert_eq!(bp.object_location.carousel_id, 0xAB);
1367        assert_eq!(bp.object_location.module_id, 1);
1368        assert_eq!(bp.object_location.version_major, 1);
1369        assert_eq!(bp.object_location.version_minor, 0);
1370        assert_eq!(bp.object_location.object_key, &[0x01]);
1371
1372        assert_eq!(bp.conn_binder.taps.len(), 1);
1373        let tap = &bp.conn_binder.taps[0];
1374        assert_eq!(tap.id, 0);
1375        assert_eq!(tap.use_, 0x0016);
1376        assert_eq!(tap.association_tag, 0x47);
1377        assert_eq!(
1378            tap.selector,
1379            &[0x00, 0x01, 0x80, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF]
1380        );
1381        assert_eq!(tap.transaction_id(), Some(0x80000002));
1382        assert_eq!(tap.timeout(), Some(0xFFFFFFFF));
1383    }
1384
1385    #[test]
1386    fn ior_rejects_non_aligned_type_id() {
1387        // type_id_length = 3 (not a multiple of 4)
1388        let bytes: &[u8] = &[
1389            0x00, 0x00, 0x00, 0x03, 0x64, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00,
1390        ];
1391        assert!(matches!(
1392            Ior::parse(bytes).unwrap_err(),
1393            crate::error::Error::ValueOutOfRange {
1394                field: "IOR.type_id_length",
1395                ..
1396            }
1397        ));
1398    }
1399
1400    #[test]
1401    fn object_kind_roundtrip() {
1402        let kinds = [
1403            ObjectKind::Directory,
1404            ObjectKind::File,
1405            ObjectKind::Stream,
1406            ObjectKind::ServiceGateway,
1407            ObjectKind::StreamEvent,
1408            ObjectKind::Unknown([0x01, 0x02, 0x03, 0x04]),
1409        ];
1410        for k in &kinds {
1411            let b = k.to_bytes();
1412            assert_eq!(ObjectKind::from_bytes(b), *k);
1413        }
1414    }
1415
1416    #[cfg(feature = "serde")]
1417    #[test]
1418    fn ior_serde_round_trip() {
1419        let raw = sample_ior();
1420        let ior = Ior::parse(&raw).unwrap();
1421        let json = serde_json::to_string(&ior).unwrap();
1422        // type_id is serialized as byte array; carousel_id is a field in ObjectLocation
1423        assert!(
1424            json.contains("carousel_id"),
1425            "JSON must contain carousel_id field"
1426        );
1427        assert!(
1428            json.contains("\"Biop\""),
1429            "JSON must contain Biop profile variant"
1430        );
1431    }
1432}