Skip to main content

dvb_si/carousel/biop/
message.rs

1//! BIOP message types and `ModuleInfo` / `ServiceGatewayInfo` wire structures.
2//!
3//! All wire layouts from `docs/iso_13818_6_biop.md` (ETSI TR 101 202 §4.7.4–4.7.5).
4//!
5//! # Key entry points
6//!
7//! - [`BiopMessage::parse_at`] — parse one BIOP message from a slice, returning the
8//!   message and the number of bytes consumed (use to walk a module buffer).
9//! - [`ModuleInfo::parse`] — parse the DII `moduleInfoBytes` (Table 4.14).
10//! - [`ServiceGatewayInfo::parse`] — parse the DSI `privateData` (Table 4.15).
11
12use alloc::vec;
13use alloc::vec::Vec;
14
15use super::{
16    ior::{Ior, NameComponent},
17    BINDING_NCONTEXT, BINDING_NOBJECT, BIOP_MAGIC, BIOP_VERSION_MAJOR, BIOP_VERSION_MINOR,
18    BYTE_ORDER_BIG_ENDIAN, COMPRESSED_MODULE_DESCRIPTOR_TAG,
19};
20use crate::error::{Error, Result};
21use dvb_common::{Parse, Serialize};
22
23/// Binding type — TR 101 202 §4.7.4.1, Table 4.9
24/// (`docs/iso_13818_6_biop.md`, p. 55–56).
25///
26/// Indicates whether a BIOP binding names a non-directory/gateway object
27/// (`nobject`) or a directory or service gateway (`ncontext`).
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize))]
30#[non_exhaustive]
31pub enum BindingType {
32    /// `0x01` — name bound to a non-Directory/ServiceGateway object (`nobject`).
33    NObject,
34    /// `0x02` — name bound to a Directory or ServiceGateway (`ncontext`).
35    NContext,
36    /// Reserved/unallocated wire value, preserved verbatim for round-trip.
37    Reserved(u8),
38}
39
40impl BindingType {
41    #[must_use]
42    /// Creates a value from a wire byte, preserving every possible byte value for
43    /// lossless round-trip.
44    pub fn from_u8(v: u8) -> Self {
45        match v {
46            BINDING_NOBJECT => Self::NObject,
47            BINDING_NCONTEXT => Self::NContext,
48            v => Self::Reserved(v),
49        }
50    }
51
52    #[must_use]
53    /// Returns the wire byte for this value.
54    pub fn to_u8(self) -> u8 {
55        match self {
56            Self::NObject => BINDING_NOBJECT,
57            Self::NContext => BINDING_NCONTEXT,
58            Self::Reserved(v) => v,
59        }
60    }
61
62    #[must_use]
63    /// Returns the spec token for this value.
64    pub fn name(self) -> &'static str {
65        match self {
66            Self::NObject => "nobject",
67            Self::NContext => "ncontext",
68            Self::Reserved(_) => "reserved",
69        }
70    }
71}
72dvb_common::impl_spec_display!(BindingType, Reserved);
73
74// ── Message header constants ──────────────────────────────────────────────────
75
76/// BIOP message header: magic(4)+major(1)+minor(1)+byte_order(1)+message_type(1)+message_size(4) = 12.
77const BIOP_HEADER_LEN: usize = 12;
78/// `objectKey_length` (1 byte) field size.
79const OBJECT_KEY_LEN_FIELD: usize = 1;
80/// `objectKind_length` (4 bytes) field size.
81const OBJECT_KIND_LEN_FIELD: usize = 4;
82/// `objectKind_data` is always 4 bytes in DVB.
83const OBJECT_KIND_DATA_LEN: usize = 4;
84/// `objectInfo_length` (2 bytes) field size.
85const OBJECT_INFO_LEN_FIELD: usize = 2;
86/// `serviceContextList_count` (1 byte) field size.
87const SERVICE_CONTEXT_COUNT_FIELD: usize = 1;
88/// Per service context: context_id(4) + context_data_length(2).
89const SERVICE_CONTEXT_FIXED: usize = 6;
90/// `messageBody_length` (4 bytes) field size.
91const MESSAGE_BODY_LEN_FIELD: usize = 4;
92/// `bindings_count` (2 bytes) field size.
93const BINDINGS_COUNT_FIELD: usize = 2;
94/// `nameComponents_count` in a BIOP binding name: 1 byte.
95const BINDING_NAME_COUNT_FIELD: usize = 1;
96/// `bindingType` (1 byte) field.
97const BINDING_TYPE_FIELD: usize = 1;
98/// `objectInfo_length` in a binding (2 bytes).
99const BINDING_OBJ_INFO_LEN_FIELD: usize = 2;
100/// FileMessage: `content_length` (4 bytes).
101const FILE_CONTENT_LEN_FIELD: usize = 4;
102/// FileMessage: `ContentSize` (8 bytes, first 8 bytes of objectInfo).
103const FILE_CONTENT_SIZE_LEN: usize = 8;
104/// StreamMessage: `aDescription_length` field (1 byte).
105const STREAM_ADESC_LEN_FIELD: usize = 1;
106/// StreamMessage: `duration.aSeconds`(4) + `duration.aMicroSeconds`(2) + `audio`(1) + `video`(1) + `data`(1) = 9.
107const STREAM_INFO_FIXED: usize = 9;
108/// StreamMessage/StreamEventMessage: `taps_count` field (1 byte) in the message body.
109const STREAM_TAPS_COUNT_FIELD: usize = 1;
110/// StreamEventMessage: `eventNames_count` field (2 bytes).
111const STREAM_EVENT_NAMES_COUNT_FIELD: usize = 2;
112/// StreamEventMessage: per `eventName_length` field (1 byte).
113const STREAM_EVENT_NAME_LEN_FIELD: usize = 1;
114/// StreamEventMessage: `eventIds_count` field (1 byte).
115const STREAM_EVENT_IDS_COUNT_FIELD: usize = 1;
116/// StreamEventMessage: each `eventId` (2 bytes).
117const STREAM_EVENT_ID_LEN: usize = 2;
118/// ModuleInfo: ModuleTimeOut(4)+BlockTimeOut(4)+MinBlockTime(4) = 12.
119const MODULE_INFO_FIXED: usize = 12;
120/// ModuleInfo: taps_count (1 byte).
121const MODULE_TAPS_COUNT_FIELD: usize = 1;
122/// ModuleInfo: UserInfoLength (1 byte) — note: 8-bit, not 16-bit.
123const MODULE_USER_INFO_LEN_FIELD: usize = 1;
124/// SGI: downloadTaps_count (1 byte).
125const SGI_DOWNLOAD_TAPS_COUNT_FIELD: usize = 1;
126/// SGI: userInfoLength (2 bytes).
127const SGI_USER_INFO_LEN_FIELD: usize = 2;
128
129// ── Binding ───────────────────────────────────────────────────────────────────
130
131/// One binding in a `DirectoryMessage` or `ServiceGatewayMessage`.
132/// TR 101 202 §4.7.4.1, Table 4.9.
133#[derive(Debug, Clone, PartialEq, Eq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize))]
135pub struct Binding<'a> {
136    /// Name components — DVB: exactly one component.
137    #[cfg_attr(feature = "serde", serde(borrow))]
138    pub name: Vec<NameComponent<'a>>,
139    /// `bindingType` — `0x01` (`nobject`) or `0x02` (`ncontext`); see the module-level constants.
140    pub binding_type: BindingType,
141    /// IOR of the bound object.
142    pub ior: Ior<'a>,
143    /// Per-binding `objectInfo` data.
144    #[cfg_attr(feature = "serde", serde(borrow))]
145    pub object_info: &'a [u8],
146}
147
148impl<'a> Binding<'a> {
149    fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
150        // nameComponents_count (1 byte)
151        if pos + BINDING_NAME_COUNT_FIELD > end {
152            return Err(Error::BufferTooShort {
153                need: pos + BINDING_NAME_COUNT_FIELD,
154                have: end,
155                what: "Binding nameComponents_count",
156            });
157        }
158        let name_count = bytes[pos] as usize;
159        let mut cur = pos + BINDING_NAME_COUNT_FIELD;
160        let mut name = Vec::with_capacity(name_count.min(4));
161        for _ in 0..name_count {
162            let (nc, next) = NameComponent::parse_8bit(bytes, cur, end)?;
163            name.push(nc);
164            cur = next;
165        }
166
167        // bindingType (1 byte)
168        if cur + BINDING_TYPE_FIELD > end {
169            return Err(Error::BufferTooShort {
170                need: cur + BINDING_TYPE_FIELD,
171                have: end,
172                what: "Binding bindingType",
173            });
174        }
175        let binding_type = BindingType::from_u8(bytes[cur]);
176        cur += BINDING_TYPE_FIELD;
177
178        // IOR — parse the remainder using Ior::parse which reads from position 0
179        // of a slice; we need to slice from cur to end.
180        let ior_slice = &bytes[cur..end];
181        let ior = Ior::parse(ior_slice)?;
182        let ior_len = ior.serialized_len();
183        cur += ior_len;
184
185        // objectInfo_length (2 bytes)
186        let (boi, _) = bytes[cur..end]
187            .split_first_chunk::<2>()
188            .ok_or(Error::BufferTooShort {
189                need: cur + BINDING_OBJ_INFO_LEN_FIELD,
190                have: end,
191                what: "Binding objectInfo_length",
192            })?;
193        let obj_info_len = u16::from_be_bytes(*boi) as usize;
194        cur += BINDING_OBJ_INFO_LEN_FIELD;
195        if cur + obj_info_len > end {
196            return Err(Error::SectionLengthOverflow {
197                declared: obj_info_len,
198                available: end - cur,
199            });
200        }
201        let object_info = &bytes[cur..cur + obj_info_len];
202        cur += obj_info_len;
203
204        Ok((
205            Binding {
206                name,
207                binding_type,
208                ior,
209                object_info,
210            },
211            cur,
212        ))
213    }
214
215    fn serialized_len(&self) -> usize {
216        let name_len: usize = self.name.iter().map(|n| n.serialized_len_8bit()).sum();
217        BINDING_NAME_COUNT_FIELD
218            + name_len
219            + BINDING_TYPE_FIELD
220            + self.ior.serialized_len()
221            + BINDING_OBJ_INFO_LEN_FIELD
222            + self.object_info.len()
223    }
224
225    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
226        let len = self.serialized_len();
227        if buf.len() < len {
228            return Err(Error::OutputBufferTooSmall {
229                need: len,
230                have: buf.len(),
231            });
232        }
233        if self.name.len() > u8::MAX as usize {
234            return Err(Error::SectionLengthOverflow {
235                declared: self.name.len(),
236                available: u8::MAX as usize,
237            });
238        }
239        buf[0] = self.name.len() as u8;
240        let mut pos = BINDING_NAME_COUNT_FIELD;
241        for nc in &self.name {
242            let written = nc.serialize_8bit(&mut buf[pos..])?;
243            pos += written;
244        }
245        buf[pos] = self.binding_type.to_u8();
246        pos += BINDING_TYPE_FIELD;
247        let written = self.ior.serialize_into(&mut buf[pos..])?;
248        pos += written;
249        if self.object_info.len() > u16::MAX as usize {
250            return Err(Error::SectionLengthOverflow {
251                declared: self.object_info.len(),
252                available: u16::MAX as usize,
253            });
254        }
255        buf[pos..pos + 2].copy_from_slice(&(self.object_info.len() as u16).to_be_bytes());
256        pos += BINDING_OBJ_INFO_LEN_FIELD;
257        buf[pos..pos + self.object_info.len()].copy_from_slice(self.object_info);
258        pos += self.object_info.len();
259        Ok(pos)
260    }
261}
262
263// ── ServiceContext ────────────────────────────────────────────────────────────
264
265/// One `serviceContext` entry in a BIOP message's `serviceContextList`.
266/// ISO/IEC 13818-6 / TR 101 202 §4.7.4.
267#[derive(Debug, Clone, PartialEq, Eq)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize))]
269pub struct ServiceContext<'a> {
270    /// CDR `context_id` (32-bit).
271    pub context_id: u32,
272    /// `context_data` bytes.
273    #[cfg_attr(feature = "serde", serde(borrow))]
274    pub data: &'a [u8],
275}
276
277// ── Helpers ───────────────────────────────────────────────────────────────────
278
279/// Parse the common BIOP message header (magic, version, byte_order, message_type,
280/// message_size, objectKey, objectKind).
281/// Returns (object_key, object_kind_bytes, message_size, end_of_header_pos).
282fn parse_biop_header(bytes: &[u8]) -> Result<(&[u8], [u8; 4], usize, usize)> {
283    let total = bytes.len();
284    let (bhdr, _) = bytes
285        .split_first_chunk::<BIOP_HEADER_LEN>()
286        .ok_or(Error::BufferTooShort {
287            need: BIOP_HEADER_LEN,
288            have: total,
289            what: "BIOP message header",
290        })?;
291    let magic = u32::from_be_bytes([bhdr[0], bhdr[1], bhdr[2], bhdr[3]]);
292    if magic != BIOP_MAGIC {
293        return Err(Error::ReservedBitsViolation {
294            field: "BIOP magic",
295            reason: "must be 0x42494F50 (\"BIOP\")",
296        });
297    }
298    if bhdr[4] != BIOP_VERSION_MAJOR || bhdr[5] != BIOP_VERSION_MINOR {
299        return Err(Error::ReservedBitsViolation {
300            field: "biop_version",
301            reason: "must be 1.0",
302        });
303    }
304    if bhdr[6] != BYTE_ORDER_BIG_ENDIAN {
305        return Err(Error::ReservedBitsViolation {
306            field: "byte_order",
307            reason: "must be 0x00 (big-endian) per DVB mandatory constraint",
308        });
309    }
310    // bhdr[7] = message_type (must be 0x00 per DVB)
311    let message_size = u32::from_be_bytes([bhdr[8], bhdr[9], bhdr[10], bhdr[11]]) as usize;
312    let end = BIOP_HEADER_LEN + message_size;
313    if total < end {
314        return Err(Error::SectionLengthOverflow {
315            declared: message_size,
316            available: total - BIOP_HEADER_LEN,
317        });
318    }
319    let mut pos = BIOP_HEADER_LEN;
320
321    // objectKey_length (1 byte) + objectKey_data
322    if pos + OBJECT_KEY_LEN_FIELD > end {
323        return Err(Error::BufferTooShort {
324            need: pos + OBJECT_KEY_LEN_FIELD,
325            have: end,
326            what: "BIOP objectKey_length",
327        });
328    }
329    let obj_key_len = bytes[pos] as usize;
330    pos += OBJECT_KEY_LEN_FIELD;
331    if pos + obj_key_len > end {
332        return Err(Error::SectionLengthOverflow {
333            declared: obj_key_len,
334            available: end - pos,
335        });
336    }
337    let object_key = &bytes[pos..pos + obj_key_len];
338    pos += obj_key_len;
339
340    // objectKind_length (4 bytes) + objectKind_data (4 bytes)
341    let (bkl, _) = bytes[pos..end]
342        .split_first_chunk::<4>()
343        .ok_or(Error::BufferTooShort {
344            need: pos + OBJECT_KIND_LEN_FIELD,
345            have: end,
346            what: "BIOP objectKind_length",
347        })?;
348    let kind_len = u32::from_be_bytes(*bkl) as usize;
349    pos += OBJECT_KIND_LEN_FIELD;
350    if kind_len != OBJECT_KIND_DATA_LEN {
351        return Err(Error::ValueOutOfRange {
352            field: "objectKind_length",
353            reason: "DVB BIOP objectKind must be exactly 4 bytes",
354        });
355    }
356    if pos + OBJECT_KIND_DATA_LEN > end {
357        return Err(Error::SectionLengthOverflow {
358            declared: OBJECT_KIND_DATA_LEN,
359            available: end - pos,
360        });
361    }
362    let mut kind_bytes = [0u8; 4];
363    kind_bytes.copy_from_slice(&bytes[pos..pos + 4]);
364    pos += OBJECT_KIND_DATA_LEN;
365
366    Ok((object_key, kind_bytes, message_size, pos))
367}
368
369/// Parse the `serviceContextList` and return the typed entries plus the
370/// position after the list.
371fn parse_service_context_list<'a>(
372    bytes: &'a [u8],
373    pos: usize,
374    end: usize,
375) -> Result<(Vec<ServiceContext<'a>>, usize)> {
376    if pos + SERVICE_CONTEXT_COUNT_FIELD > end {
377        return Err(Error::BufferTooShort {
378            need: pos + SERVICE_CONTEXT_COUNT_FIELD,
379            have: end,
380            what: "serviceContextList_count",
381        });
382    }
383    let count = bytes[pos] as usize;
384    let mut cur = pos + SERVICE_CONTEXT_COUNT_FIELD;
385    let mut list = Vec::with_capacity(count.min(16));
386    for _ in 0..count {
387        let (sch, _) = bytes[cur..end]
388            .split_first_chunk::<SERVICE_CONTEXT_FIXED>()
389            .ok_or(Error::BufferTooShort {
390                need: cur + SERVICE_CONTEXT_FIXED,
391                have: end,
392                what: "serviceContext entry",
393            })?;
394        let context_id = u32::from_be_bytes([sch[0], sch[1], sch[2], sch[3]]);
395        let ctx_data_len = u16::from_be_bytes([sch[4], sch[5]]) as usize;
396        cur += SERVICE_CONTEXT_FIXED;
397        if cur + ctx_data_len > end {
398            return Err(Error::SectionLengthOverflow {
399                declared: ctx_data_len,
400                available: end - cur,
401            });
402        }
403        let data = &bytes[cur..cur + ctx_data_len];
404        cur += ctx_data_len;
405        list.push(ServiceContext { context_id, data });
406    }
407    Ok((list, cur))
408}
409
410/// Serialized byte length of a `serviceContextList` (count byte + all entries).
411fn service_context_list_len(list: &[ServiceContext]) -> usize {
412    SERVICE_CONTEXT_COUNT_FIELD
413        + list
414            .iter()
415            .map(|e| SERVICE_CONTEXT_FIXED + e.data.len())
416            .sum::<usize>()
417}
418
419/// Write a `serviceContextList` into `buf` starting at offset 0. Returns bytes written.
420fn write_service_context_list(buf: &mut [u8], list: &[ServiceContext]) -> Result<usize> {
421    if list.len() > u8::MAX as usize {
422        return Err(Error::SectionLengthOverflow {
423            declared: list.len(),
424            available: u8::MAX as usize,
425        });
426    }
427    buf[0] = list.len() as u8;
428    let mut pos = SERVICE_CONTEXT_COUNT_FIELD;
429    for entry in list {
430        if entry.data.len() > u16::MAX as usize {
431            return Err(Error::SectionLengthOverflow {
432                declared: entry.data.len(),
433                available: u16::MAX as usize,
434            });
435        }
436        buf[pos..pos + 4].copy_from_slice(&entry.context_id.to_be_bytes());
437        buf[pos + 4..pos + 6].copy_from_slice(&(entry.data.len() as u16).to_be_bytes());
438        pos += SERVICE_CONTEXT_FIXED;
439        buf[pos..pos + entry.data.len()].copy_from_slice(entry.data);
440        pos += entry.data.len();
441    }
442    Ok(pos)
443}
444
445/// Write the 12-byte BIOP message header to `buf` at position 0.
446fn write_biop_header(buf: &mut [u8], message_size: u32) {
447    buf[0..4].copy_from_slice(&BIOP_MAGIC.to_be_bytes());
448    buf[4] = BIOP_VERSION_MAJOR;
449    buf[5] = BIOP_VERSION_MINOR;
450    buf[6] = BYTE_ORDER_BIG_ENDIAN;
451    buf[7] = 0x00; // message_type
452    buf[8..12].copy_from_slice(&message_size.to_be_bytes());
453}
454
455// ── DirectoryMessage ──────────────────────────────────────────────────────────
456
457/// BIOP::DirectoryMessage — or ServiceGatewayMessage (same wire format, kind differs).
458/// TR 101 202 §4.7.4.1/§4.7.4.4, Table 4.9.
459#[derive(Debug, Clone, PartialEq, Eq)]
460#[cfg_attr(feature = "serde", derive(serde::Serialize))]
461pub struct DirectoryMessage<'a> {
462    /// Object kind (`"dir\0"` or `"srg\0"`).
463    pub object_kind: [u8; 4],
464    /// `objectKey_data`.
465    #[cfg_attr(feature = "serde", serde(borrow))]
466    pub object_key: &'a [u8],
467    /// `objectInfo_data` (after key and kind but before serviceContextList).
468    #[cfg_attr(feature = "serde", serde(borrow))]
469    pub object_info: &'a [u8],
470    /// Parsed `serviceContextList` entries.
471    #[cfg_attr(feature = "serde", serde(borrow))]
472    pub service_context: Vec<ServiceContext<'a>>,
473    /// Binding entries.
474    pub bindings: Vec<Binding<'a>>,
475}
476
477impl<'a> DirectoryMessage<'a> {
478    /// True if this is a ServiceGateway object (`object_kind == "srg\0"`).
479    pub fn is_service_gateway(&self) -> bool {
480        &self.object_kind == b"srg\0"
481    }
482
483    fn parse_from(
484        bytes: &'a [u8],
485        object_key: &'a [u8],
486        object_kind: [u8; 4],
487        pos: usize,
488        end: usize,
489    ) -> Result<Self> {
490        let mut cur = pos;
491
492        // objectInfo_length (2 bytes) + objectInfo_data
493        let (bdoi, _) = bytes[cur..end]
494            .split_first_chunk::<2>()
495            .ok_or(Error::BufferTooShort {
496                need: cur + OBJECT_INFO_LEN_FIELD,
497                have: end,
498                what: "DirectoryMessage objectInfo_length",
499            })?;
500        let obj_info_len = u16::from_be_bytes(*bdoi) as usize;
501        cur += OBJECT_INFO_LEN_FIELD;
502        if cur + obj_info_len > end {
503            return Err(Error::SectionLengthOverflow {
504                declared: obj_info_len,
505                available: end - cur,
506            });
507        }
508        let object_info = &bytes[cur..cur + obj_info_len];
509        cur += obj_info_len;
510
511        // serviceContextList (raw)
512        let (service_context, next) = parse_service_context_list(bytes, cur, end)?;
513        cur = next;
514
515        // messageBody_length (4 bytes)
516        let (bbl, _) = bytes[cur..end]
517            .split_first_chunk::<4>()
518            .ok_or(Error::BufferTooShort {
519                need: cur + MESSAGE_BODY_LEN_FIELD,
520                have: end,
521                what: "DirectoryMessage messageBody_length",
522            })?;
523        let body_len = u32::from_be_bytes(*bbl) as usize;
524        cur += MESSAGE_BODY_LEN_FIELD;
525        let body_end = cur + body_len;
526        if body_end > end {
527            return Err(Error::SectionLengthOverflow {
528                declared: body_len,
529                available: end - cur,
530            });
531        }
532
533        // bindings_count (2 bytes)
534        let (bbc, _) =
535            bytes[cur..body_end]
536                .split_first_chunk::<2>()
537                .ok_or(Error::BufferTooShort {
538                    need: cur + BINDINGS_COUNT_FIELD,
539                    have: body_end,
540                    what: "DirectoryMessage bindings_count",
541                })?;
542        let bindings_count = u16::from_be_bytes(*bbc) as usize;
543        cur += BINDINGS_COUNT_FIELD;
544
545        let mut bindings = Vec::with_capacity(bindings_count.min(256));
546        for _ in 0..bindings_count {
547            let (binding, next) = Binding::parse_from(bytes, cur, body_end)?;
548            bindings.push(binding);
549            cur = next;
550        }
551
552        Ok(DirectoryMessage {
553            object_kind,
554            object_key,
555            object_info,
556            service_context,
557            bindings,
558        })
559    }
560
561    fn body_len(&self) -> usize {
562        let bindings_len: usize = self.bindings.iter().map(|b| b.serialized_len()).sum();
563        BINDINGS_COUNT_FIELD + bindings_len
564    }
565
566    fn serialized_len_inner(&self) -> usize {
567        // after the header: objectKey + objectKind + objectInfo + serviceContext + messageBody
568        let key_part = OBJECT_KEY_LEN_FIELD
569            + self.object_key.len()
570            + OBJECT_KIND_LEN_FIELD
571            + OBJECT_KIND_DATA_LEN;
572        let info_part = OBJECT_INFO_LEN_FIELD + self.object_info.len();
573        let svc_ctx_part = service_context_list_len(&self.service_context);
574        let body_part = MESSAGE_BODY_LEN_FIELD + self.body_len();
575        key_part + info_part + svc_ctx_part + body_part
576    }
577
578    /// Total serialized length including the 12-byte BIOP header.
579    pub fn serialized_len_total(&self) -> usize {
580        BIOP_HEADER_LEN + self.serialized_len_inner()
581    }
582
583    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
584        let inner_len = self.serialized_len_inner();
585        let total = BIOP_HEADER_LEN + inner_len;
586        if buf.len() < total {
587            return Err(Error::OutputBufferTooSmall {
588                need: total,
589                have: buf.len(),
590            });
591        }
592        if inner_len > u32::MAX as usize {
593            return Err(Error::SectionLengthOverflow {
594                declared: inner_len,
595                available: u32::MAX as usize,
596            });
597        }
598        write_biop_header(buf, inner_len as u32);
599        let mut pos = BIOP_HEADER_LEN;
600
601        // objectKey
602        if self.object_key.len() > u8::MAX as usize {
603            return Err(Error::SectionLengthOverflow {
604                declared: self.object_key.len(),
605                available: u8::MAX as usize,
606            });
607        }
608        buf[pos] = self.object_key.len() as u8;
609        pos += OBJECT_KEY_LEN_FIELD;
610        buf[pos..pos + self.object_key.len()].copy_from_slice(self.object_key);
611        pos += self.object_key.len();
612
613        // objectKind
614        buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
615        pos += OBJECT_KIND_LEN_FIELD;
616        buf[pos..pos + 4].copy_from_slice(&self.object_kind);
617        pos += OBJECT_KIND_DATA_LEN;
618
619        // objectInfo
620        if self.object_info.len() > u16::MAX as usize {
621            return Err(Error::SectionLengthOverflow {
622                declared: self.object_info.len(),
623                available: u16::MAX as usize,
624            });
625        }
626        buf[pos..pos + 2].copy_from_slice(&(self.object_info.len() as u16).to_be_bytes());
627        pos += OBJECT_INFO_LEN_FIELD;
628        buf[pos..pos + self.object_info.len()].copy_from_slice(self.object_info);
629        pos += self.object_info.len();
630
631        // serviceContextList
632        pos += write_service_context_list(&mut buf[pos..], &self.service_context)?;
633
634        // messageBody
635        let body_len = self.body_len();
636        if body_len > u32::MAX as usize {
637            return Err(Error::SectionLengthOverflow {
638                declared: body_len,
639                available: u32::MAX as usize,
640            });
641        }
642        buf[pos..pos + 4].copy_from_slice(&(body_len as u32).to_be_bytes());
643        pos += MESSAGE_BODY_LEN_FIELD;
644
645        // bindings_count
646        if self.bindings.len() > u16::MAX as usize {
647            return Err(Error::SectionLengthOverflow {
648                declared: self.bindings.len(),
649                available: u16::MAX as usize,
650            });
651        }
652        buf[pos..pos + 2].copy_from_slice(&(self.bindings.len() as u16).to_be_bytes());
653        pos += BINDINGS_COUNT_FIELD;
654
655        for binding in &self.bindings {
656            let written = binding.serialize_into_buf(&mut buf[pos..])?;
657            pos += written;
658        }
659
660        Ok(total)
661    }
662}
663
664// ── FileMessage ───────────────────────────────────────────────────────────────
665
666/// BIOP::FileMessage — TR 101 202 §4.7.4.2, Table 4.10.
667///
668/// `objectInfo_length ≥ 8`; the first 8 bytes of objectInfo are the
669/// `DSM::File::ContentSize` (64-bit big-endian).
670#[derive(Debug, Clone, PartialEq, Eq)]
671#[cfg_attr(feature = "serde", derive(serde::Serialize))]
672pub struct FileMessage<'a> {
673    /// `objectKey_data`.
674    #[cfg_attr(feature = "serde", serde(borrow))]
675    pub object_key: &'a [u8],
676    /// `DSM::File::ContentSize` from the first 8 bytes of objectInfo.
677    pub content_size: u64,
678    /// Remaining objectInfo bytes after the 8-byte ContentSize.
679    #[cfg_attr(feature = "serde", serde(borrow))]
680    pub object_info_extra: &'a [u8],
681    /// Parsed `serviceContextList` entries.
682    #[cfg_attr(feature = "serde", serde(borrow))]
683    pub service_context: Vec<ServiceContext<'a>>,
684    /// File content bytes.
685    #[cfg_attr(feature = "serde", serde(borrow))]
686    pub content: &'a [u8],
687}
688
689impl<'a> FileMessage<'a> {
690    fn parse_from(bytes: &'a [u8], object_key: &'a [u8], pos: usize, end: usize) -> Result<Self> {
691        let mut cur = pos;
692
693        // objectInfo_length (2 bytes)
694        let (bfoi, _) = bytes[cur..end]
695            .split_first_chunk::<2>()
696            .ok_or(Error::BufferTooShort {
697                need: cur + OBJECT_INFO_LEN_FIELD,
698                have: end,
699                what: "FileMessage objectInfo_length",
700            })?;
701        let obj_info_len = u16::from_be_bytes(*bfoi) as usize;
702        cur += OBJECT_INFO_LEN_FIELD;
703        if obj_info_len < FILE_CONTENT_SIZE_LEN {
704            return Err(Error::ValueOutOfRange {
705                field: "FileMessage.objectInfo_length",
706                reason: "FileMessage objectInfo must be at least 8 bytes (ContentSize)",
707            });
708        }
709        if cur + obj_info_len > end {
710            return Err(Error::SectionLengthOverflow {
711                declared: obj_info_len,
712                available: end - cur,
713            });
714        }
715        let (bcs, _) =
716            bytes[cur..end]
717                .split_first_chunk::<8>()
718                .ok_or(Error::SectionLengthOverflow {
719                    declared: obj_info_len,
720                    available: end - cur,
721                })?;
722        let content_size = u64::from_be_bytes(*bcs);
723        let object_info_extra = &bytes[cur + FILE_CONTENT_SIZE_LEN..cur + obj_info_len];
724        cur += obj_info_len;
725
726        // serviceContextList
727        let (service_context, next) = parse_service_context_list(bytes, cur, end)?;
728        cur = next;
729
730        // messageBody_length (4 bytes)
731        let (bfbl, _) = bytes[cur..end]
732            .split_first_chunk::<4>()
733            .ok_or(Error::BufferTooShort {
734                need: cur + MESSAGE_BODY_LEN_FIELD,
735                have: end,
736                what: "FileMessage messageBody_length",
737            })?;
738        let body_len = u32::from_be_bytes(*bfbl) as usize;
739        cur += MESSAGE_BODY_LEN_FIELD;
740        let body_end = cur + body_len;
741        if body_end > end {
742            return Err(Error::SectionLengthOverflow {
743                declared: body_len,
744                available: end - cur,
745            });
746        }
747
748        // content_length (4 bytes) + content_data
749        let (bfcl, _) =
750            bytes[cur..body_end]
751                .split_first_chunk::<4>()
752                .ok_or(Error::BufferTooShort {
753                    need: cur + FILE_CONTENT_LEN_FIELD,
754                    have: body_end,
755                    what: "FileMessage content_length",
756                })?;
757        let content_len = u32::from_be_bytes(*bfcl) as usize;
758        cur += FILE_CONTENT_LEN_FIELD;
759        if cur + content_len > body_end {
760            return Err(Error::SectionLengthOverflow {
761                declared: content_len,
762                available: body_end - cur,
763            });
764        }
765        let content = &bytes[cur..cur + content_len];
766
767        Ok(FileMessage {
768            object_key,
769            content_size,
770            object_info_extra,
771            service_context,
772            content,
773        })
774    }
775
776    fn serialized_len_inner(&self) -> usize {
777        let obj_info_total = FILE_CONTENT_SIZE_LEN + self.object_info_extra.len();
778        OBJECT_KEY_LEN_FIELD
779            + self.object_key.len()
780            + OBJECT_KIND_LEN_FIELD
781            + OBJECT_KIND_DATA_LEN
782            + OBJECT_INFO_LEN_FIELD
783            + obj_info_total
784            + service_context_list_len(&self.service_context)
785            + MESSAGE_BODY_LEN_FIELD
786            + FILE_CONTENT_LEN_FIELD
787            + self.content.len()
788    }
789
790    /// Total serialized length including the 12-byte BIOP header.
791    pub fn serialized_len_total(&self) -> usize {
792        BIOP_HEADER_LEN + self.serialized_len_inner()
793    }
794
795    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
796        let inner_len = self.serialized_len_inner();
797        let total = BIOP_HEADER_LEN + inner_len;
798        if buf.len() < total {
799            return Err(Error::OutputBufferTooSmall {
800                need: total,
801                have: buf.len(),
802            });
803        }
804        write_biop_header(buf, inner_len as u32);
805        let mut pos = BIOP_HEADER_LEN;
806
807        if self.object_key.len() > u8::MAX as usize {
808            return Err(Error::SectionLengthOverflow {
809                declared: self.object_key.len(),
810                available: u8::MAX as usize,
811            });
812        }
813        buf[pos] = self.object_key.len() as u8;
814        pos += OBJECT_KEY_LEN_FIELD;
815        buf[pos..pos + self.object_key.len()].copy_from_slice(self.object_key);
816        pos += self.object_key.len();
817
818        // objectKind = "fil\0"
819        buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
820        pos += OBJECT_KIND_LEN_FIELD;
821        buf[pos..pos + 4].copy_from_slice(b"fil\0");
822        pos += OBJECT_KIND_DATA_LEN;
823
824        // objectInfo: ContentSize(8) + extra
825        let obj_info_total = FILE_CONTENT_SIZE_LEN + self.object_info_extra.len();
826        if obj_info_total > u16::MAX as usize {
827            return Err(Error::SectionLengthOverflow {
828                declared: obj_info_total,
829                available: u16::MAX as usize,
830            });
831        }
832        buf[pos..pos + 2].copy_from_slice(&(obj_info_total as u16).to_be_bytes());
833        pos += OBJECT_INFO_LEN_FIELD;
834        buf[pos..pos + 8].copy_from_slice(&self.content_size.to_be_bytes());
835        pos += FILE_CONTENT_SIZE_LEN;
836        buf[pos..pos + self.object_info_extra.len()].copy_from_slice(self.object_info_extra);
837        pos += self.object_info_extra.len();
838
839        // serviceContextList
840        pos += write_service_context_list(&mut buf[pos..], &self.service_context)?;
841
842        // messageBody
843        let body_len = FILE_CONTENT_LEN_FIELD + self.content.len();
844        buf[pos..pos + 4].copy_from_slice(&(body_len as u32).to_be_bytes());
845        pos += MESSAGE_BODY_LEN_FIELD;
846        buf[pos..pos + 4].copy_from_slice(&(self.content.len() as u32).to_be_bytes());
847        pos += FILE_CONTENT_LEN_FIELD;
848        buf[pos..pos + self.content.len()].copy_from_slice(self.content);
849
850        Ok(total)
851    }
852}
853
854// ── DsmStreamInfo ─────────────────────────────────────────────────────────────
855
856/// `DSM::Stream::Info_T` — the mandatory objectInfo head shared by
857/// `StreamMessage` and `StreamEventMessage`.
858/// TR 101 202 §4.7.4.3, Table 4.11.
859#[derive(Debug, Clone, PartialEq, Eq)]
860#[cfg_attr(feature = "serde", derive(serde::Serialize))]
861pub struct DsmStreamInfo<'a> {
862    /// `aDescription_bytes` — freeform description of the stream.
863    #[cfg_attr(feature = "serde", serde(borrow))]
864    pub description: &'a [u8],
865    /// `duration.aSeconds` — AppNPT seconds (signed, `simsbf`).
866    pub duration_seconds: i32,
867    /// `duration.aMicroSeconds`.
868    pub duration_microseconds: u16,
869    /// `audio` flag byte.
870    pub audio: u8,
871    /// `video` flag byte.
872    pub video: u8,
873    /// `data` flag byte.
874    pub data: u8,
875}
876
877impl<'a> DsmStreamInfo<'a> {
878    /// Serialized byte length of this Info_T block (N2 + 10 per the spec).
879    fn serialized_len(&self) -> usize {
880        STREAM_ADESC_LEN_FIELD + self.description.len() + STREAM_INFO_FIXED
881    }
882
883    /// Parse an Info_T block from `bytes[pos..end]`, return `(Self, next_pos)`.
884    fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
885        // aDescription_length (1 byte)
886        if pos + STREAM_ADESC_LEN_FIELD > end {
887            return Err(Error::BufferTooShort {
888                need: pos + STREAM_ADESC_LEN_FIELD,
889                have: end,
890                what: "DsmStreamInfo aDescription_length",
891            });
892        }
893        let desc_len = bytes[pos] as usize;
894        let mut cur = pos + STREAM_ADESC_LEN_FIELD;
895
896        // aDescription_bytes
897        if cur + desc_len > end {
898            return Err(Error::SectionLengthOverflow {
899                declared: desc_len,
900                available: end - cur,
901            });
902        }
903        let description = &bytes[cur..cur + desc_len];
904        cur += desc_len;
905
906        // duration.aSeconds(4 signed) + aMicroSeconds(2) + audio(1) + video(1) + data(1)
907        let (sif, _) = bytes[cur..end]
908            .split_first_chunk::<STREAM_INFO_FIXED>()
909            .ok_or(Error::BufferTooShort {
910                need: cur + STREAM_INFO_FIXED,
911                have: end,
912                what: "DsmStreamInfo fixed fields",
913            })?;
914        let duration_seconds = i32::from_be_bytes([sif[0], sif[1], sif[2], sif[3]]);
915        let duration_microseconds = u16::from_be_bytes([sif[4], sif[5]]);
916        let audio = sif[6];
917        let video = sif[7];
918        let data = sif[8];
919        cur += STREAM_INFO_FIXED;
920
921        Ok((
922            DsmStreamInfo {
923                description,
924                duration_seconds,
925                duration_microseconds,
926                audio,
927                video,
928                data,
929            },
930            cur,
931        ))
932    }
933
934    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
935        let len = self.serialized_len();
936        if buf.len() < len {
937            return Err(Error::OutputBufferTooSmall {
938                need: len,
939                have: buf.len(),
940            });
941        }
942        if self.description.len() > u8::MAX as usize {
943            return Err(Error::SectionLengthOverflow {
944                declared: self.description.len(),
945                available: u8::MAX as usize,
946            });
947        }
948        buf[0] = self.description.len() as u8;
949        let mut pos = STREAM_ADESC_LEN_FIELD;
950        buf[pos..pos + self.description.len()].copy_from_slice(self.description);
951        pos += self.description.len();
952        buf[pos..pos + 4].copy_from_slice(&self.duration_seconds.to_be_bytes());
953        pos += 4;
954        buf[pos..pos + 2].copy_from_slice(&self.duration_microseconds.to_be_bytes());
955        pos += 2;
956        buf[pos] = self.audio;
957        pos += 1;
958        buf[pos] = self.video;
959        pos += 1;
960        buf[pos] = self.data;
961        pos += 1;
962        Ok(pos)
963    }
964}
965
966// ── StreamMessage ─────────────────────────────────────────────────────────────
967
968/// BIOP::StreamMessage — TR 101 202 §4.7.4.3, Table 4.11.
969/// `objectKind = "str\0"`.
970#[derive(Debug, Clone, PartialEq, Eq)]
971#[cfg_attr(feature = "serde", derive(serde::Serialize))]
972pub struct StreamMessage<'a> {
973    /// `objectKey_data`.
974    #[cfg_attr(feature = "serde", serde(borrow))]
975    pub object_key: &'a [u8],
976    /// `DSM::Stream::Info_T` parsed from the head of objectInfo.
977    pub stream_info: DsmStreamInfo<'a>,
978    /// Trailing objectInfo bytes after Info_T (may be empty).
979    #[cfg_attr(feature = "serde", serde(borrow))]
980    pub object_info_extra: &'a [u8],
981    /// Parsed `serviceContextList` entries.
982    #[cfg_attr(feature = "serde", serde(borrow))]
983    pub service_context: Vec<ServiceContext<'a>>,
984    /// Tap entries from the message body.
985    pub taps: Vec<super::ior::Tap<'a>>,
986}
987
988impl<'a> StreamMessage<'a> {
989    fn parse_from(bytes: &'a [u8], object_key: &'a [u8], pos: usize, end: usize) -> Result<Self> {
990        let mut cur = pos;
991
992        // objectInfo_length (2 bytes)
993        let (bsmoi, _) = bytes[cur..end]
994            .split_first_chunk::<2>()
995            .ok_or(Error::BufferTooShort {
996                need: cur + OBJECT_INFO_LEN_FIELD,
997                have: end,
998                what: "StreamMessage objectInfo_length",
999            })?;
1000        let obj_info_len = u16::from_be_bytes(*bsmoi) as usize;
1001        cur += OBJECT_INFO_LEN_FIELD;
1002        if cur + obj_info_len > end {
1003            return Err(Error::SectionLengthOverflow {
1004                declared: obj_info_len,
1005                available: end - cur,
1006            });
1007        }
1008        let obj_info_start = cur;
1009        let obj_info_end = cur + obj_info_len;
1010
1011        // DSM::Stream::Info_T
1012        let (stream_info, _) = DsmStreamInfo::parse_from(bytes, cur, obj_info_end)?;
1013        let info_len = stream_info.serialized_len();
1014        if obj_info_len < info_len {
1015            return Err(Error::ValueOutOfRange {
1016                field: "StreamMessage.objectInfo_length",
1017                reason: "objectInfo too short for DSM::Stream::Info_T",
1018            });
1019        }
1020        let object_info_extra = &bytes[obj_info_start + info_len..obj_info_end];
1021        cur = obj_info_end;
1022
1023        // serviceContextList
1024        let (service_context, next) = parse_service_context_list(bytes, cur, end)?;
1025        cur = next;
1026
1027        // messageBody_length (4 bytes)
1028        let (bsmbl, _) = bytes[cur..end]
1029            .split_first_chunk::<4>()
1030            .ok_or(Error::BufferTooShort {
1031                need: cur + MESSAGE_BODY_LEN_FIELD,
1032                have: end,
1033                what: "StreamMessage messageBody_length",
1034            })?;
1035        let body_len = u32::from_be_bytes(*bsmbl) as usize;
1036        cur += MESSAGE_BODY_LEN_FIELD;
1037        let body_end = cur + body_len;
1038        if body_end > end {
1039            return Err(Error::SectionLengthOverflow {
1040                declared: body_len,
1041                available: end - cur,
1042            });
1043        }
1044
1045        // taps_count (1 byte)
1046        if cur + STREAM_TAPS_COUNT_FIELD > body_end {
1047            return Err(Error::BufferTooShort {
1048                need: cur + STREAM_TAPS_COUNT_FIELD,
1049                have: body_end,
1050                what: "StreamMessage taps_count",
1051            });
1052        }
1053        let taps_count = bytes[cur] as usize;
1054        cur += STREAM_TAPS_COUNT_FIELD;
1055
1056        let mut taps = Vec::with_capacity(taps_count.min(16));
1057        for _ in 0..taps_count {
1058            let (tap, next) = super::ior::Tap::parse_from(bytes, cur, body_end)?;
1059            taps.push(tap);
1060            cur = next;
1061        }
1062
1063        Ok(StreamMessage {
1064            object_key,
1065            stream_info,
1066            object_info_extra,
1067            service_context,
1068            taps,
1069        })
1070    }
1071
1072    fn body_len(&self) -> usize {
1073        let taps_len: usize = self.taps.iter().map(|t| t.serialized_len()).sum();
1074        STREAM_TAPS_COUNT_FIELD + taps_len
1075    }
1076
1077    fn obj_info_len(&self) -> usize {
1078        self.stream_info.serialized_len() + self.object_info_extra.len()
1079    }
1080
1081    fn serialized_len_inner(&self) -> usize {
1082        OBJECT_KEY_LEN_FIELD
1083            + self.object_key.len()
1084            + OBJECT_KIND_LEN_FIELD
1085            + OBJECT_KIND_DATA_LEN
1086            + OBJECT_INFO_LEN_FIELD
1087            + self.obj_info_len()
1088            + service_context_list_len(&self.service_context)
1089            + MESSAGE_BODY_LEN_FIELD
1090            + self.body_len()
1091    }
1092
1093    /// Total serialized length including the 12-byte BIOP header.
1094    pub fn serialized_len_total(&self) -> usize {
1095        BIOP_HEADER_LEN + self.serialized_len_inner()
1096    }
1097
1098    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
1099        let inner_len = self.serialized_len_inner();
1100        let total = BIOP_HEADER_LEN + inner_len;
1101        if buf.len() < total {
1102            return Err(Error::OutputBufferTooSmall {
1103                need: total,
1104                have: buf.len(),
1105            });
1106        }
1107        write_biop_header(buf, inner_len as u32);
1108        let mut pos = BIOP_HEADER_LEN;
1109
1110        // objectKey
1111        if self.object_key.len() > u8::MAX as usize {
1112            return Err(Error::SectionLengthOverflow {
1113                declared: self.object_key.len(),
1114                available: u8::MAX as usize,
1115            });
1116        }
1117        buf[pos] = self.object_key.len() as u8;
1118        pos += OBJECT_KEY_LEN_FIELD;
1119        buf[pos..pos + self.object_key.len()].copy_from_slice(self.object_key);
1120        pos += self.object_key.len();
1121
1122        // objectKind = "str\0"
1123        buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
1124        pos += OBJECT_KIND_LEN_FIELD;
1125        buf[pos..pos + 4].copy_from_slice(b"str\0");
1126        pos += OBJECT_KIND_DATA_LEN;
1127
1128        // objectInfo_length
1129        let oi_len = self.obj_info_len();
1130        if oi_len > u16::MAX as usize {
1131            return Err(Error::SectionLengthOverflow {
1132                declared: oi_len,
1133                available: u16::MAX as usize,
1134            });
1135        }
1136        buf[pos..pos + 2].copy_from_slice(&(oi_len as u16).to_be_bytes());
1137        pos += OBJECT_INFO_LEN_FIELD;
1138
1139        // Info_T
1140        let written = self.stream_info.serialize_into_buf(&mut buf[pos..])?;
1141        pos += written;
1142
1143        // object_info_extra
1144        buf[pos..pos + self.object_info_extra.len()].copy_from_slice(self.object_info_extra);
1145        pos += self.object_info_extra.len();
1146
1147        // serviceContextList
1148        pos += write_service_context_list(&mut buf[pos..], &self.service_context)?;
1149
1150        // messageBody_length
1151        let bl = self.body_len();
1152        buf[pos..pos + 4].copy_from_slice(&(bl as u32).to_be_bytes());
1153        pos += MESSAGE_BODY_LEN_FIELD;
1154
1155        // taps_count
1156        if self.taps.len() > u8::MAX as usize {
1157            return Err(Error::SectionLengthOverflow {
1158                declared: self.taps.len(),
1159                available: u8::MAX as usize,
1160            });
1161        }
1162        buf[pos] = self.taps.len() as u8;
1163        pos += STREAM_TAPS_COUNT_FIELD;
1164        for tap in &self.taps {
1165            let written = tap.serialize_into_buf(&mut buf[pos..])?;
1166            pos += written;
1167        }
1168
1169        Ok(total)
1170    }
1171}
1172
1173// ── StreamEventMessage ────────────────────────────────────────────────────────
1174
1175/// BIOP::StreamEventMessage — TR 101 202 §4.7.4.5, Table 4.13.
1176/// `objectKind = "ste\0"`.
1177#[derive(Debug, Clone, PartialEq, Eq)]
1178#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1179pub struct StreamEventMessage<'a> {
1180    /// `objectKey_data`.
1181    #[cfg_attr(feature = "serde", serde(borrow))]
1182    pub object_key: &'a [u8],
1183    /// `DSM::Stream::Info_T` parsed from the head of objectInfo.
1184    pub stream_info: DsmStreamInfo<'a>,
1185    /// Event names from `DSM::Event::EventList_T` (each = raw `eventName_data` bytes,
1186    /// without the length prefix).
1187    pub event_names: Vec<&'a [u8]>,
1188    /// Trailing objectInfo bytes after Info_T and EventList_T (may be empty).
1189    #[cfg_attr(feature = "serde", serde(borrow))]
1190    pub object_info_extra: &'a [u8],
1191    /// Parsed `serviceContextList` entries.
1192    #[cfg_attr(feature = "serde", serde(borrow))]
1193    pub service_context: Vec<ServiceContext<'a>>,
1194    /// Tap entries from the message body.
1195    pub taps: Vec<super::ior::Tap<'a>>,
1196    /// `eventId` values — one per `event_names` entry.
1197    pub event_ids: Vec<u16>,
1198}
1199
1200impl<'a> StreamEventMessage<'a> {
1201    fn parse_from(bytes: &'a [u8], object_key: &'a [u8], pos: usize, end: usize) -> Result<Self> {
1202        let mut cur = pos;
1203
1204        // objectInfo_length (2 bytes)
1205        let (bseoi, _) = bytes[cur..end]
1206            .split_first_chunk::<2>()
1207            .ok_or(Error::BufferTooShort {
1208                need: cur + OBJECT_INFO_LEN_FIELD,
1209                have: end,
1210                what: "StreamEventMessage objectInfo_length",
1211            })?;
1212        let obj_info_len = u16::from_be_bytes(*bseoi) as usize;
1213        cur += OBJECT_INFO_LEN_FIELD;
1214        if cur + obj_info_len > end {
1215            return Err(Error::SectionLengthOverflow {
1216                declared: obj_info_len,
1217                available: end - cur,
1218            });
1219        }
1220        let obj_info_end = cur + obj_info_len;
1221
1222        // DSM::Stream::Info_T
1223        let (stream_info, next_cur) = DsmStreamInfo::parse_from(bytes, cur, obj_info_end)?;
1224        cur = next_cur;
1225
1226        // DSM::Event::EventList_T: eventNames_count (2 bytes)
1227        let (benc, _) =
1228            bytes[cur..obj_info_end]
1229                .split_first_chunk::<2>()
1230                .ok_or(Error::BufferTooShort {
1231                    need: cur + STREAM_EVENT_NAMES_COUNT_FIELD,
1232                    have: obj_info_end,
1233                    what: "StreamEventMessage eventNames_count",
1234                })?;
1235        let event_names_count = u16::from_be_bytes(*benc) as usize;
1236        cur += STREAM_EVENT_NAMES_COUNT_FIELD;
1237
1238        let mut event_names = Vec::with_capacity(event_names_count.min(64));
1239        for _ in 0..event_names_count {
1240            if cur + STREAM_EVENT_NAME_LEN_FIELD > obj_info_end {
1241                return Err(Error::BufferTooShort {
1242                    need: cur + STREAM_EVENT_NAME_LEN_FIELD,
1243                    have: obj_info_end,
1244                    what: "StreamEventMessage eventName_length",
1245                });
1246            }
1247            let name_len = bytes[cur] as usize;
1248            cur += STREAM_EVENT_NAME_LEN_FIELD;
1249            if cur + name_len > obj_info_end {
1250                return Err(Error::SectionLengthOverflow {
1251                    declared: name_len,
1252                    available: obj_info_end - cur,
1253                });
1254            }
1255            event_names.push(&bytes[cur..cur + name_len]);
1256            cur += name_len;
1257        }
1258
1259        // trailing objectInfo extra
1260        let object_info_extra = &bytes[cur..obj_info_end];
1261        cur = obj_info_end;
1262
1263        // serviceContextList
1264        let (service_context, next) = parse_service_context_list(bytes, cur, end)?;
1265        cur = next;
1266
1267        // messageBody_length (4 bytes)
1268        let (bsebl, _) = bytes[cur..end]
1269            .split_first_chunk::<4>()
1270            .ok_or(Error::BufferTooShort {
1271                need: cur + MESSAGE_BODY_LEN_FIELD,
1272                have: end,
1273                what: "StreamEventMessage messageBody_length",
1274            })?;
1275        let body_len = u32::from_be_bytes(*bsebl) as usize;
1276        cur += MESSAGE_BODY_LEN_FIELD;
1277        let body_end = cur + body_len;
1278        if body_end > end {
1279            return Err(Error::SectionLengthOverflow {
1280                declared: body_len,
1281                available: end - cur,
1282            });
1283        }
1284
1285        // taps_count (1 byte)
1286        if cur + STREAM_TAPS_COUNT_FIELD > body_end {
1287            return Err(Error::BufferTooShort {
1288                need: cur + STREAM_TAPS_COUNT_FIELD,
1289                have: body_end,
1290                what: "StreamEventMessage taps_count",
1291            });
1292        }
1293        let taps_count = bytes[cur] as usize;
1294        cur += STREAM_TAPS_COUNT_FIELD;
1295
1296        let mut taps = Vec::with_capacity(taps_count.min(16));
1297        for _ in 0..taps_count {
1298            let (tap, next) = super::ior::Tap::parse_from(bytes, cur, body_end)?;
1299            taps.push(tap);
1300            cur = next;
1301        }
1302
1303        // eventIds_count (1 byte) — must equal eventNames_count
1304        if cur + STREAM_EVENT_IDS_COUNT_FIELD > body_end {
1305            return Err(Error::BufferTooShort {
1306                need: cur + STREAM_EVENT_IDS_COUNT_FIELD,
1307                have: body_end,
1308                what: "StreamEventMessage eventIds_count",
1309            });
1310        }
1311        let event_ids_count = bytes[cur] as usize;
1312        cur += STREAM_EVENT_IDS_COUNT_FIELD;
1313        if event_ids_count != event_names_count {
1314            return Err(Error::ValueOutOfRange {
1315                field: "StreamEventMessage.eventIds_count",
1316                reason: "eventIds_count must equal eventNames_count",
1317            });
1318        }
1319
1320        let mut event_ids = Vec::with_capacity(event_ids_count.min(64));
1321        for _ in 0..event_ids_count {
1322            let (bei, _) =
1323                bytes[cur..body_end]
1324                    .split_first_chunk::<2>()
1325                    .ok_or(Error::BufferTooShort {
1326                        need: cur + STREAM_EVENT_ID_LEN,
1327                        have: body_end,
1328                        what: "StreamEventMessage eventId",
1329                    })?;
1330            event_ids.push(u16::from_be_bytes(*bei));
1331            cur += STREAM_EVENT_ID_LEN;
1332        }
1333
1334        let _ = cur; // consumed
1335        Ok(StreamEventMessage {
1336            object_key,
1337            stream_info,
1338            event_names,
1339            object_info_extra,
1340            service_context,
1341            taps,
1342            event_ids,
1343        })
1344    }
1345
1346    /// Byte count of the EventList_T block as written on the wire.
1347    fn event_list_wire_len(&self) -> usize {
1348        let names_len: usize = self
1349            .event_names
1350            .iter()
1351            .map(|n| STREAM_EVENT_NAME_LEN_FIELD + n.len())
1352            .sum();
1353        STREAM_EVENT_NAMES_COUNT_FIELD + names_len
1354    }
1355
1356    fn body_len(&self) -> usize {
1357        let taps_len: usize = self.taps.iter().map(|t| t.serialized_len()).sum();
1358        STREAM_TAPS_COUNT_FIELD
1359            + taps_len
1360            + STREAM_EVENT_IDS_COUNT_FIELD
1361            + self.event_ids.len() * STREAM_EVENT_ID_LEN
1362    }
1363
1364    fn obj_info_len(&self) -> usize {
1365        self.stream_info.serialized_len()
1366            + self.event_list_wire_len()
1367            + self.object_info_extra.len()
1368    }
1369
1370    fn serialized_len_inner(&self) -> usize {
1371        OBJECT_KEY_LEN_FIELD
1372            + self.object_key.len()
1373            + OBJECT_KIND_LEN_FIELD
1374            + OBJECT_KIND_DATA_LEN
1375            + OBJECT_INFO_LEN_FIELD
1376            + self.obj_info_len()
1377            + service_context_list_len(&self.service_context)
1378            + MESSAGE_BODY_LEN_FIELD
1379            + self.body_len()
1380    }
1381
1382    /// Total serialized length including the 12-byte BIOP header.
1383    pub fn serialized_len_total(&self) -> usize {
1384        BIOP_HEADER_LEN + self.serialized_len_inner()
1385    }
1386
1387    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
1388        let inner_len = self.serialized_len_inner();
1389        let total = BIOP_HEADER_LEN + inner_len;
1390        if buf.len() < total {
1391            return Err(Error::OutputBufferTooSmall {
1392                need: total,
1393                have: buf.len(),
1394            });
1395        }
1396        write_biop_header(buf, inner_len as u32);
1397        let mut pos = BIOP_HEADER_LEN;
1398
1399        // objectKey
1400        if self.object_key.len() > u8::MAX as usize {
1401            return Err(Error::SectionLengthOverflow {
1402                declared: self.object_key.len(),
1403                available: u8::MAX as usize,
1404            });
1405        }
1406        buf[pos] = self.object_key.len() as u8;
1407        pos += OBJECT_KEY_LEN_FIELD;
1408        buf[pos..pos + self.object_key.len()].copy_from_slice(self.object_key);
1409        pos += self.object_key.len();
1410
1411        // objectKind = "ste\0"
1412        buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
1413        pos += OBJECT_KIND_LEN_FIELD;
1414        buf[pos..pos + 4].copy_from_slice(b"ste\0");
1415        pos += OBJECT_KIND_DATA_LEN;
1416
1417        // objectInfo_length
1418        let oi_len = self.obj_info_len();
1419        if oi_len > u16::MAX as usize {
1420            return Err(Error::SectionLengthOverflow {
1421                declared: oi_len,
1422                available: u16::MAX as usize,
1423            });
1424        }
1425        buf[pos..pos + 2].copy_from_slice(&(oi_len as u16).to_be_bytes());
1426        pos += OBJECT_INFO_LEN_FIELD;
1427
1428        // Info_T
1429        let written = self.stream_info.serialize_into_buf(&mut buf[pos..])?;
1430        pos += written;
1431
1432        // EventList_T: eventNames_count (2 bytes)
1433        if self.event_names.len() > u16::MAX as usize {
1434            return Err(Error::SectionLengthOverflow {
1435                declared: self.event_names.len(),
1436                available: u16::MAX as usize,
1437            });
1438        }
1439        buf[pos..pos + 2].copy_from_slice(&(self.event_names.len() as u16).to_be_bytes());
1440        pos += STREAM_EVENT_NAMES_COUNT_FIELD;
1441        for name in &self.event_names {
1442            if name.len() > u8::MAX as usize {
1443                return Err(Error::SectionLengthOverflow {
1444                    declared: name.len(),
1445                    available: u8::MAX as usize,
1446                });
1447            }
1448            buf[pos] = name.len() as u8;
1449            pos += STREAM_EVENT_NAME_LEN_FIELD;
1450            buf[pos..pos + name.len()].copy_from_slice(name);
1451            pos += name.len();
1452        }
1453
1454        // object_info_extra
1455        buf[pos..pos + self.object_info_extra.len()].copy_from_slice(self.object_info_extra);
1456        pos += self.object_info_extra.len();
1457
1458        // serviceContextList
1459        pos += write_service_context_list(&mut buf[pos..], &self.service_context)?;
1460
1461        // messageBody_length
1462        let bl = self.body_len();
1463        buf[pos..pos + 4].copy_from_slice(&(bl as u32).to_be_bytes());
1464        pos += MESSAGE_BODY_LEN_FIELD;
1465
1466        // taps_count
1467        if self.taps.len() > u8::MAX as usize {
1468            return Err(Error::SectionLengthOverflow {
1469                declared: self.taps.len(),
1470                available: u8::MAX as usize,
1471            });
1472        }
1473        buf[pos] = self.taps.len() as u8;
1474        pos += STREAM_TAPS_COUNT_FIELD;
1475        for tap in &self.taps {
1476            let written = tap.serialize_into_buf(&mut buf[pos..])?;
1477            pos += written;
1478        }
1479
1480        // eventIds_count (1 byte) + eventIds
1481        if self.event_ids.len() > u8::MAX as usize {
1482            return Err(Error::SectionLengthOverflow {
1483                declared: self.event_ids.len(),
1484                available: u8::MAX as usize,
1485            });
1486        }
1487        buf[pos] = self.event_ids.len() as u8;
1488        pos += STREAM_EVENT_IDS_COUNT_FIELD;
1489        for &id in &self.event_ids {
1490            buf[pos..pos + 2].copy_from_slice(&id.to_be_bytes());
1491            pos += STREAM_EVENT_ID_LEN;
1492        }
1493
1494        Ok(total)
1495    }
1496}
1497
1498// ── BiopMessage ───────────────────────────────────────────────────────────────
1499
1500/// A parsed BIOP message — discriminated by `objectKind`.
1501/// TR 101 202 §4.7.4.
1502#[derive(Debug, Clone, PartialEq, Eq)]
1503#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1504#[non_exhaustive]
1505pub enum BiopMessage<'a> {
1506    /// `"dir\0"` — DSM::DirectoryMessage.
1507    Directory(DirectoryMessage<'a>),
1508    /// `"fil\0"` — DSM::FileMessage.
1509    File(FileMessage<'a>),
1510    /// `"srg\0"` — DSM::ServiceGatewayMessage (same wire format as Directory).
1511    ServiceGateway(DirectoryMessage<'a>),
1512    /// `"str\0"` — DSM::StreamMessage.
1513    Stream(StreamMessage<'a>),
1514    /// `"ste\0"` — BIOP::StreamEventMessage.
1515    StreamEvent(StreamEventMessage<'a>),
1516}
1517
1518impl<'a> BiopMessage<'a> {
1519    /// Parse one BIOP message from `bytes` starting at offset 0.
1520    ///
1521    /// Returns `(message, consumed)` where `consumed` is exactly
1522    /// `12 + message_size` (the number of bytes consumed from `bytes`).
1523    pub fn parse_at(bytes: &'a [u8]) -> Result<(Self, usize)> {
1524        let (object_key, kind_bytes, message_size, pos) = parse_biop_header(bytes)?;
1525        let consumed = BIOP_HEADER_LEN + message_size;
1526        let end = consumed;
1527
1528        let msg = match &kind_bytes {
1529            b"dir\0" => {
1530                let dm = DirectoryMessage::parse_from(bytes, object_key, kind_bytes, pos, end)?;
1531                BiopMessage::Directory(dm)
1532            }
1533            b"srg\0" => {
1534                let dm = DirectoryMessage::parse_from(bytes, object_key, kind_bytes, pos, end)?;
1535                BiopMessage::ServiceGateway(dm)
1536            }
1537            b"fil\0" => {
1538                let fm = FileMessage::parse_from(bytes, object_key, pos, end)?;
1539                BiopMessage::File(fm)
1540            }
1541            b"str\0" => {
1542                let sm = StreamMessage::parse_from(bytes, object_key, pos, end)?;
1543                BiopMessage::Stream(sm)
1544            }
1545            b"ste\0" => {
1546                let se = StreamEventMessage::parse_from(bytes, object_key, pos, end)?;
1547                BiopMessage::StreamEvent(se)
1548            }
1549            _ => {
1550                return Err(Error::ValueOutOfRange {
1551                    field: "BiopMessage.objectKind",
1552                    reason: "unknown BIOP objectKind",
1553                });
1554            }
1555        };
1556
1557        Ok((msg, consumed))
1558    }
1559
1560    fn serialized_len_total(&self) -> usize {
1561        match self {
1562            Self::Directory(d) | Self::ServiceGateway(d) => d.serialized_len_total(),
1563            Self::File(f) => f.serialized_len_total(),
1564            Self::Stream(s) => s.serialized_len_total(),
1565            Self::StreamEvent(se) => se.serialized_len_total(),
1566        }
1567    }
1568}
1569
1570impl Serialize for BiopMessage<'_> {
1571    type Error = crate::error::Error;
1572
1573    fn serialized_len(&self) -> usize {
1574        self.serialized_len_total()
1575    }
1576
1577    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1578        let len = self.serialized_len_total();
1579        if buf.len() < len {
1580            return Err(Error::OutputBufferTooSmall {
1581                need: len,
1582                have: buf.len(),
1583            });
1584        }
1585        match self {
1586            Self::Directory(d) | Self::ServiceGateway(d) => {
1587                d.serialize_into_buf(buf)?;
1588            }
1589            Self::File(f) => {
1590                f.serialize_into_buf(buf)?;
1591            }
1592            Self::Stream(s) => {
1593                s.serialize_into_buf(buf)?;
1594            }
1595            Self::StreamEvent(se) => {
1596                se.serialize_into_buf(buf)?;
1597            }
1598        }
1599        Ok(len)
1600    }
1601}
1602
1603// ── ModuleInfo ────────────────────────────────────────────────────────────────
1604
1605/// BIOP::ModuleInfo — carried in the DII `moduleInfoBytes`.
1606/// TR 101 202 §4.7.5.1, Table 4.14.
1607#[derive(Debug, Clone, PartialEq, Eq)]
1608#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1609pub struct ModuleInfo<'a> {
1610    /// `ModuleTimeOut` — µs to time out acquisition of all blocks.
1611    pub module_timeout: u32,
1612    /// `BlockTimeOut` — µs to time out the next block.
1613    pub block_timeout: u32,
1614    /// `MinBlockTime` — min µs between two blocks.
1615    pub min_block_time: u32,
1616    /// BIOP::Tap entries (≥1 BIOP_OBJECT_USE tap).
1617    #[cfg_attr(feature = "serde", serde(borrow))]
1618    pub taps: Vec<super::ior::Tap<'a>>,
1619    /// `userInfo` descriptor loop bytes.
1620    #[cfg_attr(feature = "serde", serde(borrow))]
1621    pub user_info: &'a [u8],
1622}
1623
1624impl<'a> ModuleInfo<'a> {
1625    /// Iterate over descriptors in the `userInfo` loop.
1626    ///
1627    /// Each item is `(tag: u8, data: &[u8])`.
1628    pub fn descriptors(&self) -> impl Iterator<Item = (u8, &[u8])> {
1629        DescriptorIter {
1630            data: self.user_info,
1631            pos: 0,
1632        }
1633    }
1634
1635    /// Return the `compressed_module_descriptor` (tag 0x09) from the userInfo
1636    /// loop, if present.
1637    pub fn compressed_module_descriptor(&self) -> Option<CompressedModuleDescriptor<'_>> {
1638        for (tag, data) in self.descriptors() {
1639            if tag == COMPRESSED_MODULE_DESCRIPTOR_TAG {
1640                return Some(CompressedModuleDescriptor { body: data });
1641            }
1642        }
1643        None
1644    }
1645}
1646
1647struct DescriptorIter<'a> {
1648    data: &'a [u8],
1649    pos: usize,
1650}
1651
1652impl<'a> Iterator for DescriptorIter<'a> {
1653    type Item = (u8, &'a [u8]);
1654    fn next(&mut self) -> Option<Self::Item> {
1655        let end = self.data.len();
1656        if self.pos + 2 > end {
1657            return None;
1658        }
1659        let tag = self.data[self.pos];
1660        let len = self.data[self.pos + 1] as usize;
1661        self.pos += 2;
1662        if self.pos + len > end {
1663            return None;
1664        }
1665        let d = &self.data[self.pos..self.pos + len];
1666        self.pos += len;
1667        Some((tag, d))
1668    }
1669}
1670
1671impl<'a> Parse<'a> for ModuleInfo<'a> {
1672    type Error = crate::error::Error;
1673
1674    fn parse(bytes: &'a [u8]) -> Result<Self> {
1675        let end = bytes.len();
1676        let mi_fixed_len = MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD;
1677        let (mi_hdr, _) = bytes
1678            .split_first_chunk::<13>()
1679            .ok_or(Error::BufferTooShort {
1680                need: mi_fixed_len,
1681                have: end,
1682                what: "ModuleInfo fixed fields",
1683            })?;
1684        let module_timeout = u32::from_be_bytes([mi_hdr[0], mi_hdr[1], mi_hdr[2], mi_hdr[3]]);
1685        let block_timeout = u32::from_be_bytes([mi_hdr[4], mi_hdr[5], mi_hdr[6], mi_hdr[7]]);
1686        let min_block_time = u32::from_be_bytes([mi_hdr[8], mi_hdr[9], mi_hdr[10], mi_hdr[11]]);
1687        let taps_count = mi_hdr[12] as usize;
1688        let mut pos = MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD;
1689
1690        let mut taps = Vec::with_capacity(taps_count.min(8));
1691        for _ in 0..taps_count {
1692            let (tap, next) = super::ior::Tap::parse_from(bytes, pos, end)?;
1693            taps.push(tap);
1694            pos = next;
1695        }
1696
1697        if pos + MODULE_USER_INFO_LEN_FIELD > end {
1698            return Err(Error::BufferTooShort {
1699                need: pos + MODULE_USER_INFO_LEN_FIELD,
1700                have: end,
1701                what: "ModuleInfo UserInfoLength",
1702            });
1703        }
1704        let user_info_len = bytes[pos] as usize;
1705        pos += MODULE_USER_INFO_LEN_FIELD;
1706        if pos + user_info_len > end {
1707            return Err(Error::SectionLengthOverflow {
1708                declared: user_info_len,
1709                available: end - pos,
1710            });
1711        }
1712        let user_info = &bytes[pos..pos + user_info_len];
1713
1714        Ok(ModuleInfo {
1715            module_timeout,
1716            block_timeout,
1717            min_block_time,
1718            taps,
1719            user_info,
1720        })
1721    }
1722}
1723
1724impl Serialize for ModuleInfo<'_> {
1725    type Error = crate::error::Error;
1726
1727    fn serialized_len(&self) -> usize {
1728        let taps_len: usize = self.taps.iter().map(|t| t.serialized_len()).sum();
1729        MODULE_INFO_FIXED
1730            + MODULE_TAPS_COUNT_FIELD
1731            + taps_len
1732            + MODULE_USER_INFO_LEN_FIELD
1733            + self.user_info.len()
1734    }
1735
1736    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1737        let len = self.serialized_len();
1738        if buf.len() < len {
1739            return Err(Error::OutputBufferTooSmall {
1740                need: len,
1741                have: buf.len(),
1742            });
1743        }
1744        buf[0..4].copy_from_slice(&self.module_timeout.to_be_bytes());
1745        buf[4..8].copy_from_slice(&self.block_timeout.to_be_bytes());
1746        buf[8..12].copy_from_slice(&self.min_block_time.to_be_bytes());
1747        if self.taps.len() > u8::MAX as usize {
1748            return Err(Error::SectionLengthOverflow {
1749                declared: self.taps.len(),
1750                available: u8::MAX as usize,
1751            });
1752        }
1753        buf[12] = self.taps.len() as u8;
1754        let mut pos = MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD;
1755        for tap in &self.taps {
1756            let written = tap.serialize_into_buf(&mut buf[pos..])?;
1757            pos += written;
1758        }
1759        if self.user_info.len() > u8::MAX as usize {
1760            return Err(Error::SectionLengthOverflow {
1761                declared: self.user_info.len(),
1762                available: u8::MAX as usize,
1763            });
1764        }
1765        buf[pos] = self.user_info.len() as u8;
1766        pos += MODULE_USER_INFO_LEN_FIELD;
1767        buf[pos..pos + self.user_info.len()].copy_from_slice(self.user_info);
1768        pos += self.user_info.len();
1769        Ok(pos)
1770    }
1771}
1772
1773// ── CompressedModuleDescriptor ────────────────────────────────────────────────
1774
1775/// A `compressed_module_descriptor` (tag 0x09) found in a `ModuleInfo` userInfo loop.
1776/// TR 101 202 §4.6.6.10.
1777///
1778/// The body bytes are the zlib-encoded module payload (RFC 1950 CMF+FLG header,
1779/// DEFLATE stream, Adler-32 checksum).  Decompression requires the `flate2` feature.
1780#[derive(Debug, Clone, PartialEq, Eq)]
1781#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1782pub struct CompressedModuleDescriptor<'a> {
1783    /// Raw descriptor body (the zlib stream).
1784    #[cfg_attr(feature = "serde", serde(borrow))]
1785    pub body: &'a [u8],
1786}
1787
1788/// Decompress a zlib-encoded module payload.
1789///
1790/// Uses [`flate2`](https://crates.io/crates/flate2) (optional feature `flate2`).
1791/// Returns the decompressed bytes, or an error if the zlib stream is invalid.
1792#[cfg(feature = "flate2")]
1793pub fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>> {
1794    use std::io::Read;
1795    let mut decoder = flate2::read::ZlibDecoder::new(data);
1796    let mut out = Vec::new();
1797    decoder
1798        .read_to_end(&mut out)
1799        .map_err(|e| Error::ReservedBitsViolation {
1800            field: "compressed_module_descriptor body",
1801            reason: if e.kind() == std::io::ErrorKind::InvalidData {
1802                "zlib decompression failed: invalid data"
1803            } else {
1804                "zlib decompression failed"
1805            },
1806        })?;
1807    Ok(out)
1808}
1809
1810// ── ServiceGatewayInfo ────────────────────────────────────────────────────────
1811
1812/// BIOP::ServiceGatewayInfo — the DSI `privateData` for an object carousel.
1813/// TR 101 202 §4.7.5.2, Table 4.15.
1814///
1815/// Parse with [`ServiceGatewayInfo::parse`]; serialize with [`ServiceGatewayInfo::to_bytes`].
1816/// The round-trip `to_bytes() == dsi.private_data` is a hard project invariant.
1817#[derive(Debug, Clone, PartialEq, Eq)]
1818#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1819pub struct ServiceGatewayInfo<'a> {
1820    /// IOR of the ServiceGateway object.
1821    pub ior: Ior<'a>,
1822    /// Raw `Tap() × downloadTaps_count` bytes (count byte + tap data).
1823    /// In practice `downloadTaps_count` is typically 0, making this `&[0x00]`.
1824    #[cfg_attr(feature = "serde", serde(borrow))]
1825    pub download_taps: &'a [u8],
1826    /// Parsed `serviceContextList` entries.
1827    #[cfg_attr(feature = "serde", serde(borrow))]
1828    pub service_context: Vec<ServiceContext<'a>>,
1829    /// `userInfo` descriptor loop bytes.
1830    #[cfg_attr(feature = "serde", serde(borrow))]
1831    pub user_info: &'a [u8],
1832}
1833
1834impl<'a> ServiceGatewayInfo<'a> {
1835    /// Parse the DSI `privateData` bytes as a ServiceGatewayInfo.
1836    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
1837        let end = bytes.len();
1838        let ior = Ior::parse(bytes)?;
1839        let mut pos = ior.serialized_len();
1840
1841        // downloadTaps: count(1) + taps (raw, count × variable)
1842        // We preserve the entire block raw: start at pos (count byte), walk past taps.
1843        if pos + SGI_DOWNLOAD_TAPS_COUNT_FIELD > end {
1844            return Err(Error::BufferTooShort {
1845                need: pos + SGI_DOWNLOAD_TAPS_COUNT_FIELD,
1846                have: end,
1847                what: "ServiceGatewayInfo downloadTaps_count",
1848            });
1849        }
1850        let tap_count = bytes[pos] as usize;
1851        let dl_taps_start = pos;
1852        pos += SGI_DOWNLOAD_TAPS_COUNT_FIELD;
1853        for _ in 0..tap_count {
1854            let (_, next) = super::ior::Tap::parse_from(bytes, pos, end)?;
1855            pos = next;
1856        }
1857        let download_taps = &bytes[dl_taps_start..pos];
1858
1859        // serviceContextList (raw)
1860        let (service_context, next) = parse_service_context_list(bytes, pos, end)?;
1861        pos = next;
1862
1863        // userInfoLength (2 bytes, 16-bit) + userInfo_data
1864        let (buil, _) = bytes[pos..end]
1865            .split_first_chunk::<2>()
1866            .ok_or(Error::BufferTooShort {
1867                need: pos + SGI_USER_INFO_LEN_FIELD,
1868                have: end,
1869                what: "ServiceGatewayInfo userInfoLength",
1870            })?;
1871        let ui_len = u16::from_be_bytes(*buil) as usize;
1872        pos += SGI_USER_INFO_LEN_FIELD;
1873        if pos + ui_len > end {
1874            return Err(Error::SectionLengthOverflow {
1875                declared: ui_len,
1876                available: end - pos,
1877            });
1878        }
1879        let user_info = &bytes[pos..pos + ui_len];
1880
1881        Ok(ServiceGatewayInfo {
1882            ior,
1883            download_taps,
1884            service_context,
1885            user_info,
1886        })
1887    }
1888
1889    /// Serialize to an owned byte vector.  The result MUST equal the original
1890    /// `dsi.private_data` bytes byte-for-byte.
1891    pub fn to_bytes(&self) -> Vec<u8> {
1892        let len = self.ior.serialized_len()
1893            + self.download_taps.len()
1894            + service_context_list_len(&self.service_context)
1895            + SGI_USER_INFO_LEN_FIELD
1896            + self.user_info.len();
1897        let mut buf = vec![0u8; len];
1898        let mut pos = 0;
1899        let written = self
1900            .ior
1901            .serialize_into(&mut buf[pos..])
1902            .expect("IOR serialize");
1903        pos += written;
1904        buf[pos..pos + self.download_taps.len()].copy_from_slice(self.download_taps);
1905        pos += self.download_taps.len();
1906        pos += write_service_context_list(&mut buf[pos..], &self.service_context)
1907            .expect("serviceContext fits");
1908        buf[pos..pos + 2].copy_from_slice(&(self.user_info.len() as u16).to_be_bytes());
1909        pos += SGI_USER_INFO_LEN_FIELD;
1910        buf[pos..pos + self.user_info.len()].copy_from_slice(self.user_info);
1911        buf
1912    }
1913}
1914
1915// ── Tests ─────────────────────────────────────────────────────────────────────
1916
1917#[cfg(test)]
1918mod tests {
1919    use super::*;
1920    use dvb_common::Parse;
1921
1922    /// Build a simple FileMessage around a buffer of bytes.
1923    fn sample_file_message(key: &'static [u8], content: &'static [u8]) -> BiopMessage<'static> {
1924        BiopMessage::File(FileMessage {
1925            object_key: key,
1926            content_size: content.len() as u64,
1927            object_info_extra: &[],
1928            service_context: vec![],
1929            content,
1930        })
1931    }
1932
1933    /// Build a minimal DirectoryMessage.
1934    fn sample_dir_message() -> BiopMessage<'static> {
1935        use crate::carousel::biop::ior::{
1936            BiopProfileBody, ConnBinder, ObjectLocation, TaggedProfile,
1937        };
1938        let ior = crate::carousel::biop::ior::Ior {
1939            type_id: b"fil\0",
1940            profiles: vec![TaggedProfile::Biop(BiopProfileBody {
1941                object_location: ObjectLocation {
1942                    carousel_id: 0xAB,
1943                    module_id: 2,
1944                    version_major: 1,
1945                    version_minor: 0,
1946                    object_key: &[0x02],
1947                },
1948                conn_binder: ConnBinder { taps: vec![] },
1949                extra: vec![],
1950            })],
1951        };
1952        BiopMessage::Directory(DirectoryMessage {
1953            object_kind: *b"dir\0",
1954            object_key: &[0x01],
1955            object_info: &[],
1956            service_context: vec![],
1957            bindings: vec![Binding {
1958                name: vec![NameComponent {
1959                    id: b"index.html",
1960                    kind: b"fil\0",
1961                }],
1962                binding_type: BindingType::NObject,
1963                ior,
1964                object_info: &[],
1965            }],
1966        })
1967    }
1968
1969    #[test]
1970    fn file_message_round_trip() {
1971        let content: &[u8] = b"Hello, BIOP!";
1972        let msg = sample_file_message(&[0x01], content);
1973        let mut buf = vec![0u8; msg.serialized_len()];
1974        msg.serialize_into(&mut buf).unwrap();
1975        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
1976        assert_eq!(consumed, buf.len());
1977        assert_eq!(parsed, msg);
1978        // byte-exact re-serialize
1979        let mut buf2 = vec![0u8; parsed.serialized_len()];
1980        parsed.serialize_into(&mut buf2).unwrap();
1981        assert_eq!(buf, buf2);
1982    }
1983
1984    #[test]
1985    fn directory_message_round_trip() {
1986        let msg = sample_dir_message();
1987        let mut buf = vec![0u8; msg.serialized_len()];
1988        msg.serialize_into(&mut buf).unwrap();
1989        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
1990        assert_eq!(consumed, buf.len());
1991        assert_eq!(parsed, msg);
1992        let mut buf2 = vec![0u8; parsed.serialized_len()];
1993        parsed.serialize_into(&mut buf2).unwrap();
1994        assert_eq!(buf, buf2, "Directory message byte-exact re-serialize");
1995    }
1996
1997    #[test]
1998    fn module_info_round_trip() {
1999        use crate::carousel::biop::ior::Tap;
2000        let info = ModuleInfo {
2001            module_timeout: 0x00FFFFFF,
2002            block_timeout: 0x00FFFFFF,
2003            min_block_time: 0x00000064,
2004            taps: vec![Tap {
2005                id: 0,
2006                use_: 0x0017,
2007                association_tag: 0x0042,
2008                selector: &[],
2009            }],
2010            user_info: &[],
2011        };
2012        let mut buf = vec![0u8; info.serialized_len()];
2013        info.serialize_into(&mut buf).unwrap();
2014        let parsed = ModuleInfo::parse(&buf).unwrap();
2015        assert_eq!(parsed, info);
2016        let mut buf2 = vec![0u8; parsed.serialized_len()];
2017        parsed.serialize_into(&mut buf2).unwrap();
2018        assert_eq!(buf, buf2, "ModuleInfo byte-exact re-serialize");
2019    }
2020
2021    #[test]
2022    fn module_info_byte_anchor() {
2023        use crate::carousel::biop::ior::Tap;
2024        // Hand-built ModuleInfo:
2025        //   moduleTimeout=0x000F4240, blockTimeout=0x000F4240, minBlockTime=0x00000064
2026        //   taps_count=1: id=0, use=0x0017, assoc=0x47, selector_length=0
2027        //   UserInfoLength=0
2028        #[rustfmt::skip]
2029        let expected: &[u8] = &[
2030            0x00, 0x0F, 0x42, 0x40, // moduleTimeout
2031            0x00, 0x0F, 0x42, 0x40, // blockTimeout
2032            0x00, 0x00, 0x00, 0x64, // minBlockTime
2033            0x01,                   // taps_count=1
2034            0x00, 0x00,             // id=0
2035            0x00, 0x17,             // use=0x0017
2036            0x00, 0x47,             // assoc=0x47
2037            0x00,                   // selector_length=0
2038            0x00,                   // UserInfoLength=0
2039        ];
2040        let info = ModuleInfo {
2041            module_timeout: 0x000F4240,
2042            block_timeout: 0x000F4240,
2043            min_block_time: 0x00000064,
2044            taps: vec![Tap {
2045                id: 0,
2046                use_: 0x0017,
2047                association_tag: 0x0047,
2048                selector: &[],
2049            }],
2050            user_info: &[],
2051        };
2052        let mut buf = vec![0u8; info.serialized_len()];
2053        info.serialize_into(&mut buf).unwrap();
2054        assert_eq!(buf.as_slice(), expected);
2055        let parsed = ModuleInfo::parse(expected).unwrap();
2056        assert_eq!(parsed, info);
2057    }
2058
2059    #[test]
2060    fn sgi_byte_anchor_m6() {
2061        // The 64-byte SGI private_data from the m6 broadcast capture.
2062        // Independently parsed in the py script above.
2063        #[rustfmt::skip]
2064        let raw: &[u8] = &[
2065            0x00, 0x00, 0x00, 0x04,  // type_id_length=4
2066            0x73, 0x72, 0x67, 0x00,  // type_id="srg\0"
2067            0x00, 0x00, 0x00, 0x01,  // taggedProfiles_count=1
2068            0x49, 0x53, 0x4F, 0x06,  // TAG_BIOP
2069            0x00, 0x00, 0x00, 0x28,  // profile_data_length=40
2070            0x00, 0x02,              // byte_order=0, liteComponents_count=2
2071            0x49, 0x53, 0x4F, 0x50, 0x0A, // TAG_ObjectLocation, len=10
2072            0x00, 0x00, 0x00, 0xAB,  // carouselId=0xAB
2073            0x00, 0x01,              // moduleId=1
2074            0x01, 0x00,              // version 1.0
2075            0x01, 0x01,              // objectKey_length=1, objectKey=0x01
2076            0x49, 0x53, 0x4F, 0x40, 0x12, // TAG_ConnBinder, len=18
2077            0x01,                    // taps_count=1
2078            0x00, 0x00,              // tap id=0
2079            0x00, 0x16,              // use=0x0016
2080            0x00, 0x47,              // association_tag=0x47
2081            0x0A,                    // selector_length=10
2082            0x00, 0x01, 0x80, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF,
2083            0x00,                    // downloadTaps_count=0
2084            0x00,                    // serviceContextList_count=0
2085            0x00, 0x00,              // userInfoLength=0
2086        ];
2087        assert_eq!(raw.len(), 64);
2088
2089        let sgi = ServiceGatewayInfo::parse(raw).unwrap();
2090
2091        // IOR assertions
2092        assert_eq!(sgi.ior.type_id, b"srg\0");
2093        assert_eq!(sgi.ior.profiles.len(), 1);
2094        let bp = sgi.ior.biop_profile().unwrap();
2095        assert_eq!(bp.object_location.carousel_id, 0xAB);
2096        assert_eq!(bp.object_location.module_id, 1);
2097        assert_eq!(bp.object_location.version_major, 1);
2098        assert_eq!(bp.object_location.version_minor, 0);
2099        assert_eq!(bp.object_location.object_key, &[0x01]);
2100        assert_eq!(bp.conn_binder.taps.len(), 1);
2101        let tap = &bp.conn_binder.taps[0];
2102        assert_eq!(tap.use_, 0x0016);
2103        assert_eq!(tap.association_tag, 0x47);
2104        assert_eq!(tap.transaction_id(), Some(0x80000002));
2105        assert_eq!(tap.timeout(), Some(0xFFFFFFFF));
2106
2107        // Byte-exact round-trip
2108        let out = sgi.to_bytes();
2109        assert_eq!(out.len(), 64, "SGI serialized length");
2110        assert_eq!(out.as_slice(), raw, "SGI byte-exact round-trip");
2111    }
2112
2113    #[cfg(feature = "serde")]
2114    #[test]
2115    fn biop_serde_round_trip() {
2116        let content: &[u8] = b"test content";
2117        let msg = sample_file_message(&[0x01], content);
2118        let json = serde_json::to_string(&msg).unwrap();
2119        assert!(json.contains("content_size"));
2120    }
2121
2122    #[cfg(feature = "flate2")]
2123    #[test]
2124    fn zlib_round_trip() {
2125        use flate2::{write::ZlibEncoder, Compression};
2126        use std::io::Write;
2127
2128        let original = b"Hello, compressed BIOP world! ".repeat(10);
2129        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
2130        encoder.write_all(&original).unwrap();
2131        let compressed = encoder.finish().unwrap();
2132
2133        let decompressed = decompress_zlib(&compressed).unwrap();
2134        assert_eq!(decompressed.as_slice(), original.as_slice());
2135    }
2136
2137    // ── StreamMessage tests ───────────────────────────────────────────────────
2138
2139    #[test]
2140    fn stream_message_round_trip() {
2141        use crate::carousel::biop::ior::Tap;
2142        let msg = BiopMessage::Stream(StreamMessage {
2143            object_key: &[0x01, 0x02],
2144            stream_info: DsmStreamInfo {
2145                description: b"audio stream",
2146                duration_seconds: -5,
2147                duration_microseconds: 500,
2148                audio: 1,
2149                video: 0,
2150                data: 0,
2151            },
2152            object_info_extra: b"\xDE\xAD",
2153            service_context: vec![],
2154            taps: vec![
2155                Tap {
2156                    id: 0,
2157                    use_: 0x0018,
2158                    association_tag: 0x0010,
2159                    selector: &[],
2160                },
2161                Tap {
2162                    id: 0,
2163                    use_: 0x0019,
2164                    association_tag: 0x0011,
2165                    selector: &[],
2166                },
2167            ],
2168        });
2169        let mut buf = vec![0u8; msg.serialized_len()];
2170        msg.serialize_into(&mut buf).unwrap();
2171        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
2172        assert_eq!(consumed, buf.len(), "consumed must equal total buf len");
2173        assert_eq!(parsed, msg);
2174        let mut buf2 = vec![0u8; parsed.serialized_len()];
2175        parsed.serialize_into(&mut buf2).unwrap();
2176        assert_eq!(buf, buf2, "StreamMessage byte-exact re-serialize");
2177    }
2178
2179    #[test]
2180    fn stream_event_message_round_trip() {
2181        use crate::carousel::biop::ior::Tap;
2182        let msg = BiopMessage::StreamEvent(StreamEventMessage {
2183            object_key: &[0x03],
2184            stream_info: DsmStreamInfo {
2185                description: b"event stream",
2186                duration_seconds: 3600,
2187                duration_microseconds: 0,
2188                audio: 0,
2189                video: 1,
2190                data: 0,
2191            },
2192            event_names: vec![b"play".as_ref(), b"pause".as_ref(), b"stop".as_ref()],
2193            object_info_extra: &[],
2194            service_context: vec![],
2195            taps: vec![Tap {
2196                id: 0,
2197                use_: 0x000C,
2198                association_tag: 0x0020,
2199                selector: &[],
2200            }],
2201            event_ids: vec![0x0001, 0x0002, 0x0003],
2202        });
2203        let mut buf = vec![0u8; msg.serialized_len()];
2204        msg.serialize_into(&mut buf).unwrap();
2205        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
2206        assert_eq!(consumed, buf.len(), "consumed must equal total buf len");
2207        assert_eq!(parsed, msg);
2208        let mut buf2 = vec![0u8; parsed.serialized_len()];
2209        parsed.serialize_into(&mut buf2).unwrap();
2210        assert_eq!(buf, buf2, "StreamEventMessage byte-exact re-serialize");
2211    }
2212
2213    #[test]
2214    fn stream_message_byte_anchor() {
2215        // Hand-built minimal StreamMessage from Table 4.11:
2216        //
2217        // Offset table (from byte 0):
2218        //  [0..4]   magic = 0x42494F50
2219        //  [4]      biop_version.major = 0x01
2220        //  [5]      biop_version.minor = 0x00
2221        //  [6]      byte_order = 0x00
2222        //  [7]      message_type = 0x00
2223        //  [8..12]  message_size = 38
2224        //  [12]     objectKey_length = 1
2225        //  [13]     objectKey_data = 0xAB
2226        //  [14..18] objectKind_length = 4
2227        //  [18..22] objectKind_data = "str\0"
2228        //  [22..24] objectInfo_length = 13 (= N6; Info_T = aDesc_len(1)+desc(3)+fixed(9) = 13)
2229        //    Info_T:
2230        //    [24]     aDescription_length = 3 (= N2)
2231        //    [25..28] aDescription_bytes = "vid"
2232        //    [28..32] duration.aSeconds = 0 (i32 big-endian)
2233        //    [32..34] duration.aMicroSeconds = 0
2234        //    [34]     audio = 1
2235        //    [35]     video = 1
2236        //    [36]     data = 0
2237        //    (no trailing objectInfo; N6 - (N2+10) = 13 - 13 = 0)
2238        //  [37]     serviceContextList_count = 0
2239        //  [38..42] messageBody_length = 8
2240        //    [42]     taps_count = 1
2241        //    [43..45] tap.id = 0
2242        //    [45..47] tap.use = 0x0018 (BIOP_ES_USE)
2243        //    [47..49] tap.association_tag = 0x0047
2244        //    [49]     tap.selector_length = 0
2245        // Total = 50 bytes
2246        // message_size = 50 - 12 = 38; objectInfo_length = 1+3+9 = 13
2247        use crate::carousel::biop::ior::Tap;
2248        #[rustfmt::skip]
2249        let expected: &[u8] = &[
2250            // BIOP header (12 bytes)
2251            0x42, 0x49, 0x4F, 0x50, // magic "BIOP"
2252            0x01,                   // major
2253            0x00,                   // minor
2254            0x00,                   // byte_order
2255            0x00,                   // message_type
2256            0x00, 0x00, 0x00, 0x26, // message_size = 38
2257            // objectKey (2 bytes)
2258            0x01,                   // objectKey_length = 1
2259            0xAB,                   // objectKey_data
2260            // objectKind (8 bytes)
2261            0x00, 0x00, 0x00, 0x04, // objectKind_length = 4
2262            0x73, 0x74, 0x72, 0x00, // "str\0"
2263            // objectInfo_length (2 bytes) + Info_T (13 bytes)
2264            0x00, 0x0D,             // objectInfo_length = 13
2265            0x03,                   // aDescription_length = 3 (= N2)
2266            0x76, 0x69, 0x64,       // "vid"
2267            0x00, 0x00, 0x00, 0x00, // duration.aSeconds = 0
2268            0x00, 0x00,             // duration.aMicroSeconds = 0
2269            0x01,                   // audio = 1
2270            0x01,                   // video = 1
2271            0x00,                   // data = 0
2272            // serviceContextList_count (1 byte)
2273            0x00,
2274            // messageBody_length (4 bytes)
2275            0x00, 0x00, 0x00, 0x08, // body_len = 8
2276            // body: taps_count(1) + 1 tap(7)
2277            0x01,                   // taps_count = 1
2278            0x00, 0x00,             // tap.id = 0
2279            0x00, 0x18,             // tap.use = 0x0018
2280            0x00, 0x47,             // tap.association_tag = 0x47
2281            0x00,                   // tap.selector_length = 0
2282        ];
2283        assert_eq!(expected.len(), 50);
2284        let expected_msg = BiopMessage::Stream(StreamMessage {
2285            object_key: &[0xAB],
2286            stream_info: DsmStreamInfo {
2287                description: b"vid",
2288                duration_seconds: 0,
2289                duration_microseconds: 0,
2290                audio: 1,
2291                video: 1,
2292                data: 0,
2293            },
2294            object_info_extra: &[],
2295            service_context: vec![],
2296            taps: vec![Tap {
2297                id: 0,
2298                use_: 0x0018,
2299                association_tag: 0x0047,
2300                selector: &[],
2301            }],
2302        });
2303
2304        // serialize → expected bytes
2305        let mut buf = vec![0u8; expected_msg.serialized_len()];
2306        expected_msg.serialize_into(&mut buf).unwrap();
2307        assert_eq!(
2308            buf.as_slice(),
2309            expected,
2310            "StreamMessage serialize must match byte anchor"
2311        );
2312
2313        // parse expected bytes → expected struct
2314        let (parsed, consumed) = BiopMessage::parse_at(expected).unwrap();
2315        assert_eq!(consumed, expected.len());
2316        assert_eq!(
2317            parsed, expected_msg,
2318            "StreamMessage parse must match byte anchor struct"
2319        );
2320    }
2321
2322    #[test]
2323    fn stream_event_message_byte_anchor() {
2324        // Hand-built minimal StreamEventMessage from Table 4.13:
2325        //
2326        // Offset table (from byte 0):
2327        //  [0..4]   magic = 0x42494F50
2328        //  [4]      major = 0x01
2329        //  [5]      minor = 0x00
2330        //  [6]      byte_order = 0x00
2331        //  [7]      message_type = 0x00
2332        //  [8..12]  message_size = 43
2333        //  [12]     objectKey_length = 1
2334        //  [13]     objectKey_data = 0xCD
2335        //  [14..18] objectKind_length = 4
2336        //  [18..22] objectKind_data = "ste\0"
2337        //  [22..24] objectInfo_length = 20 (= N6)
2338        //    Info_T (10 bytes, N2=0):
2339        //    [24]     aDescription_length = 0
2340        //    (no description bytes)
2341        //    [25..29] duration.aSeconds = 0
2342        //    [29..31] duration.aMicroSeconds = 0
2343        //    [31]     audio = 0
2344        //    [32]     video = 0
2345        //    [33]     data = 0
2346        //    EventList_T (10 bytes):
2347        //    [34..36] eventNames_count = 2
2348        //    [36]     name0_length = 3
2349        //    [37..40] "foo"
2350        //    [40]     name1_length = 3
2351        //    [41..44] "bar"
2352        //    (no trailing objectInfo extra; 20 - 10 - 10 = 0)
2353        //  [44]     serviceContextList_count = 0
2354        //  [45..49] messageBody_length = 6
2355        //    [49]     taps_count = 0
2356        //    [50]     eventIds_count = 2
2357        //    [51..53] eventId[0] = 0x0001
2358        //    [53..55] eventId[1] = 0x0002
2359        // Total = 55 bytes
2360        #[rustfmt::skip]
2361        let expected: &[u8] = &[
2362            // BIOP header (12 bytes)
2363            0x42, 0x49, 0x4F, 0x50, // magic "BIOP"
2364            0x01,                   // major
2365            0x00,                   // minor
2366            0x00,                   // byte_order
2367            0x00,                   // message_type
2368            0x00, 0x00, 0x00, 0x2B, // message_size = 43
2369            // objectKey (2 bytes)
2370            0x01,                   // objectKey_length = 1
2371            0xCD,                   // objectKey_data
2372            // objectKind (8 bytes)
2373            0x00, 0x00, 0x00, 0x04, // objectKind_length = 4
2374            0x73, 0x74, 0x65, 0x00, // "ste\0"
2375            // objectInfo_length (2 bytes) + objectInfo (20 bytes)
2376            0x00, 0x14,             // objectInfo_length = 20
2377            // Info_T (10 bytes, N2=0)
2378            0x00,                   // aDescription_length = 0
2379            0x00, 0x00, 0x00, 0x00, // duration.aSeconds = 0
2380            0x00, 0x00,             // duration.aMicroSeconds = 0
2381            0x00,                   // audio = 0
2382            0x00,                   // video = 0
2383            0x00,                   // data = 0
2384            // EventList_T (10 bytes): eventNames_count(2) + 2×(len(1)+name(3))
2385            0x00, 0x02,             // eventNames_count = 2
2386            0x03,                   // name0_length = 3
2387            0x66, 0x6F, 0x6F,       // "foo"
2388            0x03,                   // name1_length = 3
2389            0x62, 0x61, 0x72,       // "bar"
2390            // serviceContextList_count (1 byte)
2391            0x00,
2392            // messageBody_length (4 bytes)
2393            0x00, 0x00, 0x00, 0x06, // body_len = 6
2394            // body: taps_count(1)=0 + eventIds_count(1)=2 + eventId×2 (4)
2395            0x00,                   // taps_count = 0
2396            0x02,                   // eventIds_count = 2
2397            0x00, 0x01,             // eventId[0] = 1
2398            0x00, 0x02,             // eventId[1] = 2
2399        ];
2400        assert_eq!(expected.len(), 55);
2401
2402        let expected_msg = BiopMessage::StreamEvent(StreamEventMessage {
2403            object_key: &[0xCD],
2404            stream_info: DsmStreamInfo {
2405                description: &[],
2406                duration_seconds: 0,
2407                duration_microseconds: 0,
2408                audio: 0,
2409                video: 0,
2410                data: 0,
2411            },
2412            event_names: vec![b"foo".as_ref(), b"bar".as_ref()],
2413            object_info_extra: &[],
2414            service_context: vec![],
2415            taps: vec![],
2416            event_ids: vec![1, 2],
2417        });
2418
2419        // serialize → expected bytes
2420        let mut buf = vec![0u8; expected_msg.serialized_len()];
2421        expected_msg.serialize_into(&mut buf).unwrap();
2422        assert_eq!(
2423            buf.as_slice(),
2424            expected,
2425            "StreamEventMessage serialize must match byte anchor"
2426        );
2427
2428        // parse expected bytes → expected struct
2429        let (parsed, consumed) = BiopMessage::parse_at(expected).unwrap();
2430        assert_eq!(consumed, expected.len());
2431        assert_eq!(
2432            parsed, expected_msg,
2433            "StreamEventMessage parse must match byte anchor struct"
2434        );
2435    }
2436
2437    #[test]
2438    fn service_context_typed_round_trip() {
2439        // FileMessage with two non-trivial serviceContext entries.
2440        let msg = BiopMessage::File(FileMessage {
2441            object_key: &[0x01],
2442            content_size: 3,
2443            object_info_extra: &[],
2444            service_context: vec![
2445                ServiceContext {
2446                    context_id: 0xDEADBEEF,
2447                    data: &[1, 2, 3],
2448                },
2449                ServiceContext {
2450                    context_id: 0x11223344,
2451                    data: &[],
2452                },
2453            ],
2454            content: b"abc",
2455        });
2456
2457        // serialize
2458        let mut buf = vec![0u8; msg.serialized_len()];
2459        msg.serialize_into(&mut buf).unwrap();
2460
2461        // parse back
2462        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
2463        assert_eq!(consumed, buf.len(), "consumed must equal total buf len");
2464        assert_eq!(parsed, msg, "parsed must equal original");
2465
2466        // byte-exact re-serialize
2467        let mut buf2 = vec![0u8; parsed.serialized_len()];
2468        parsed.serialize_into(&mut buf2).unwrap();
2469        assert_eq!(
2470            buf, buf2,
2471            "serviceContext typed round-trip must be byte-exact"
2472        );
2473
2474        // spot-check the wire: count byte should be 2
2475        // serviceContextList starts after the BIOP header + key + kind + objectInfo
2476        // (12 + 2 + 8 + 2 + 8 = 32 bytes in)
2477        assert_eq!(buf[32], 2, "serviceContextList_count must be 2");
2478        // first entry: context_id = 0xDEADBEEF
2479        assert_eq!(&buf[33..37], &[0xDE, 0xAD, 0xBE, 0xEF]);
2480        // first entry: context_data_length = 3
2481        assert_eq!(&buf[37..39], &[0x00, 0x03]);
2482        // first entry: context_data = [1, 2, 3]
2483        assert_eq!(&buf[39..42], &[0x01, 0x02, 0x03]);
2484        // second entry: context_id = 0x11223344
2485        assert_eq!(&buf[42..46], &[0x11, 0x22, 0x33, 0x44]);
2486        // second entry: context_data_length = 0
2487        assert_eq!(&buf[46..48], &[0x00, 0x00]);
2488    }
2489
2490    #[test]
2491    fn binding_type_full_range_round_trip() {
2492        for v in 0u8..=0xFF {
2493            let bt = BindingType::from_u8(v);
2494            assert_eq!(bt.to_u8(), v, "BindingType round-trip failed for 0x{v:02X}");
2495        }
2496    }
2497
2498    #[test]
2499    fn binding_type_known_values() {
2500        assert_eq!(BindingType::from_u8(0x01), BindingType::NObject);
2501        assert_eq!(BindingType::from_u8(0x02), BindingType::NContext);
2502        assert_eq!(BindingType::NObject.name(), "nobject");
2503        assert_eq!(BindingType::NContext.name(), "ncontext");
2504        assert_eq!(BindingType::Reserved(0x05).name(), "reserved");
2505    }
2506
2507    #[test]
2508    fn directory_message_binding_type_round_trip() {
2509        let msg = sample_dir_message();
2510        let mut buf = vec![0u8; msg.serialized_len()];
2511        msg.serialize_into(&mut buf).unwrap();
2512        let (parsed, _) = BiopMessage::parse_at(&buf).unwrap();
2513        match parsed {
2514            BiopMessage::Directory(d) => {
2515                assert_eq!(d.bindings[0].binding_type, BindingType::NObject);
2516            }
2517            other => panic!("expected Directory, got {other:?}"),
2518        }
2519    }
2520}