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