Skip to main content

dvb_si/carousel/biop/
message.rs

1//! BIOP message types and `ModuleInfo` / `ServiceGatewayInfo` wire structures.
2//!
3//! All wire layouts from `docs/iso_13818_6_biop.md` (ETSI TR 101 202 §4.7.4–4.7.5).
4//!
5//! # Key entry points
6//!
7//! - [`BiopMessage::parse_at`] — parse one BIOP message from a slice, returning the
8//!   message and the number of bytes consumed (use to walk a module buffer).
9//! - [`ModuleInfo::parse`] — parse the DII `moduleInfoBytes` (Table 4.14).
10//! - [`ServiceGatewayInfo::parse`] — parse the DSI `privateData` (Table 4.15).
11
12use super::{
13    ior::{Ior, NameComponent},
14    BIOP_MAGIC, BIOP_VERSION_MAJOR, BIOP_VERSION_MINOR, BYTE_ORDER_BIG_ENDIAN,
15    COMPRESSED_MODULE_DESCRIPTOR_TAG,
16};
17use crate::error::{Error, Result};
18use dvb_common::{Parse, Serialize};
19
20// ── Message header constants ──────────────────────────────────────────────────
21
22/// BIOP message header: magic(4)+major(1)+minor(1)+byte_order(1)+message_type(1)+message_size(4) = 12.
23const BIOP_HEADER_LEN: usize = 12;
24/// `objectKey_length` (1 byte) field size.
25const OBJECT_KEY_LEN_FIELD: usize = 1;
26/// `objectKind_length` (4 bytes) field size.
27const OBJECT_KIND_LEN_FIELD: usize = 4;
28/// `objectKind_data` is always 4 bytes in DVB.
29const OBJECT_KIND_DATA_LEN: usize = 4;
30/// `objectInfo_length` (2 bytes) field size.
31const OBJECT_INFO_LEN_FIELD: usize = 2;
32/// `serviceContextList_count` (1 byte) field size.
33const SERVICE_CONTEXT_COUNT_FIELD: usize = 1;
34/// Per service context: context_id(4) + context_data_length(2).
35const SERVICE_CONTEXT_FIXED: usize = 6;
36/// `messageBody_length` (4 bytes) field size.
37const MESSAGE_BODY_LEN_FIELD: usize = 4;
38/// `bindings_count` (2 bytes) field size.
39const BINDINGS_COUNT_FIELD: usize = 2;
40/// `nameComponents_count` in a BIOP binding name: 1 byte.
41const BINDING_NAME_COUNT_FIELD: usize = 1;
42/// `bindingType` (1 byte) field.
43const BINDING_TYPE_FIELD: usize = 1;
44/// `objectInfo_length` in a binding (2 bytes).
45const BINDING_OBJ_INFO_LEN_FIELD: usize = 2;
46/// FileMessage: `content_length` (4 bytes).
47const FILE_CONTENT_LEN_FIELD: usize = 4;
48/// FileMessage: `ContentSize` (8 bytes, first 8 bytes of objectInfo).
49const FILE_CONTENT_SIZE_LEN: usize = 8;
50/// ModuleInfo: ModuleTimeOut(4)+BlockTimeOut(4)+MinBlockTime(4) = 12.
51const MODULE_INFO_FIXED: usize = 12;
52/// ModuleInfo: taps_count (1 byte).
53const MODULE_TAPS_COUNT_FIELD: usize = 1;
54/// ModuleInfo: UserInfoLength (1 byte) — note: 8-bit, not 16-bit.
55const MODULE_USER_INFO_LEN_FIELD: usize = 1;
56/// SGI: downloadTaps_count (1 byte).
57const SGI_DOWNLOAD_TAPS_COUNT_FIELD: usize = 1;
58/// SGI: userInfoLength (2 bytes).
59const SGI_USER_INFO_LEN_FIELD: usize = 2;
60
61// ── Binding ───────────────────────────────────────────────────────────────────
62
63/// One binding in a `DirectoryMessage` or `ServiceGatewayMessage`.
64/// TR 101 202 §4.7.4.1, Table 4.9.
65#[derive(Debug, Clone, PartialEq, Eq)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize))]
67pub struct Binding<'a> {
68    /// Name components — DVB: exactly one component.
69    #[cfg_attr(feature = "serde", serde(borrow))]
70    pub name: Vec<NameComponent<'a>>,
71    /// `bindingType` — `0x01` (`nobject`) or `0x02` (`ncontext`); see the module-level constants.
72    pub binding_type: u8,
73    /// IOR of the bound object.
74    pub ior: Ior<'a>,
75    /// Per-binding `objectInfo` data.
76    #[cfg_attr(feature = "serde", serde(borrow))]
77    pub object_info: &'a [u8],
78}
79
80impl<'a> Binding<'a> {
81    fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
82        // nameComponents_count (1 byte)
83        if pos + BINDING_NAME_COUNT_FIELD > end {
84            return Err(Error::BufferTooShort {
85                need: pos + BINDING_NAME_COUNT_FIELD,
86                have: end,
87                what: "Binding nameComponents_count",
88            });
89        }
90        let name_count = bytes[pos] as usize;
91        let mut cur = pos + BINDING_NAME_COUNT_FIELD;
92        let mut name = Vec::with_capacity(name_count.min(4));
93        for _ in 0..name_count {
94            let (nc, next) = NameComponent::parse_8bit(bytes, cur, end)?;
95            name.push(nc);
96            cur = next;
97        }
98
99        // bindingType (1 byte)
100        if cur + BINDING_TYPE_FIELD > end {
101            return Err(Error::BufferTooShort {
102                need: cur + BINDING_TYPE_FIELD,
103                have: end,
104                what: "Binding bindingType",
105            });
106        }
107        let binding_type = bytes[cur];
108        cur += BINDING_TYPE_FIELD;
109
110        // IOR — parse the remainder using Ior::parse which reads from position 0
111        // of a slice; we need to slice from cur to end.
112        let ior_slice = &bytes[cur..end];
113        let ior = Ior::parse(ior_slice)?;
114        let ior_len = ior.serialized_len();
115        cur += ior_len;
116
117        // objectInfo_length (2 bytes)
118        if cur + BINDING_OBJ_INFO_LEN_FIELD > end {
119            return Err(Error::BufferTooShort {
120                need: cur + BINDING_OBJ_INFO_LEN_FIELD,
121                have: end,
122                what: "Binding objectInfo_length",
123            });
124        }
125        let obj_info_len = u16::from_be_bytes([bytes[cur], bytes[cur + 1]]) as usize;
126        cur += BINDING_OBJ_INFO_LEN_FIELD;
127        if cur + obj_info_len > end {
128            return Err(Error::SectionLengthOverflow {
129                declared: obj_info_len,
130                available: end - cur,
131            });
132        }
133        let object_info = &bytes[cur..cur + obj_info_len];
134        cur += obj_info_len;
135
136        Ok((
137            Binding {
138                name,
139                binding_type,
140                ior,
141                object_info,
142            },
143            cur,
144        ))
145    }
146
147    fn serialized_len(&self) -> usize {
148        let name_len: usize = self.name.iter().map(|n| n.serialized_len_8bit()).sum();
149        BINDING_NAME_COUNT_FIELD
150            + name_len
151            + BINDING_TYPE_FIELD
152            + self.ior.serialized_len()
153            + BINDING_OBJ_INFO_LEN_FIELD
154            + self.object_info.len()
155    }
156
157    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
158        let len = self.serialized_len();
159        if buf.len() < len {
160            return Err(Error::OutputBufferTooSmall {
161                need: len,
162                have: buf.len(),
163            });
164        }
165        if self.name.len() > u8::MAX as usize {
166            return Err(Error::SectionLengthOverflow {
167                declared: self.name.len(),
168                available: u8::MAX as usize,
169            });
170        }
171        buf[0] = self.name.len() as u8;
172        let mut pos = BINDING_NAME_COUNT_FIELD;
173        for nc in &self.name {
174            let written = nc.serialize_8bit(&mut buf[pos..])?;
175            pos += written;
176        }
177        buf[pos] = self.binding_type;
178        pos += BINDING_TYPE_FIELD;
179        let written = self.ior.serialize_into(&mut buf[pos..])?;
180        pos += written;
181        if self.object_info.len() > u16::MAX as usize {
182            return Err(Error::SectionLengthOverflow {
183                declared: self.object_info.len(),
184                available: u16::MAX as usize,
185            });
186        }
187        buf[pos..pos + 2].copy_from_slice(&(self.object_info.len() as u16).to_be_bytes());
188        pos += BINDING_OBJ_INFO_LEN_FIELD;
189        buf[pos..pos + self.object_info.len()].copy_from_slice(self.object_info);
190        pos += self.object_info.len();
191        Ok(pos)
192    }
193}
194
195// ── Helpers ───────────────────────────────────────────────────────────────────
196
197/// Parse the common BIOP message header (magic, version, byte_order, message_type,
198/// message_size, objectKey, objectKind).
199/// Returns (object_key, object_kind_bytes, message_size, end_of_header_pos).
200fn parse_biop_header(bytes: &[u8]) -> Result<(&[u8], [u8; 4], usize, usize)> {
201    let total = bytes.len();
202    if total < BIOP_HEADER_LEN {
203        return Err(Error::BufferTooShort {
204            need: BIOP_HEADER_LEN,
205            have: total,
206            what: "BIOP message header",
207        });
208    }
209    let magic = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
210    if magic != BIOP_MAGIC {
211        return Err(Error::ReservedBitsViolation {
212            field: "BIOP magic",
213            reason: "must be 0x42494F50 (\"BIOP\")",
214        });
215    }
216    if bytes[4] != BIOP_VERSION_MAJOR || bytes[5] != BIOP_VERSION_MINOR {
217        return Err(Error::ReservedBitsViolation {
218            field: "biop_version",
219            reason: "must be 1.0",
220        });
221    }
222    if bytes[6] != BYTE_ORDER_BIG_ENDIAN {
223        return Err(Error::ReservedBitsViolation {
224            field: "byte_order",
225            reason: "must be 0x00 (big-endian) per DVB mandatory constraint",
226        });
227    }
228    // bytes[7] = message_type (must be 0x00 per DVB)
229    let message_size = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
230    let end = BIOP_HEADER_LEN + message_size;
231    if total < end {
232        return Err(Error::SectionLengthOverflow {
233            declared: message_size,
234            available: total - BIOP_HEADER_LEN,
235        });
236    }
237    let mut pos = BIOP_HEADER_LEN;
238
239    // objectKey_length (1 byte) + objectKey_data
240    if pos + OBJECT_KEY_LEN_FIELD > end {
241        return Err(Error::BufferTooShort {
242            need: pos + OBJECT_KEY_LEN_FIELD,
243            have: end,
244            what: "BIOP objectKey_length",
245        });
246    }
247    let obj_key_len = bytes[pos] as usize;
248    pos += OBJECT_KEY_LEN_FIELD;
249    if pos + obj_key_len > end {
250        return Err(Error::SectionLengthOverflow {
251            declared: obj_key_len,
252            available: end - pos,
253        });
254    }
255    let object_key = &bytes[pos..pos + obj_key_len];
256    pos += obj_key_len;
257
258    // objectKind_length (4 bytes) + objectKind_data (4 bytes)
259    if pos + OBJECT_KIND_LEN_FIELD > end {
260        return Err(Error::BufferTooShort {
261            need: pos + OBJECT_KIND_LEN_FIELD,
262            have: end,
263            what: "BIOP objectKind_length",
264        });
265    }
266    let kind_len =
267        u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) as usize;
268    pos += OBJECT_KIND_LEN_FIELD;
269    if kind_len != OBJECT_KIND_DATA_LEN {
270        return Err(Error::ValueOutOfRange {
271            field: "objectKind_length",
272            reason: "DVB BIOP objectKind must be exactly 4 bytes",
273        });
274    }
275    if pos + OBJECT_KIND_DATA_LEN > end {
276        return Err(Error::SectionLengthOverflow {
277            declared: OBJECT_KIND_DATA_LEN,
278            available: end - pos,
279        });
280    }
281    let mut kind_bytes = [0u8; 4];
282    kind_bytes.copy_from_slice(&bytes[pos..pos + 4]);
283    pos += OBJECT_KIND_DATA_LEN;
284
285    Ok((object_key, kind_bytes, message_size, pos))
286}
287
288/// Parse the serviceContextList and return the raw bytes (including count byte)
289/// and the position after it. We preserve as raw bytes for round-trip fidelity.
290fn parse_service_context_list(bytes: &[u8], pos: usize, end: usize) -> Result<(&[u8], usize)> {
291    if pos + SERVICE_CONTEXT_COUNT_FIELD > end {
292        return Err(Error::BufferTooShort {
293            need: pos + SERVICE_CONTEXT_COUNT_FIELD,
294            have: end,
295            what: "serviceContextList_count",
296        });
297    }
298    let count = bytes[pos] as usize;
299    let list_start = pos;
300    let mut cur = pos + SERVICE_CONTEXT_COUNT_FIELD;
301    for _ in 0..count {
302        if cur + SERVICE_CONTEXT_FIXED > end {
303            return Err(Error::BufferTooShort {
304                need: cur + SERVICE_CONTEXT_FIXED,
305                have: end,
306                what: "serviceContext entry",
307            });
308        }
309        let ctx_data_len = u16::from_be_bytes([bytes[cur + 4], bytes[cur + 5]]) as usize;
310        cur += SERVICE_CONTEXT_FIXED;
311        if cur + ctx_data_len > end {
312            return Err(Error::SectionLengthOverflow {
313                declared: ctx_data_len,
314                available: end - cur,
315            });
316        }
317        cur += ctx_data_len;
318    }
319    Ok((&bytes[list_start..cur], cur))
320}
321
322/// Write the 12-byte BIOP message header to `buf` at position 0.
323fn write_biop_header(buf: &mut [u8], message_size: u32) {
324    buf[0..4].copy_from_slice(&BIOP_MAGIC.to_be_bytes());
325    buf[4] = BIOP_VERSION_MAJOR;
326    buf[5] = BIOP_VERSION_MINOR;
327    buf[6] = BYTE_ORDER_BIG_ENDIAN;
328    buf[7] = 0x00; // message_type
329    buf[8..12].copy_from_slice(&message_size.to_be_bytes());
330}
331
332// ── DirectoryMessage ──────────────────────────────────────────────────────────
333
334/// BIOP::DirectoryMessage — or ServiceGatewayMessage (same wire format, kind differs).
335/// TR 101 202 §4.7.4.1/§4.7.4.4, Table 4.9.
336#[derive(Debug, Clone, PartialEq, Eq)]
337#[cfg_attr(feature = "serde", derive(serde::Serialize))]
338pub struct DirectoryMessage<'a> {
339    /// Object kind (`"dir\0"` or `"srg\0"`).
340    pub object_kind: [u8; 4],
341    /// `objectKey_data`.
342    #[cfg_attr(feature = "serde", serde(borrow))]
343    pub object_key: &'a [u8],
344    /// `objectInfo_data` (after key and kind but before serviceContextList).
345    #[cfg_attr(feature = "serde", serde(borrow))]
346    pub object_info: &'a [u8],
347    /// Raw `serviceContextList` bytes (count byte + context entries), preserved for round-trip.
348    #[cfg_attr(feature = "serde", serde(borrow))]
349    pub service_context: &'a [u8],
350    /// Binding entries.
351    pub bindings: Vec<Binding<'a>>,
352}
353
354impl<'a> DirectoryMessage<'a> {
355    /// True if this is a ServiceGateway object (`object_kind == "srg\0"`).
356    pub fn is_service_gateway(&self) -> bool {
357        &self.object_kind == b"srg\0"
358    }
359
360    fn parse_from(
361        bytes: &'a [u8],
362        object_key: &'a [u8],
363        object_kind: [u8; 4],
364        pos: usize,
365        end: usize,
366    ) -> Result<Self> {
367        let mut cur = pos;
368
369        // objectInfo_length (2 bytes) + objectInfo_data
370        if cur + OBJECT_INFO_LEN_FIELD > end {
371            return Err(Error::BufferTooShort {
372                need: cur + OBJECT_INFO_LEN_FIELD,
373                have: end,
374                what: "DirectoryMessage objectInfo_length",
375            });
376        }
377        let obj_info_len = u16::from_be_bytes([bytes[cur], bytes[cur + 1]]) as usize;
378        cur += OBJECT_INFO_LEN_FIELD;
379        if cur + obj_info_len > end {
380            return Err(Error::SectionLengthOverflow {
381                declared: obj_info_len,
382                available: end - cur,
383            });
384        }
385        let object_info = &bytes[cur..cur + obj_info_len];
386        cur += obj_info_len;
387
388        // serviceContextList (raw)
389        let (service_context, next) = parse_service_context_list(bytes, cur, end)?;
390        cur = next;
391
392        // messageBody_length (4 bytes)
393        if cur + MESSAGE_BODY_LEN_FIELD > end {
394            return Err(Error::BufferTooShort {
395                need: cur + MESSAGE_BODY_LEN_FIELD,
396                have: end,
397                what: "DirectoryMessage messageBody_length",
398            });
399        }
400        let body_len =
401            u32::from_be_bytes([bytes[cur], bytes[cur + 1], bytes[cur + 2], bytes[cur + 3]])
402                as usize;
403        cur += MESSAGE_BODY_LEN_FIELD;
404        let body_end = cur + body_len;
405        if body_end > end {
406            return Err(Error::SectionLengthOverflow {
407                declared: body_len,
408                available: end - cur,
409            });
410        }
411
412        // bindings_count (2 bytes)
413        if cur + BINDINGS_COUNT_FIELD > body_end {
414            return Err(Error::BufferTooShort {
415                need: cur + BINDINGS_COUNT_FIELD,
416                have: body_end,
417                what: "DirectoryMessage bindings_count",
418            });
419        }
420        let bindings_count = u16::from_be_bytes([bytes[cur], bytes[cur + 1]]) as usize;
421        cur += BINDINGS_COUNT_FIELD;
422
423        let mut bindings = Vec::with_capacity(bindings_count.min(256));
424        for _ in 0..bindings_count {
425            let (binding, next) = Binding::parse_from(bytes, cur, body_end)?;
426            bindings.push(binding);
427            cur = next;
428        }
429
430        Ok(DirectoryMessage {
431            object_kind,
432            object_key,
433            object_info,
434            service_context,
435            bindings,
436        })
437    }
438
439    fn body_len(&self) -> usize {
440        let bindings_len: usize = self.bindings.iter().map(|b| b.serialized_len()).sum();
441        BINDINGS_COUNT_FIELD + bindings_len
442    }
443
444    fn serialized_len_inner(&self) -> usize {
445        // after the header: objectKey + objectKind + objectInfo + serviceContext + messageBody
446        let key_part = OBJECT_KEY_LEN_FIELD
447            + self.object_key.len()
448            + OBJECT_KIND_LEN_FIELD
449            + OBJECT_KIND_DATA_LEN;
450        let info_part = OBJECT_INFO_LEN_FIELD + self.object_info.len();
451        let svc_ctx_part = self.service_context.len();
452        let body_part = MESSAGE_BODY_LEN_FIELD + self.body_len();
453        key_part + info_part + svc_ctx_part + body_part
454    }
455
456    /// Total serialized length including the 12-byte BIOP header.
457    pub fn serialized_len_total(&self) -> usize {
458        BIOP_HEADER_LEN + self.serialized_len_inner()
459    }
460
461    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
462        let inner_len = self.serialized_len_inner();
463        let total = BIOP_HEADER_LEN + inner_len;
464        if buf.len() < total {
465            return Err(Error::OutputBufferTooSmall {
466                need: total,
467                have: buf.len(),
468            });
469        }
470        if inner_len > u32::MAX as usize {
471            return Err(Error::SectionLengthOverflow {
472                declared: inner_len,
473                available: u32::MAX as usize,
474            });
475        }
476        write_biop_header(buf, inner_len as u32);
477        let mut pos = BIOP_HEADER_LEN;
478
479        // objectKey
480        if self.object_key.len() > u8::MAX as usize {
481            return Err(Error::SectionLengthOverflow {
482                declared: self.object_key.len(),
483                available: u8::MAX as usize,
484            });
485        }
486        buf[pos] = self.object_key.len() as u8;
487        pos += OBJECT_KEY_LEN_FIELD;
488        buf[pos..pos + self.object_key.len()].copy_from_slice(self.object_key);
489        pos += self.object_key.len();
490
491        // objectKind
492        buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
493        pos += OBJECT_KIND_LEN_FIELD;
494        buf[pos..pos + 4].copy_from_slice(&self.object_kind);
495        pos += OBJECT_KIND_DATA_LEN;
496
497        // objectInfo
498        if self.object_info.len() > u16::MAX as usize {
499            return Err(Error::SectionLengthOverflow {
500                declared: self.object_info.len(),
501                available: u16::MAX as usize,
502            });
503        }
504        buf[pos..pos + 2].copy_from_slice(&(self.object_info.len() as u16).to_be_bytes());
505        pos += OBJECT_INFO_LEN_FIELD;
506        buf[pos..pos + self.object_info.len()].copy_from_slice(self.object_info);
507        pos += self.object_info.len();
508
509        // serviceContextList (raw, already includes count byte)
510        buf[pos..pos + self.service_context.len()].copy_from_slice(self.service_context);
511        pos += self.service_context.len();
512
513        // messageBody
514        let body_len = self.body_len();
515        if body_len > u32::MAX as usize {
516            return Err(Error::SectionLengthOverflow {
517                declared: body_len,
518                available: u32::MAX as usize,
519            });
520        }
521        buf[pos..pos + 4].copy_from_slice(&(body_len as u32).to_be_bytes());
522        pos += MESSAGE_BODY_LEN_FIELD;
523
524        // bindings_count
525        if self.bindings.len() > u16::MAX as usize {
526            return Err(Error::SectionLengthOverflow {
527                declared: self.bindings.len(),
528                available: u16::MAX as usize,
529            });
530        }
531        buf[pos..pos + 2].copy_from_slice(&(self.bindings.len() as u16).to_be_bytes());
532        pos += BINDINGS_COUNT_FIELD;
533
534        for binding in &self.bindings {
535            let written = binding.serialize_into_buf(&mut buf[pos..])?;
536            pos += written;
537        }
538
539        Ok(total)
540    }
541}
542
543// ── FileMessage ───────────────────────────────────────────────────────────────
544
545/// BIOP::FileMessage — TR 101 202 §4.7.4.2, Table 4.10.
546///
547/// `objectInfo_length ≥ 8`; the first 8 bytes of objectInfo are the
548/// `DSM::File::ContentSize` (64-bit big-endian).
549#[derive(Debug, Clone, PartialEq, Eq)]
550#[cfg_attr(feature = "serde", derive(serde::Serialize))]
551pub struct FileMessage<'a> {
552    /// `objectKey_data`.
553    #[cfg_attr(feature = "serde", serde(borrow))]
554    pub object_key: &'a [u8],
555    /// `DSM::File::ContentSize` from the first 8 bytes of objectInfo.
556    pub content_size: u64,
557    /// Remaining objectInfo bytes after the 8-byte ContentSize.
558    #[cfg_attr(feature = "serde", serde(borrow))]
559    pub object_info_extra: &'a [u8],
560    /// Raw serviceContextList bytes.
561    #[cfg_attr(feature = "serde", serde(borrow))]
562    pub service_context: &'a [u8],
563    /// File content bytes.
564    #[cfg_attr(feature = "serde", serde(borrow))]
565    pub content: &'a [u8],
566}
567
568impl<'a> FileMessage<'a> {
569    fn parse_from(bytes: &'a [u8], object_key: &'a [u8], pos: usize, end: usize) -> Result<Self> {
570        let mut cur = pos;
571
572        // objectInfo_length (2 bytes)
573        if cur + OBJECT_INFO_LEN_FIELD > end {
574            return Err(Error::BufferTooShort {
575                need: cur + OBJECT_INFO_LEN_FIELD,
576                have: end,
577                what: "FileMessage objectInfo_length",
578            });
579        }
580        let obj_info_len = u16::from_be_bytes([bytes[cur], bytes[cur + 1]]) as usize;
581        cur += OBJECT_INFO_LEN_FIELD;
582        if obj_info_len < FILE_CONTENT_SIZE_LEN {
583            return Err(Error::ValueOutOfRange {
584                field: "FileMessage.objectInfo_length",
585                reason: "FileMessage objectInfo must be at least 8 bytes (ContentSize)",
586            });
587        }
588        if cur + obj_info_len > end {
589            return Err(Error::SectionLengthOverflow {
590                declared: obj_info_len,
591                available: end - cur,
592            });
593        }
594        let content_size = u64::from_be_bytes([
595            bytes[cur],
596            bytes[cur + 1],
597            bytes[cur + 2],
598            bytes[cur + 3],
599            bytes[cur + 4],
600            bytes[cur + 5],
601            bytes[cur + 6],
602            bytes[cur + 7],
603        ]);
604        let object_info_extra = &bytes[cur + FILE_CONTENT_SIZE_LEN..cur + obj_info_len];
605        cur += obj_info_len;
606
607        // serviceContextList
608        let (service_context, next) = parse_service_context_list(bytes, cur, end)?;
609        cur = next;
610
611        // messageBody_length (4 bytes)
612        if cur + MESSAGE_BODY_LEN_FIELD > end {
613            return Err(Error::BufferTooShort {
614                need: cur + MESSAGE_BODY_LEN_FIELD,
615                have: end,
616                what: "FileMessage messageBody_length",
617            });
618        }
619        let body_len =
620            u32::from_be_bytes([bytes[cur], bytes[cur + 1], bytes[cur + 2], bytes[cur + 3]])
621                as usize;
622        cur += MESSAGE_BODY_LEN_FIELD;
623        let body_end = cur + body_len;
624        if body_end > end {
625            return Err(Error::SectionLengthOverflow {
626                declared: body_len,
627                available: end - cur,
628            });
629        }
630
631        // content_length (4 bytes) + content_data
632        if cur + FILE_CONTENT_LEN_FIELD > body_end {
633            return Err(Error::BufferTooShort {
634                need: cur + FILE_CONTENT_LEN_FIELD,
635                have: body_end,
636                what: "FileMessage content_length",
637            });
638        }
639        let content_len =
640            u32::from_be_bytes([bytes[cur], bytes[cur + 1], bytes[cur + 2], bytes[cur + 3]])
641                as usize;
642        cur += FILE_CONTENT_LEN_FIELD;
643        if cur + content_len > body_end {
644            return Err(Error::SectionLengthOverflow {
645                declared: content_len,
646                available: body_end - cur,
647            });
648        }
649        let content = &bytes[cur..cur + content_len];
650
651        Ok(FileMessage {
652            object_key,
653            content_size,
654            object_info_extra,
655            service_context,
656            content,
657        })
658    }
659
660    fn serialized_len_inner(&self) -> usize {
661        let obj_info_total = FILE_CONTENT_SIZE_LEN + self.object_info_extra.len();
662        OBJECT_KEY_LEN_FIELD
663            + self.object_key.len()
664            + OBJECT_KIND_LEN_FIELD
665            + OBJECT_KIND_DATA_LEN
666            + OBJECT_INFO_LEN_FIELD
667            + obj_info_total
668            + self.service_context.len()
669            + MESSAGE_BODY_LEN_FIELD
670            + FILE_CONTENT_LEN_FIELD
671            + self.content.len()
672    }
673
674    /// Total serialized length including the 12-byte BIOP header.
675    pub fn serialized_len_total(&self) -> usize {
676        BIOP_HEADER_LEN + self.serialized_len_inner()
677    }
678
679    fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
680        let inner_len = self.serialized_len_inner();
681        let total = BIOP_HEADER_LEN + inner_len;
682        if buf.len() < total {
683            return Err(Error::OutputBufferTooSmall {
684                need: total,
685                have: buf.len(),
686            });
687        }
688        write_biop_header(buf, inner_len as u32);
689        let mut pos = BIOP_HEADER_LEN;
690
691        if self.object_key.len() > u8::MAX as usize {
692            return Err(Error::SectionLengthOverflow {
693                declared: self.object_key.len(),
694                available: u8::MAX as usize,
695            });
696        }
697        buf[pos] = self.object_key.len() as u8;
698        pos += OBJECT_KEY_LEN_FIELD;
699        buf[pos..pos + self.object_key.len()].copy_from_slice(self.object_key);
700        pos += self.object_key.len();
701
702        // objectKind = "fil\0"
703        buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
704        pos += OBJECT_KIND_LEN_FIELD;
705        buf[pos..pos + 4].copy_from_slice(b"fil\0");
706        pos += OBJECT_KIND_DATA_LEN;
707
708        // objectInfo: ContentSize(8) + extra
709        let obj_info_total = FILE_CONTENT_SIZE_LEN + self.object_info_extra.len();
710        if obj_info_total > u16::MAX as usize {
711            return Err(Error::SectionLengthOverflow {
712                declared: obj_info_total,
713                available: u16::MAX as usize,
714            });
715        }
716        buf[pos..pos + 2].copy_from_slice(&(obj_info_total as u16).to_be_bytes());
717        pos += OBJECT_INFO_LEN_FIELD;
718        buf[pos..pos + 8].copy_from_slice(&self.content_size.to_be_bytes());
719        pos += FILE_CONTENT_SIZE_LEN;
720        buf[pos..pos + self.object_info_extra.len()].copy_from_slice(self.object_info_extra);
721        pos += self.object_info_extra.len();
722
723        // serviceContextList (raw)
724        buf[pos..pos + self.service_context.len()].copy_from_slice(self.service_context);
725        pos += self.service_context.len();
726
727        // messageBody
728        let body_len = FILE_CONTENT_LEN_FIELD + self.content.len();
729        buf[pos..pos + 4].copy_from_slice(&(body_len as u32).to_be_bytes());
730        pos += MESSAGE_BODY_LEN_FIELD;
731        buf[pos..pos + 4].copy_from_slice(&(self.content.len() as u32).to_be_bytes());
732        pos += FILE_CONTENT_LEN_FIELD;
733        buf[pos..pos + self.content.len()].copy_from_slice(self.content);
734
735        Ok(total)
736    }
737}
738
739// ── BiopMessage ───────────────────────────────────────────────────────────────
740
741/// A parsed BIOP message — discriminated by `objectKind`.
742/// TR 101 202 §4.7.4.
743#[derive(Debug, Clone, PartialEq, Eq)]
744#[cfg_attr(feature = "serde", derive(serde::Serialize))]
745#[non_exhaustive]
746pub enum BiopMessage<'a> {
747    /// `"dir\0"` — DSM::DirectoryMessage.
748    Directory(DirectoryMessage<'a>),
749    /// `"fil\0"` — DSM::FileMessage.
750    File(FileMessage<'a>),
751    /// `"srg\0"` — DSM::ServiceGatewayMessage (same wire format as Directory).
752    ServiceGateway(DirectoryMessage<'a>),
753    /// `"str\0"` — DSM::StreamMessage (raw body).
754    #[cfg_attr(feature = "serde", serde(borrow))]
755    Stream(&'a [u8]),
756    /// `"ste\0"` — BIOP::StreamEventMessage (raw body).
757    #[cfg_attr(feature = "serde", serde(borrow))]
758    StreamEvent(&'a [u8]),
759}
760
761impl<'a> BiopMessage<'a> {
762    /// Parse one BIOP message from `bytes` starting at offset 0.
763    ///
764    /// Returns `(message, consumed)` where `consumed` is exactly
765    /// `12 + message_size` (the number of bytes consumed from `bytes`).
766    pub fn parse_at(bytes: &'a [u8]) -> Result<(Self, usize)> {
767        let (object_key, kind_bytes, message_size, pos) = parse_biop_header(bytes)?;
768        let consumed = BIOP_HEADER_LEN + message_size;
769        let end = consumed;
770
771        let msg = match &kind_bytes {
772            b"dir\0" => {
773                let dm = DirectoryMessage::parse_from(bytes, object_key, kind_bytes, pos, end)?;
774                BiopMessage::Directory(dm)
775            }
776            b"srg\0" => {
777                let dm = DirectoryMessage::parse_from(bytes, object_key, kind_bytes, pos, end)?;
778                BiopMessage::ServiceGateway(dm)
779            }
780            b"fil\0" => {
781                let fm = FileMessage::parse_from(bytes, object_key, pos, end)?;
782                BiopMessage::File(fm)
783            }
784            b"str\0" => BiopMessage::Stream(&bytes[pos..end]),
785            b"ste\0" => BiopMessage::StreamEvent(&bytes[pos..end]),
786            _ => {
787                // Unknown kind — return as raw stream body
788                BiopMessage::Stream(&bytes[pos..end])
789            }
790        };
791
792        Ok((msg, consumed))
793    }
794
795    fn serialized_len_total(&self) -> usize {
796        match self {
797            Self::Directory(d) | Self::ServiceGateway(d) => d.serialized_len_total(),
798            Self::File(f) => f.serialized_len_total(),
799            Self::Stream(raw) | Self::StreamEvent(raw) => BIOP_HEADER_LEN + raw.len(),
800        }
801    }
802
803    fn kind_bytes(&self) -> [u8; 4] {
804        match self {
805            Self::Directory(_) => *b"dir\0",
806            Self::File(_) => *b"fil\0",
807            Self::ServiceGateway(_) => *b"srg\0",
808            Self::Stream(_) => *b"str\0",
809            Self::StreamEvent(_) => *b"ste\0",
810        }
811    }
812}
813
814impl Serialize for BiopMessage<'_> {
815    type Error = crate::error::Error;
816
817    fn serialized_len(&self) -> usize {
818        self.serialized_len_total()
819    }
820
821    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
822        let len = self.serialized_len_total();
823        if buf.len() < len {
824            return Err(Error::OutputBufferTooSmall {
825                need: len,
826                have: buf.len(),
827            });
828        }
829        match self {
830            Self::Directory(d) | Self::ServiceGateway(d) => {
831                d.serialize_into_buf(buf)?;
832            }
833            Self::File(f) => {
834                f.serialize_into_buf(buf)?;
835            }
836            Self::Stream(raw) | Self::StreamEvent(raw) => {
837                // For stream/stream-event the kind tells us what to write in the header.
838                let kind = self.kind_bytes();
839                let inner_len = OBJECT_KEY_LEN_FIELD // key_len=0
840                    + OBJECT_KIND_LEN_FIELD + OBJECT_KIND_DATA_LEN
841                    + raw.len();
842                write_biop_header(buf, inner_len as u32);
843                let mut pos = BIOP_HEADER_LEN;
844                buf[pos] = 0; // objectKey_length = 0
845                pos += OBJECT_KEY_LEN_FIELD;
846                buf[pos..pos + 4].copy_from_slice(&(OBJECT_KIND_DATA_LEN as u32).to_be_bytes());
847                pos += OBJECT_KIND_LEN_FIELD;
848                buf[pos..pos + 4].copy_from_slice(&kind);
849                pos += OBJECT_KIND_DATA_LEN;
850                buf[pos..pos + raw.len()].copy_from_slice(raw);
851            }
852        }
853        Ok(len)
854    }
855}
856
857// ── ModuleInfo ────────────────────────────────────────────────────────────────
858
859/// BIOP::ModuleInfo — carried in the DII `moduleInfoBytes`.
860/// TR 101 202 §4.7.5.1, Table 4.14.
861#[derive(Debug, Clone, PartialEq, Eq)]
862#[cfg_attr(feature = "serde", derive(serde::Serialize))]
863pub struct ModuleInfo<'a> {
864    /// `ModuleTimeOut` — µs to time out acquisition of all blocks.
865    pub module_timeout: u32,
866    /// `BlockTimeOut` — µs to time out the next block.
867    pub block_timeout: u32,
868    /// `MinBlockTime` — min µs between two blocks.
869    pub min_block_time: u32,
870    /// BIOP::Tap entries (≥1 BIOP_OBJECT_USE tap).
871    #[cfg_attr(feature = "serde", serde(borrow))]
872    pub taps: Vec<super::ior::Tap<'a>>,
873    /// `userInfo` descriptor loop bytes.
874    #[cfg_attr(feature = "serde", serde(borrow))]
875    pub user_info: &'a [u8],
876}
877
878impl<'a> ModuleInfo<'a> {
879    /// Iterate over descriptors in the `userInfo` loop.
880    ///
881    /// Each item is `(tag: u8, data: &[u8])`.
882    pub fn descriptors(&self) -> impl Iterator<Item = (u8, &[u8])> {
883        DescriptorIter {
884            data: self.user_info,
885            pos: 0,
886        }
887    }
888
889    /// Return the `compressed_module_descriptor` (tag 0x09) from the userInfo
890    /// loop, if present.
891    pub fn compressed_module_descriptor(&self) -> Option<CompressedModuleDescriptor<'_>> {
892        for (tag, data) in self.descriptors() {
893            if tag == COMPRESSED_MODULE_DESCRIPTOR_TAG {
894                return Some(CompressedModuleDescriptor { body: data });
895            }
896        }
897        None
898    }
899}
900
901struct DescriptorIter<'a> {
902    data: &'a [u8],
903    pos: usize,
904}
905
906impl<'a> Iterator for DescriptorIter<'a> {
907    type Item = (u8, &'a [u8]);
908    fn next(&mut self) -> Option<Self::Item> {
909        let end = self.data.len();
910        if self.pos + 2 > end {
911            return None;
912        }
913        let tag = self.data[self.pos];
914        let len = self.data[self.pos + 1] as usize;
915        self.pos += 2;
916        if self.pos + len > end {
917            return None;
918        }
919        let d = &self.data[self.pos..self.pos + len];
920        self.pos += len;
921        Some((tag, d))
922    }
923}
924
925impl<'a> Parse<'a> for ModuleInfo<'a> {
926    type Error = crate::error::Error;
927
928    fn parse(bytes: &'a [u8]) -> Result<Self> {
929        let end = bytes.len();
930        if end < MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD {
931            return Err(Error::BufferTooShort {
932                need: MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD,
933                have: end,
934                what: "ModuleInfo fixed fields",
935            });
936        }
937        let module_timeout = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
938        let block_timeout = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
939        let min_block_time = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
940        let taps_count = bytes[12] as usize;
941        let mut pos = MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD;
942
943        let mut taps = Vec::with_capacity(taps_count.min(8));
944        for _ in 0..taps_count {
945            let (tap, next) = super::ior::Tap::parse_from(bytes, pos, end)?;
946            taps.push(tap);
947            pos = next;
948        }
949
950        if pos + MODULE_USER_INFO_LEN_FIELD > end {
951            return Err(Error::BufferTooShort {
952                need: pos + MODULE_USER_INFO_LEN_FIELD,
953                have: end,
954                what: "ModuleInfo UserInfoLength",
955            });
956        }
957        let user_info_len = bytes[pos] as usize;
958        pos += MODULE_USER_INFO_LEN_FIELD;
959        if pos + user_info_len > end {
960            return Err(Error::SectionLengthOverflow {
961                declared: user_info_len,
962                available: end - pos,
963            });
964        }
965        let user_info = &bytes[pos..pos + user_info_len];
966
967        Ok(ModuleInfo {
968            module_timeout,
969            block_timeout,
970            min_block_time,
971            taps,
972            user_info,
973        })
974    }
975}
976
977impl Serialize for ModuleInfo<'_> {
978    type Error = crate::error::Error;
979
980    fn serialized_len(&self) -> usize {
981        let taps_len: usize = self.taps.iter().map(|t| t.serialized_len()).sum();
982        MODULE_INFO_FIXED
983            + MODULE_TAPS_COUNT_FIELD
984            + taps_len
985            + MODULE_USER_INFO_LEN_FIELD
986            + self.user_info.len()
987    }
988
989    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
990        let len = self.serialized_len();
991        if buf.len() < len {
992            return Err(Error::OutputBufferTooSmall {
993                need: len,
994                have: buf.len(),
995            });
996        }
997        buf[0..4].copy_from_slice(&self.module_timeout.to_be_bytes());
998        buf[4..8].copy_from_slice(&self.block_timeout.to_be_bytes());
999        buf[8..12].copy_from_slice(&self.min_block_time.to_be_bytes());
1000        if self.taps.len() > u8::MAX as usize {
1001            return Err(Error::SectionLengthOverflow {
1002                declared: self.taps.len(),
1003                available: u8::MAX as usize,
1004            });
1005        }
1006        buf[12] = self.taps.len() as u8;
1007        let mut pos = MODULE_INFO_FIXED + MODULE_TAPS_COUNT_FIELD;
1008        for tap in &self.taps {
1009            let written = tap.serialize_into_buf(&mut buf[pos..])?;
1010            pos += written;
1011        }
1012        if self.user_info.len() > u8::MAX as usize {
1013            return Err(Error::SectionLengthOverflow {
1014                declared: self.user_info.len(),
1015                available: u8::MAX as usize,
1016            });
1017        }
1018        buf[pos] = self.user_info.len() as u8;
1019        pos += MODULE_USER_INFO_LEN_FIELD;
1020        buf[pos..pos + self.user_info.len()].copy_from_slice(self.user_info);
1021        pos += self.user_info.len();
1022        Ok(pos)
1023    }
1024}
1025
1026// ── CompressedModuleDescriptor ────────────────────────────────────────────────
1027
1028/// A `compressed_module_descriptor` (tag 0x09) found in a `ModuleInfo` userInfo loop.
1029/// TR 101 202 §4.6.6.10.
1030///
1031/// The body bytes are the zlib-encoded module payload (RFC 1950 CMF+FLG header,
1032/// DEFLATE stream, Adler-32 checksum).  Decompression requires the `flate2` feature.
1033#[derive(Debug, Clone, PartialEq, Eq)]
1034#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1035pub struct CompressedModuleDescriptor<'a> {
1036    /// Raw descriptor body (the zlib stream).
1037    #[cfg_attr(feature = "serde", serde(borrow))]
1038    pub body: &'a [u8],
1039}
1040
1041/// Decompress a zlib-encoded module payload.
1042///
1043/// Uses [`flate2`](https://crates.io/crates/flate2) (optional feature `flate2`).
1044/// Returns the decompressed bytes, or an error if the zlib stream is invalid.
1045#[cfg(feature = "flate2")]
1046pub fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>> {
1047    use std::io::Read;
1048    let mut decoder = flate2::read::ZlibDecoder::new(data);
1049    let mut out = Vec::new();
1050    decoder
1051        .read_to_end(&mut out)
1052        .map_err(|e| Error::ReservedBitsViolation {
1053            field: "compressed_module_descriptor body",
1054            reason: if e.kind() == std::io::ErrorKind::InvalidData {
1055                "zlib decompression failed: invalid data"
1056            } else {
1057                "zlib decompression failed"
1058            },
1059        })?;
1060    Ok(out)
1061}
1062
1063// ── ServiceGatewayInfo ────────────────────────────────────────────────────────
1064
1065/// BIOP::ServiceGatewayInfo — the DSI `privateData` for an object carousel.
1066/// TR 101 202 §4.7.5.2, Table 4.15.
1067///
1068/// Parse with [`ServiceGatewayInfo::parse`]; serialize with [`ServiceGatewayInfo::to_bytes`].
1069/// The round-trip `to_bytes() == dsi.private_data` is a hard project invariant.
1070#[derive(Debug, Clone, PartialEq, Eq)]
1071#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1072pub struct ServiceGatewayInfo<'a> {
1073    /// IOR of the ServiceGateway object.
1074    pub ior: Ior<'a>,
1075    /// Raw `Tap() × downloadTaps_count` bytes (count byte + tap data).
1076    /// In practice `downloadTaps_count` is typically 0, making this `&[0x00]`.
1077    #[cfg_attr(feature = "serde", serde(borrow))]
1078    pub download_taps: &'a [u8],
1079    /// Raw serviceContextList bytes (count byte + context entries).
1080    #[cfg_attr(feature = "serde", serde(borrow))]
1081    pub service_context: &'a [u8],
1082    /// `userInfo` descriptor loop bytes.
1083    #[cfg_attr(feature = "serde", serde(borrow))]
1084    pub user_info: &'a [u8],
1085}
1086
1087impl<'a> ServiceGatewayInfo<'a> {
1088    /// Parse the DSI `privateData` bytes as a ServiceGatewayInfo.
1089    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
1090        let end = bytes.len();
1091        let ior = Ior::parse(bytes)?;
1092        let mut pos = ior.serialized_len();
1093
1094        // downloadTaps: count(1) + taps (raw, count × variable)
1095        // We preserve the entire block raw: start at pos (count byte), walk past taps.
1096        if pos + SGI_DOWNLOAD_TAPS_COUNT_FIELD > end {
1097            return Err(Error::BufferTooShort {
1098                need: pos + SGI_DOWNLOAD_TAPS_COUNT_FIELD,
1099                have: end,
1100                what: "ServiceGatewayInfo downloadTaps_count",
1101            });
1102        }
1103        let tap_count = bytes[pos] as usize;
1104        let dl_taps_start = pos;
1105        pos += SGI_DOWNLOAD_TAPS_COUNT_FIELD;
1106        for _ in 0..tap_count {
1107            let (_, next) = super::ior::Tap::parse_from(bytes, pos, end)?;
1108            pos = next;
1109        }
1110        let download_taps = &bytes[dl_taps_start..pos];
1111
1112        // serviceContextList (raw)
1113        let (service_context, next) = parse_service_context_list(bytes, pos, end)?;
1114        pos = next;
1115
1116        // userInfoLength (2 bytes, 16-bit) + userInfo_data
1117        if pos + SGI_USER_INFO_LEN_FIELD > end {
1118            return Err(Error::BufferTooShort {
1119                need: pos + SGI_USER_INFO_LEN_FIELD,
1120                have: end,
1121                what: "ServiceGatewayInfo userInfoLength",
1122            });
1123        }
1124        let ui_len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
1125        pos += SGI_USER_INFO_LEN_FIELD;
1126        if pos + ui_len > end {
1127            return Err(Error::SectionLengthOverflow {
1128                declared: ui_len,
1129                available: end - pos,
1130            });
1131        }
1132        let user_info = &bytes[pos..pos + ui_len];
1133
1134        Ok(ServiceGatewayInfo {
1135            ior,
1136            download_taps,
1137            service_context,
1138            user_info,
1139        })
1140    }
1141
1142    /// Serialize to an owned byte vector.  The result MUST equal the original
1143    /// `dsi.private_data` bytes byte-for-byte.
1144    pub fn to_bytes(&self) -> Vec<u8> {
1145        let len = self.ior.serialized_len()
1146            + self.download_taps.len()
1147            + self.service_context.len()
1148            + SGI_USER_INFO_LEN_FIELD
1149            + self.user_info.len();
1150        let mut buf = vec![0u8; len];
1151        let mut pos = 0;
1152        let written = self
1153            .ior
1154            .serialize_into(&mut buf[pos..])
1155            .expect("IOR serialize");
1156        pos += written;
1157        buf[pos..pos + self.download_taps.len()].copy_from_slice(self.download_taps);
1158        pos += self.download_taps.len();
1159        buf[pos..pos + self.service_context.len()].copy_from_slice(self.service_context);
1160        pos += self.service_context.len();
1161        buf[pos..pos + 2].copy_from_slice(&(self.user_info.len() as u16).to_be_bytes());
1162        pos += SGI_USER_INFO_LEN_FIELD;
1163        buf[pos..pos + self.user_info.len()].copy_from_slice(self.user_info);
1164        buf
1165    }
1166}
1167
1168// ── Tests ─────────────────────────────────────────────────────────────────────
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173    use crate::carousel::biop::BINDING_NOBJECT;
1174    use dvb_common::Parse;
1175
1176    /// Build a simple FileMessage around a buffer of bytes.
1177    fn sample_file_message(key: &'static [u8], content: &'static [u8]) -> BiopMessage<'static> {
1178        BiopMessage::File(FileMessage {
1179            object_key: key,
1180            content_size: content.len() as u64,
1181            object_info_extra: &[],
1182            service_context: &[0x00], // count=0
1183            content,
1184        })
1185    }
1186
1187    /// Build a minimal DirectoryMessage.
1188    fn sample_dir_message() -> BiopMessage<'static> {
1189        use crate::carousel::biop::ior::{
1190            BiopProfileBody, ConnBinder, ObjectLocation, TaggedProfile,
1191        };
1192        let ior = crate::carousel::biop::ior::Ior {
1193            type_id: b"fil\0",
1194            profiles: vec![TaggedProfile::Biop(BiopProfileBody {
1195                object_location: ObjectLocation {
1196                    carousel_id: 0xAB,
1197                    module_id: 2,
1198                    version_major: 1,
1199                    version_minor: 0,
1200                    object_key: &[0x02],
1201                },
1202                conn_binder: ConnBinder { taps: vec![] },
1203                extra: vec![],
1204            })],
1205        };
1206        BiopMessage::Directory(DirectoryMessage {
1207            object_kind: *b"dir\0",
1208            object_key: &[0x01],
1209            object_info: &[],
1210            service_context: &[0x00], // count=0
1211            bindings: vec![Binding {
1212                name: vec![NameComponent {
1213                    id: b"index.html",
1214                    kind: b"fil\0",
1215                }],
1216                binding_type: BINDING_NOBJECT,
1217                ior,
1218                object_info: &[],
1219            }],
1220        })
1221    }
1222
1223    #[test]
1224    fn file_message_round_trip() {
1225        let content: &[u8] = b"Hello, BIOP!";
1226        let msg = sample_file_message(&[0x01], content);
1227        let mut buf = vec![0u8; msg.serialized_len()];
1228        msg.serialize_into(&mut buf).unwrap();
1229        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
1230        assert_eq!(consumed, buf.len());
1231        assert_eq!(parsed, msg);
1232        // byte-exact re-serialize
1233        let mut buf2 = vec![0u8; parsed.serialized_len()];
1234        parsed.serialize_into(&mut buf2).unwrap();
1235        assert_eq!(buf, buf2);
1236    }
1237
1238    #[test]
1239    fn directory_message_round_trip() {
1240        let msg = sample_dir_message();
1241        let mut buf = vec![0u8; msg.serialized_len()];
1242        msg.serialize_into(&mut buf).unwrap();
1243        let (parsed, consumed) = BiopMessage::parse_at(&buf).unwrap();
1244        assert_eq!(consumed, buf.len());
1245        assert_eq!(parsed, msg);
1246        let mut buf2 = vec![0u8; parsed.serialized_len()];
1247        parsed.serialize_into(&mut buf2).unwrap();
1248        assert_eq!(buf, buf2, "Directory message byte-exact re-serialize");
1249    }
1250
1251    #[test]
1252    fn module_info_round_trip() {
1253        use crate::carousel::biop::ior::Tap;
1254        let info = ModuleInfo {
1255            module_timeout: 0x00FFFFFF,
1256            block_timeout: 0x00FFFFFF,
1257            min_block_time: 0x00000064,
1258            taps: vec![Tap {
1259                id: 0,
1260                use_: 0x0017,
1261                association_tag: 0x0042,
1262                selector: &[],
1263            }],
1264            user_info: &[],
1265        };
1266        let mut buf = vec![0u8; info.serialized_len()];
1267        info.serialize_into(&mut buf).unwrap();
1268        let parsed = ModuleInfo::parse(&buf).unwrap();
1269        assert_eq!(parsed, info);
1270        let mut buf2 = vec![0u8; parsed.serialized_len()];
1271        parsed.serialize_into(&mut buf2).unwrap();
1272        assert_eq!(buf, buf2, "ModuleInfo byte-exact re-serialize");
1273    }
1274
1275    #[test]
1276    fn module_info_byte_anchor() {
1277        use crate::carousel::biop::ior::Tap;
1278        // Hand-built ModuleInfo:
1279        //   moduleTimeout=0x000F4240, blockTimeout=0x000F4240, minBlockTime=0x00000064
1280        //   taps_count=1: id=0, use=0x0017, assoc=0x47, selector_length=0
1281        //   UserInfoLength=0
1282        #[rustfmt::skip]
1283        let expected: &[u8] = &[
1284            0x00, 0x0F, 0x42, 0x40, // moduleTimeout
1285            0x00, 0x0F, 0x42, 0x40, // blockTimeout
1286            0x00, 0x00, 0x00, 0x64, // minBlockTime
1287            0x01,                   // taps_count=1
1288            0x00, 0x00,             // id=0
1289            0x00, 0x17,             // use=0x0017
1290            0x00, 0x47,             // assoc=0x47
1291            0x00,                   // selector_length=0
1292            0x00,                   // UserInfoLength=0
1293        ];
1294        let info = ModuleInfo {
1295            module_timeout: 0x000F4240,
1296            block_timeout: 0x000F4240,
1297            min_block_time: 0x00000064,
1298            taps: vec![Tap {
1299                id: 0,
1300                use_: 0x0017,
1301                association_tag: 0x0047,
1302                selector: &[],
1303            }],
1304            user_info: &[],
1305        };
1306        let mut buf = vec![0u8; info.serialized_len()];
1307        info.serialize_into(&mut buf).unwrap();
1308        assert_eq!(buf.as_slice(), expected);
1309        let parsed = ModuleInfo::parse(expected).unwrap();
1310        assert_eq!(parsed, info);
1311    }
1312
1313    #[test]
1314    fn sgi_byte_anchor_m6() {
1315        // The 64-byte SGI private_data from the m6 broadcast capture.
1316        // Independently parsed in the py script above.
1317        #[rustfmt::skip]
1318        let raw: &[u8] = &[
1319            0x00, 0x00, 0x00, 0x04,  // type_id_length=4
1320            0x73, 0x72, 0x67, 0x00,  // type_id="srg\0"
1321            0x00, 0x00, 0x00, 0x01,  // taggedProfiles_count=1
1322            0x49, 0x53, 0x4F, 0x06,  // TAG_BIOP
1323            0x00, 0x00, 0x00, 0x28,  // profile_data_length=40
1324            0x00, 0x02,              // byte_order=0, liteComponents_count=2
1325            0x49, 0x53, 0x4F, 0x50, 0x0A, // TAG_ObjectLocation, len=10
1326            0x00, 0x00, 0x00, 0xAB,  // carouselId=0xAB
1327            0x00, 0x01,              // moduleId=1
1328            0x01, 0x00,              // version 1.0
1329            0x01, 0x01,              // objectKey_length=1, objectKey=0x01
1330            0x49, 0x53, 0x4F, 0x40, 0x12, // TAG_ConnBinder, len=18
1331            0x01,                    // taps_count=1
1332            0x00, 0x00,              // tap id=0
1333            0x00, 0x16,              // use=0x0016
1334            0x00, 0x47,              // association_tag=0x47
1335            0x0A,                    // selector_length=10
1336            0x00, 0x01, 0x80, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF,
1337            0x00,                    // downloadTaps_count=0
1338            0x00,                    // serviceContextList_count=0
1339            0x00, 0x00,              // userInfoLength=0
1340        ];
1341        assert_eq!(raw.len(), 64);
1342
1343        let sgi = ServiceGatewayInfo::parse(raw).unwrap();
1344
1345        // IOR assertions
1346        assert_eq!(sgi.ior.type_id, b"srg\0");
1347        assert_eq!(sgi.ior.profiles.len(), 1);
1348        let bp = sgi.ior.biop_profile().unwrap();
1349        assert_eq!(bp.object_location.carousel_id, 0xAB);
1350        assert_eq!(bp.object_location.module_id, 1);
1351        assert_eq!(bp.object_location.version_major, 1);
1352        assert_eq!(bp.object_location.version_minor, 0);
1353        assert_eq!(bp.object_location.object_key, &[0x01]);
1354        assert_eq!(bp.conn_binder.taps.len(), 1);
1355        let tap = &bp.conn_binder.taps[0];
1356        assert_eq!(tap.use_, 0x0016);
1357        assert_eq!(tap.association_tag, 0x47);
1358        assert_eq!(tap.transaction_id(), Some(0x80000002));
1359        assert_eq!(tap.timeout(), Some(0xFFFFFFFF));
1360
1361        // Byte-exact round-trip
1362        let out = sgi.to_bytes();
1363        assert_eq!(out.len(), 64, "SGI serialized length");
1364        assert_eq!(out.as_slice(), raw, "SGI byte-exact round-trip");
1365    }
1366
1367    #[cfg(feature = "serde")]
1368    #[test]
1369    fn biop_serde_round_trip() {
1370        let content: &[u8] = b"test content";
1371        let msg = sample_file_message(&[0x01], content);
1372        let json = serde_json::to_string(&msg).unwrap();
1373        assert!(json.contains("content_size"));
1374    }
1375
1376    #[cfg(feature = "flate2")]
1377    #[test]
1378    fn zlib_round_trip() {
1379        use flate2::{write::ZlibEncoder, Compression};
1380        use std::io::Write;
1381
1382        let original = b"Hello, compressed BIOP world! ".repeat(10);
1383        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1384        encoder.write_all(&original).unwrap();
1385        let compressed = encoder.finish().unwrap();
1386
1387        let decompressed = decompress_zlib(&compressed).unwrap();
1388        assert_eq!(decompressed.as_slice(), original.as_slice());
1389    }
1390}