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