Skip to main content

dvb_si/carousel/
messages.rs

1//! DSM-CC U-N download protocol messages — ISO/IEC 13818-6 §7.2/§7.3.
2//!
3//! Layouts per `dvb-si/docs/text/iso_13818_6/` (hand-transcribed; ISO/IEC
4//! 13818-6 is not freely redistributable), cross-checked against the vendored
5//! TR 101 202 §4.6/§4.7.5 + TS 102 006 Table 15, and pinned live against the
6//! `m6-single.ts` capture by the `carousel_fixture` integration test.
7//!
8//! Control messages (DSI/DII) are the payload of DSM-CC sections with
9//! table_id 0x3B; data messages (DDB) of table_id 0x3C — see
10//! [`crate::tables::dsmcc`] for the section framing.
11//!
12//! # SSU DSI `privateData`
13//!
14//! When a DSI is part of a DVB System Software Update carousel
15//! (TS 102 006 §8.1.1), its `private_data` field carries a
16//! [`GroupInfoIndication`] (Table 6). Parse it with
17//! `GroupInfoIndication::parse(dsi.private_data)`.
18
19use crate::compatibility::CompatibilityDescriptor;
20use crate::descriptors::carousel::CarouselDescriptorLoop;
21use crate::error::{Error, Result};
22use alloc::vec::Vec;
23use broadcast_common::{Parse, Serialize};
24
25/// `protocolDiscriminator` — always 0x11 for MPEG-2 DSM-CC.
26pub const PROTOCOL_DISCRIMINATOR: u8 = 0x11;
27/// `dsmccType` for U-N download messages (§7.2: 0x03).
28pub const DSMCC_TYPE_UN_DOWNLOAD: u8 = 0x03;
29/// `messageId` of DownloadInfoIndication.
30pub const MESSAGE_ID_DII: u16 = 0x1002;
31/// `messageId` of DownloadDataBlock.
32pub const MESSAGE_ID_DDB: u16 = 0x1003;
33/// `messageId` of DownloadServerInitiate.
34pub const MESSAGE_ID_DSI: u16 = 0x1006;
35
36/// Bytes of dsmccMessageHeader / dsmccDownloadDataHeader before the
37/// adaptation header: pd(1) + type(1) + messageId(2) + transactionId-or-
38/// downloadId(4) + reserved(1) + adaptationLength(1) + messageLength(2).
39const MESSAGE_HEADER_LEN: usize = 12;
40/// serverId is a fixed 20-byte field in the DSI (DVB: all 0xFF).
41const SERVER_ID_LEN: usize = 20;
42/// 16-bit privateDataLength field.
43const PRIVATE_LEN_FIELD: usize = 2;
44/// Fixed DII body bytes before the compatibilityDescriptor: downloadId(4) +
45/// blockSize(2) + windowSize(1) + ackPeriod(1) + tCDownloadWindow(4) +
46/// tCDownloadScenario(4).
47const DII_FIXED_LEN: usize = 16;
48/// Per-module fixed bytes: moduleId(2) + moduleSize(4) + moduleVersion(1) +
49/// moduleInfoLength(1).
50const MODULE_HEADER_LEN: usize = 8;
51/// DDB body bytes before blockData: moduleId(2) + moduleVersion(1) +
52/// reserved(1) + blockNumber(2).
53const DDB_FIXED_LEN: usize = 6;
54
55// ── GroupInfoIndication layout constants (TS 102 006 Table 6) ────────────────
56/// NumberOfGroups field: 2 bytes.
57const GII_NUMBER_OF_GROUPS_LEN: usize = 2;
58/// GroupId field: 4 bytes.
59const GII_GROUP_ID_LEN: usize = 4;
60/// GroupSize field: 4 bytes.
61const GII_GROUP_SIZE_LEN: usize = 4;
62/// GroupInfoLength field: 2 bytes.
63const GII_GROUP_INFO_LEN_FIELD: usize = 2;
64/// PrivateDataLength field: 2 bytes.
65const GII_PRIVATE_DATA_LEN_FIELD: usize = 2;
66
67/// One group entry in a [`GroupInfoIndication`] — TS 102 006 Table 6.
68#[derive(Debug, Clone, PartialEq, Eq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize))]
70pub struct GroupInfo<'a> {
71    /// `GroupId` — 4-byte group identifier.
72    pub group_id: u32,
73    /// `GroupSize` — total size of the SSU update group in bytes.
74    pub group_size: u32,
75    /// `GroupCompatibility` — compatibility descriptor for this group
76    /// (TS 102 006 Table 7 / Table 15).
77    pub group_compatibility: CompatibilityDescriptor<'a>,
78    /// `GroupInfoByte` loop — application-specific per-group data.
79    #[cfg_attr(feature = "serde", serde(borrow))]
80    pub group_info: &'a [u8],
81    /// `PrivateDataByte` loop — private extension bytes.
82    #[cfg_attr(feature = "serde", serde(borrow))]
83    pub private_data: &'a [u8],
84}
85
86impl<'a> GroupInfo<'a> {
87    /// The `GroupInfoByte` loop walked as DVB data-carousel descriptors
88    /// (EN 301 192 §10.2.1 Table 48: type, name, info, location,
89    /// est_download_time, group_link, subgroup_association). Never panics;
90    /// unallocated tags surface as `Unknown`.
91    #[must_use]
92    pub const fn descriptors(&self) -> CarouselDescriptorLoop<'a> {
93        CarouselDescriptorLoop::new(self.group_info)
94    }
95}
96
97/// GroupInfoIndication — TS 102 006 §8.1.1 Table 6.
98///
99/// Carried as the `privateData` payload of a DSI message
100/// ([`Dsi::private_data`]) when the carousel is an SSU update carousel
101/// (`data_broadcast_id = 0x000A`). It lists one entry per update group
102/// delivered in the carousel, with its compatibility requirements
103/// (hardware/software OUI constraints) and size.
104///
105/// Wire layout (bytes):
106///
107/// ```text
108/// GroupInfoIndication() {
109///   NumberOfGroups           2
110///   for (i=0; i<N; i++) {
111///     GroupId                4
112///     GroupSize              4
113///     GroupCompatibility     variable  (CompatibilityDescriptor — Table 7)
114///     GroupInfoLength        2
115///     for (j=0; j<M; j++) GroupInfoByte   1
116///     PrivateDataLength      2
117///     for (j=0; j<M; j++) PrivateDataByte 1
118///   }
119/// }
120/// ```
121#[derive(Debug, Clone, PartialEq, Eq)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize))]
123pub struct GroupInfoIndication<'a> {
124    /// Group entries in wire order.
125    pub groups: Vec<GroupInfo<'a>>,
126}
127
128impl<'a> Parse<'a> for GroupInfoIndication<'a> {
129    type Error = crate::error::Error;
130
131    fn parse(bytes: &'a [u8]) -> Result<Self> {
132        let (b, _) = bytes
133            .split_first_chunk::<2>()
134            .ok_or(Error::BufferTooShort {
135                need: GII_NUMBER_OF_GROUPS_LEN,
136                have: bytes.len(),
137                what: "GroupInfoIndication NumberOfGroups",
138            })?;
139        let number_of_groups = u16::from_be_bytes(*b) as usize;
140        let mut pos = GII_NUMBER_OF_GROUPS_LEN;
141        let end = bytes.len();
142        let mut groups = Vec::with_capacity(number_of_groups.min(256));
143
144        for _ in 0..number_of_groups {
145            // GroupId (4 bytes) + GroupSize (4 bytes)
146            let fixed = GII_GROUP_ID_LEN + GII_GROUP_SIZE_LEN;
147            let (hdr, _) =
148                bytes[pos..end]
149                    .split_first_chunk::<8>()
150                    .ok_or(Error::BufferTooShort {
151                        need: pos + fixed,
152                        have: end,
153                        what: "GroupInfo GroupId/GroupSize",
154                    })?;
155            let group_id = u32::from_be_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
156            let group_size = u32::from_be_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
157            pos += fixed;
158
159            // GroupCompatibility — CompatibilityDescriptor (Table 7).
160            // CompatibilityDescriptor::parse consumes exactly the declared
161            // compatibilityDescriptorLength + 2-byte length prefix.
162            use crate::compatibility::COMPAT_DESC_LEN_FIELD;
163            let (bc, _) =
164                bytes[pos..end]
165                    .split_first_chunk::<2>()
166                    .ok_or(Error::BufferTooShort {
167                        need: pos + COMPAT_DESC_LEN_FIELD,
168                        have: end,
169                        what: "GroupCompatibility length field",
170                    })?;
171            let compat_len = u16::from_be_bytes(*bc) as usize;
172            let compat_total = COMPAT_DESC_LEN_FIELD + compat_len;
173            if pos + compat_total > end {
174                return Err(Error::SectionLengthOverflow {
175                    declared: compat_len,
176                    available: end - pos - COMPAT_DESC_LEN_FIELD,
177                });
178            }
179            let group_compatibility =
180                CompatibilityDescriptor::parse(&bytes[pos..pos + compat_total])?;
181            pos += compat_total;
182
183            // GroupInfoLength + GroupInfoByte loop.
184            let (bgi, _) =
185                bytes[pos..end]
186                    .split_first_chunk::<2>()
187                    .ok_or(Error::BufferTooShort {
188                        need: pos + GII_GROUP_INFO_LEN_FIELD,
189                        have: end,
190                        what: "GroupInfo GroupInfoLength",
191                    })?;
192            let group_info_len = u16::from_be_bytes(*bgi) as usize;
193            pos += GII_GROUP_INFO_LEN_FIELD;
194            if pos + group_info_len > end {
195                return Err(Error::SectionLengthOverflow {
196                    declared: group_info_len,
197                    available: end - pos,
198                });
199            }
200            let group_info = &bytes[pos..pos + group_info_len];
201            pos += group_info_len;
202
203            // PrivateDataLength + PrivateDataByte loop.
204            let (bpd, _) =
205                bytes[pos..end]
206                    .split_first_chunk::<2>()
207                    .ok_or(Error::BufferTooShort {
208                        need: pos + GII_PRIVATE_DATA_LEN_FIELD,
209                        have: end,
210                        what: "GroupInfo PrivateDataLength",
211                    })?;
212            let private_data_len = u16::from_be_bytes(*bpd) as usize;
213            pos += GII_PRIVATE_DATA_LEN_FIELD;
214            if pos + private_data_len > end {
215                return Err(Error::SectionLengthOverflow {
216                    declared: private_data_len,
217                    available: end - pos,
218                });
219            }
220            let private_data = &bytes[pos..pos + private_data_len];
221            pos += private_data_len;
222
223            groups.push(GroupInfo {
224                group_id,
225                group_size,
226                group_compatibility,
227                group_info,
228                private_data,
229            });
230        }
231
232        Ok(GroupInfoIndication { groups })
233    }
234}
235
236impl Serialize for GroupInfoIndication<'_> {
237    type Error = crate::error::Error;
238
239    fn serialized_len(&self) -> usize {
240        GII_NUMBER_OF_GROUPS_LEN
241            + self
242                .groups
243                .iter()
244                .map(|g| {
245                    GII_GROUP_ID_LEN
246                        + GII_GROUP_SIZE_LEN
247                        + g.group_compatibility.serialized_len()
248                        + GII_GROUP_INFO_LEN_FIELD
249                        + g.group_info.len()
250                        + GII_PRIVATE_DATA_LEN_FIELD
251                        + g.private_data.len()
252                })
253                .sum::<usize>()
254    }
255
256    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
257        let len = self.serialized_len();
258        if buf.len() < len {
259            return Err(Error::OutputBufferTooSmall {
260                need: len,
261                have: buf.len(),
262            });
263        }
264        if self.groups.len() > u16::MAX as usize {
265            return Err(Error::SectionLengthOverflow {
266                declared: self.groups.len(),
267                available: u16::MAX as usize,
268            });
269        }
270        buf[0..2].copy_from_slice(
271            &broadcast_common::len::fit_u16(self.groups.len(), "self.groups")?.to_be_bytes(),
272        );
273        let mut pos = GII_NUMBER_OF_GROUPS_LEN;
274
275        for g in &self.groups {
276            buf[pos..pos + 4].copy_from_slice(&g.group_id.to_be_bytes());
277            buf[pos + 4..pos + 8].copy_from_slice(&g.group_size.to_be_bytes());
278            pos += GII_GROUP_ID_LEN + GII_GROUP_SIZE_LEN;
279
280            let written = g.group_compatibility.serialize_into(&mut buf[pos..])?;
281            pos += written;
282
283            if g.group_info.len() > u16::MAX as usize {
284                return Err(Error::SectionLengthOverflow {
285                    declared: g.group_info.len(),
286                    available: u16::MAX as usize,
287                });
288            }
289            buf[pos..pos + 2].copy_from_slice(
290                &broadcast_common::len::fit_u16(g.group_info.len(), "g.group_info")?.to_be_bytes(),
291            );
292            pos += GII_GROUP_INFO_LEN_FIELD;
293            buf[pos..pos + g.group_info.len()].copy_from_slice(g.group_info);
294            pos += g.group_info.len();
295
296            if g.private_data.len() > u16::MAX as usize {
297                return Err(Error::SectionLengthOverflow {
298                    declared: g.private_data.len(),
299                    available: u16::MAX as usize,
300                });
301            }
302            buf[pos..pos + 2].copy_from_slice(
303                &broadcast_common::len::fit_u16(g.private_data.len(), "g.private_data")?
304                    .to_be_bytes(),
305            );
306            pos += GII_PRIVATE_DATA_LEN_FIELD;
307            buf[pos..pos + g.private_data.len()].copy_from_slice(g.private_data);
308            pos += g.private_data.len();
309        }
310
311        Ok(len)
312    }
313}
314
315/// DownloadServerInitiate (§7.3.6, messageId 0x1006).
316#[derive(Debug, Clone, PartialEq, Eq)]
317#[cfg_attr(feature = "serde", derive(serde::Serialize))]
318pub struct Dsi<'a> {
319    /// 32-bit transactionId. DVB (TR 101 202 §4.7.9): the 2 LSBs are 0x0000
320    /// for a DSI; bit 31 toggles on update.
321    pub transaction_id: u32,
322    /// Raw dsmccAdaptationHeader bytes (usually empty).
323    pub adaptation: &'a [u8],
324    /// 20-byte serverId — all 0xFF under the DVB profile.
325    pub server_id: [u8; SERVER_ID_LEN],
326    /// compatibilityDescriptor() — TS 102 006 Table 15 / ISO/IEC 13818-6.
327    pub compatibility_descriptor: CompatibilityDescriptor<'a>,
328    /// privateData, raw. SSU: GroupInfoIndication (TS 102 006 Table 6);
329    /// object carousel: ServiceGatewayInfo (TR 101 202 Table 4.15).
330    pub private_data: &'a [u8],
331}
332
333/// One module entry in a DII (§7.3.3).
334#[derive(Debug, Clone, PartialEq, Eq)]
335#[cfg_attr(feature = "serde", derive(serde::Serialize))]
336pub struct DiiModule<'a> {
337    /// moduleId referenced by DDB messages.
338    pub module_id: u16,
339    /// Total module size in bytes.
340    pub module_size: u32,
341    /// moduleVersion; DDBs must match.
342    pub module_version: u8,
343    /// moduleInfo, raw (object carousel: BIOP::ModuleInfo, TR 101 202
344    /// Table 4.14).
345    pub module_info: &'a [u8],
346}
347
348impl<'a> DiiModule<'a> {
349    /// The `moduleInfo` of a plain DVB **data** carousel walked as data-carousel
350    /// descriptors (EN 301 192 §10.2.1 Table 48: type, name, info,
351    /// module_link, CRC32, location, est_download_time, compressed_module,
352    /// SSU_module_type). Valid when `moduleInfo` is just a descriptor loop; an
353    /// object carousel's `moduleInfo` is a BIOP `ModuleInfo` (timeouts, taps,
354    /// then a `userInfo` loop) — parse that with
355    /// [`ModuleInfo::parse`](crate::carousel::biop::message::ModuleInfo).
356    #[must_use]
357    pub const fn descriptors(&self) -> CarouselDescriptorLoop<'a> {
358        CarouselDescriptorLoop::new(self.module_info)
359    }
360}
361
362/// DownloadInfoIndication (§7.3.3, messageId 0x1002).
363#[derive(Debug, Clone, PartialEq, Eq)]
364#[cfg_attr(feature = "serde", derive(serde::Serialize))]
365pub struct Dii<'a> {
366    /// 32-bit transactionId (TR 101 202 Table 4.1 sub-fields).
367    pub transaction_id: u32,
368    /// Raw dsmccAdaptationHeader bytes (usually empty).
369    pub adaptation: &'a [u8],
370    /// downloadId — links this DII to its DDB messages.
371    pub download_id: u32,
372    /// Bytes per DDB block (every block except possibly the last).
373    pub block_size: u16,
374    /// windowSize — 0 under the DVB profile.
375    pub window_size: u8,
376    /// ackPeriod — 0 under the DVB profile.
377    pub ack_period: u8,
378    /// tCDownloadWindow — 0 under the DVB profile.
379    pub t_c_download_window: u32,
380    /// tCDownloadScenario.
381    pub t_c_download_scenario: u32,
382    /// compatibilityDescriptor() — TS 102 006 Table 15 / ISO/IEC 13818-6.
383    pub compatibility_descriptor: CompatibilityDescriptor<'a>,
384    /// Module entries in wire order.
385    pub modules: Vec<DiiModule<'a>>,
386    /// privateData, raw.
387    pub private_data: &'a [u8],
388}
389
390/// A U-N download control message — payload of a table_id 0x3B DSM-CC
391/// section, discriminated by `messageId`.
392#[derive(Debug, Clone, PartialEq, Eq)]
393#[cfg_attr(feature = "serde", derive(serde::Serialize))]
394#[non_exhaustive]
395pub enum UnMessage<'a> {
396    /// DownloadServerInitiate (messageId 0x1006).
397    Dsi(Dsi<'a>),
398    /// DownloadInfoIndication (messageId 0x1002).
399    Dii(Dii<'a>),
400}
401
402/// DownloadDataBlock (§7.3.7.1, messageId 0x1003) — payload of a table_id
403/// 0x3C DSM-CC section, including its dsmccDownloadDataHeader.
404#[derive(Debug, Clone, PartialEq, Eq)]
405#[cfg_attr(feature = "serde", derive(serde::Serialize))]
406pub struct DownloadDataBlock<'a> {
407    /// downloadId from the dsmccDownloadDataHeader — matches the DII.
408    pub download_id: u32,
409    /// Raw dsmccAdaptationHeader bytes (usually empty).
410    pub adaptation: &'a [u8],
411    /// moduleId of the module this block belongs to.
412    pub module_id: u16,
413    /// moduleVersion — must match the DII module entry.
414    pub module_version: u8,
415    /// Block index; byte offset within the module = blockNumber × blockSize.
416    pub block_number: u16,
417    /// The block payload.
418    pub block_data: &'a [u8],
419}
420
421/// Parse the 12-byte dsmccMessageHeader / dsmccDownloadDataHeader common
422/// shape. Returns (messageId, transaction_or_download_id, adaptation,
423/// payload) where `payload` is bounded by `messageLength`.
424fn parse_header<'a>(bytes: &'a [u8], what: &'static str) -> Result<(u16, u32, &'a [u8], &'a [u8])> {
425    let (hdr, _) =
426        bytes
427            .split_first_chunk::<MESSAGE_HEADER_LEN>()
428            .ok_or(Error::BufferTooShort {
429                need: MESSAGE_HEADER_LEN,
430                have: bytes.len(),
431                what,
432            })?;
433    if hdr[0] != PROTOCOL_DISCRIMINATOR {
434        return Err(Error::ReservedBitsViolation {
435            field: "protocolDiscriminator",
436            reason: "must be 0x11 (ISO/IEC 13818-6 §7.2)",
437        });
438    }
439    if hdr[1] != DSMCC_TYPE_UN_DOWNLOAD {
440        return Err(Error::ReservedBitsViolation {
441            field: "dsmccType",
442            reason: "must be 0x03 — U-N download (ISO/IEC 13818-6 §7.2)",
443        });
444    }
445    let message_id = u16::from_be_bytes([hdr[2], hdr[3]]);
446    let id = u32::from_be_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
447    let adaptation_length = hdr[9] as usize;
448    let message_length = u16::from_be_bytes([hdr[10], hdr[11]]) as usize;
449    let total = MESSAGE_HEADER_LEN + message_length;
450    if bytes.len() < total {
451        return Err(Error::SectionLengthOverflow {
452            declared: message_length,
453            available: bytes.len() - MESSAGE_HEADER_LEN,
454        });
455    }
456    if adaptation_length > message_length {
457        return Err(Error::SectionLengthOverflow {
458            declared: adaptation_length,
459            available: message_length,
460        });
461    }
462    let adaptation = &bytes[MESSAGE_HEADER_LEN..MESSAGE_HEADER_LEN + adaptation_length];
463    let payload = &bytes[MESSAGE_HEADER_LEN + adaptation_length..total];
464    Ok((message_id, id, adaptation, payload))
465}
466
467/// Serialize the common 12-byte header followed by the adaptation bytes.
468/// `payload_len` is the body length after the adaptation header.
469fn serialize_header(
470    buf: &mut [u8],
471    message_id: u16,
472    id: u32,
473    adaptation: &[u8],
474    payload_len: usize,
475) -> Result<usize> {
476    let message_length = adaptation.len() + payload_len;
477    if adaptation.len() > u8::MAX as usize {
478        return Err(Error::SectionLengthOverflow {
479            declared: adaptation.len(),
480            available: u8::MAX as usize,
481        });
482    }
483    if message_length > u16::MAX as usize {
484        return Err(Error::SectionLengthOverflow {
485            declared: message_length,
486            available: u16::MAX as usize,
487        });
488    }
489    buf[0] = PROTOCOL_DISCRIMINATOR;
490    buf[1] = DSMCC_TYPE_UN_DOWNLOAD;
491    buf[2..4].copy_from_slice(&message_id.to_be_bytes());
492    buf[4..8].copy_from_slice(&id.to_be_bytes());
493    buf[8] = 0xFF; // reserved
494    buf[9] = broadcast_common::len::fit_u8(adaptation.len(), "adaptation")?;
495    buf[10..12].copy_from_slice(&(message_length as u16).to_be_bytes());
496    buf[MESSAGE_HEADER_LEN..MESSAGE_HEADER_LEN + adaptation.len()].copy_from_slice(adaptation);
497    Ok(MESSAGE_HEADER_LEN + adaptation.len())
498}
499
500/// Read a 16-bit-length-prefixed slice at `pos`, bounds-checked against `end`.
501fn length_prefixed(bytes: &[u8], pos: usize, end: usize) -> Result<(&[u8], usize)> {
502    let (b, _) = bytes[pos..end]
503        .split_first_chunk::<2>()
504        .ok_or(Error::BufferTooShort {
505            need: pos + 2,
506            have: end,
507            what: "DSM-CC 16-bit length field",
508        })?;
509    let len = u16::from_be_bytes(*b) as usize;
510    let start = pos + 2;
511    if start + len > end {
512        return Err(Error::SectionLengthOverflow {
513            declared: len,
514            available: end - start,
515        });
516    }
517    Ok((&bytes[start..start + len], start + len))
518}
519
520/// Parse a compatibilityDescriptor() block at `offset` inside `payload` that
521/// ends at `end`. Returns the parsed descriptor and the position just past it.
522fn parse_compat_block(
523    payload: &[u8],
524    offset: usize,
525    end: usize,
526) -> Result<(CompatibilityDescriptor<'_>, usize)> {
527    use crate::compatibility::COMPAT_DESC_LEN_FIELD;
528    let (b, _) = payload[offset..]
529        .split_first_chunk::<2>()
530        .ok_or(Error::BufferTooShort {
531            need: offset + COMPAT_DESC_LEN_FIELD,
532            have: end,
533            what: "compatibilityDescriptor in DSM-CC message",
534        })?;
535    let compat_desc_len = u16::from_be_bytes(*b) as usize;
536    let compat_end = offset + COMPAT_DESC_LEN_FIELD + compat_desc_len;
537    if compat_end > end {
538        return Err(Error::SectionLengthOverflow {
539            declared: compat_desc_len,
540            available: end - offset - COMPAT_DESC_LEN_FIELD,
541        });
542    }
543    let cd = CompatibilityDescriptor::parse(&payload[offset..compat_end])?;
544    Ok((cd, compat_end))
545}
546
547impl<'a> Parse<'a> for UnMessage<'a> {
548    type Error = crate::error::Error;
549
550    fn parse(bytes: &'a [u8]) -> Result<Self> {
551        let (message_id, transaction_id, adaptation, payload) =
552            parse_header(bytes, "UnMessage header")?;
553        let end = payload.len();
554        match message_id {
555            MESSAGE_ID_DSI => {
556                if end < SERVER_ID_LEN {
557                    return Err(Error::BufferTooShort {
558                        need: SERVER_ID_LEN,
559                        have: end,
560                        what: "Dsi body",
561                    });
562                }
563                let mut server_id = [0u8; SERVER_ID_LEN];
564                server_id.copy_from_slice(&payload[..SERVER_ID_LEN]);
565                let (compatibility_descriptor, pos) =
566                    parse_compat_block(payload, SERVER_ID_LEN, end)?;
567                let (private_data, _pos) = length_prefixed(payload, pos, end)?;
568                Ok(UnMessage::Dsi(Dsi {
569                    transaction_id,
570                    adaptation,
571                    server_id,
572                    compatibility_descriptor,
573                    private_data,
574                }))
575            }
576            MESSAGE_ID_DII => {
577                let (dii_hdr, _) =
578                    payload
579                        .split_first_chunk::<DII_FIXED_LEN>()
580                        .ok_or(Error::BufferTooShort {
581                            need: DII_FIXED_LEN,
582                            have: end,
583                            what: "Dii body",
584                        })?;
585                let download_id =
586                    u32::from_be_bytes([dii_hdr[0], dii_hdr[1], dii_hdr[2], dii_hdr[3]]);
587                let block_size = u16::from_be_bytes([dii_hdr[4], dii_hdr[5]]);
588                let window_size = dii_hdr[6];
589                let ack_period = dii_hdr[7];
590                let t_c_download_window =
591                    u32::from_be_bytes([dii_hdr[8], dii_hdr[9], dii_hdr[10], dii_hdr[11]]);
592                let t_c_download_scenario =
593                    u32::from_be_bytes([dii_hdr[12], dii_hdr[13], dii_hdr[14], dii_hdr[15]]);
594                let (compatibility_descriptor, mut pos) =
595                    parse_compat_block(payload, DII_FIXED_LEN, end)?;
596                let (bnm, _) = payload
597                    .get(pos..)
598                    .and_then(|s| s.split_first_chunk::<2>())
599                    .ok_or(Error::BufferTooShort {
600                        need: pos + 2,
601                        have: end,
602                        what: "Dii numberOfModules",
603                    })?;
604                let number_of_modules = u16::from_be_bytes(*bnm) as usize;
605                pos += 2;
606                let mut modules = Vec::with_capacity(number_of_modules.min(256));
607                for _ in 0..number_of_modules {
608                    let (mhdr, _) = payload
609                        .get(pos..)
610                        .and_then(|s| s.split_first_chunk::<MODULE_HEADER_LEN>())
611                        .ok_or(Error::BufferTooShort {
612                            need: pos + MODULE_HEADER_LEN,
613                            have: end,
614                            what: "Dii module entry",
615                        })?;
616                    let module_id = u16::from_be_bytes([mhdr[0], mhdr[1]]);
617                    let module_size = u32::from_be_bytes([mhdr[2], mhdr[3], mhdr[4], mhdr[5]]);
618                    let module_version = mhdr[6];
619                    let module_info_length = mhdr[7] as usize;
620                    let info_start = pos + MODULE_HEADER_LEN;
621                    if info_start + module_info_length > end {
622                        return Err(Error::SectionLengthOverflow {
623                            declared: module_info_length,
624                            available: end - info_start,
625                        });
626                    }
627                    modules.push(DiiModule {
628                        module_id,
629                        module_size,
630                        module_version,
631                        module_info: &payload[info_start..info_start + module_info_length],
632                    });
633                    pos = info_start + module_info_length;
634                }
635                let (private_data, _pos) = length_prefixed(payload, pos, end)?;
636                Ok(UnMessage::Dii(Dii {
637                    transaction_id,
638                    adaptation,
639                    download_id,
640                    block_size,
641                    window_size,
642                    ack_period,
643                    t_c_download_window,
644                    t_c_download_scenario,
645                    compatibility_descriptor,
646                    modules,
647                    private_data,
648                }))
649            }
650            _ => Err(Error::ReservedBitsViolation {
651                field: "messageId",
652                reason: "expected 0x1002 (DII) or 0x1006 (DSI) on table_id 0x3B \
653                         (ISO/IEC 13818-6 §7.3)",
654            }),
655        }
656    }
657}
658
659impl Serialize for UnMessage<'_> {
660    type Error = crate::error::Error;
661
662    fn serialized_len(&self) -> usize {
663        match self {
664            UnMessage::Dsi(dsi) => {
665                MESSAGE_HEADER_LEN
666                    + dsi.adaptation.len()
667                    + SERVER_ID_LEN
668                    + dsi.compatibility_descriptor.serialized_len()
669                    + PRIVATE_LEN_FIELD
670                    + dsi.private_data.len()
671            }
672            UnMessage::Dii(dii) => {
673                MESSAGE_HEADER_LEN
674                    + dii.adaptation.len()
675                    + DII_FIXED_LEN
676                    + dii.compatibility_descriptor.serialized_len()
677                    + 2 // numberOfModules
678                    + dii
679                        .modules
680                        .iter()
681                        .map(|m| MODULE_HEADER_LEN + m.module_info.len())
682                        .sum::<usize>()
683                    + PRIVATE_LEN_FIELD
684                    + dii.private_data.len()
685            }
686        }
687    }
688
689    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
690        let len = self.serialized_len();
691        if buf.len() < len {
692            return Err(Error::OutputBufferTooSmall {
693                need: len,
694                have: buf.len(),
695            });
696        }
697        match self {
698            UnMessage::Dsi(dsi) => {
699                let payload_len = len - MESSAGE_HEADER_LEN - dsi.adaptation.len();
700                let mut pos = serialize_header(
701                    buf,
702                    MESSAGE_ID_DSI,
703                    dsi.transaction_id,
704                    dsi.adaptation,
705                    payload_len,
706                )?;
707                buf[pos..pos + SERVER_ID_LEN].copy_from_slice(&dsi.server_id);
708                pos += SERVER_ID_LEN;
709                let written = dsi
710                    .compatibility_descriptor
711                    .serialize_into(&mut buf[pos..])?;
712                pos += written;
713                put_length_prefixed(buf, pos, dsi.private_data)?;
714            }
715            UnMessage::Dii(dii) => {
716                let payload_len = len - MESSAGE_HEADER_LEN - dii.adaptation.len();
717                let mut pos = serialize_header(
718                    buf,
719                    MESSAGE_ID_DII,
720                    dii.transaction_id,
721                    dii.adaptation,
722                    payload_len,
723                )?;
724                buf[pos..pos + 4].copy_from_slice(&dii.download_id.to_be_bytes());
725                buf[pos + 4..pos + 6].copy_from_slice(&dii.block_size.to_be_bytes());
726                buf[pos + 6] = dii.window_size;
727                buf[pos + 7] = dii.ack_period;
728                buf[pos + 8..pos + 12].copy_from_slice(&dii.t_c_download_window.to_be_bytes());
729                buf[pos + 12..pos + 16].copy_from_slice(&dii.t_c_download_scenario.to_be_bytes());
730                pos += DII_FIXED_LEN;
731                let written = dii
732                    .compatibility_descriptor
733                    .serialize_into(&mut buf[pos..])?;
734                pos += written;
735                if dii.modules.len() > u16::MAX as usize {
736                    return Err(Error::SectionLengthOverflow {
737                        declared: dii.modules.len(),
738                        available: u16::MAX as usize,
739                    });
740                }
741                buf[pos..pos + 2].copy_from_slice(
742                    &broadcast_common::len::fit_u16(dii.modules.len(), "dii.modules")?
743                        .to_be_bytes(),
744                );
745                pos += 2;
746                for m in &dii.modules {
747                    if m.module_info.len() > u8::MAX as usize {
748                        return Err(Error::SectionLengthOverflow {
749                            declared: m.module_info.len(),
750                            available: u8::MAX as usize,
751                        });
752                    }
753                    buf[pos..pos + 2].copy_from_slice(&m.module_id.to_be_bytes());
754                    buf[pos + 2..pos + 6].copy_from_slice(&m.module_size.to_be_bytes());
755                    buf[pos + 6] = m.module_version;
756                    buf[pos + 7] =
757                        broadcast_common::len::fit_u8(m.module_info.len(), "m.module_info")?;
758                    pos += MODULE_HEADER_LEN;
759                    buf[pos..pos + m.module_info.len()].copy_from_slice(m.module_info);
760                    pos += m.module_info.len();
761                }
762                put_length_prefixed(buf, pos, dii.private_data)?;
763            }
764        }
765        Ok(len)
766    }
767}
768
769/// Write a 16-bit length then the slice; returns the new position.
770fn put_length_prefixed(buf: &mut [u8], pos: usize, data: &[u8]) -> Result<usize> {
771    if data.len() > u16::MAX as usize {
772        return Err(Error::SectionLengthOverflow {
773            declared: data.len(),
774            available: u16::MAX as usize,
775        });
776    }
777    buf[pos..pos + 2]
778        .copy_from_slice(&broadcast_common::len::fit_u16(data.len(), "data")?.to_be_bytes());
779    buf[pos + 2..pos + 2 + data.len()].copy_from_slice(data);
780    Ok(pos + 2 + data.len())
781}
782
783impl<'a> Parse<'a> for DownloadDataBlock<'a> {
784    type Error = crate::error::Error;
785
786    fn parse(bytes: &'a [u8]) -> Result<Self> {
787        let (message_id, download_id, adaptation, payload) =
788            parse_header(bytes, "DownloadDataBlock header")?;
789        if message_id != MESSAGE_ID_DDB {
790            return Err(Error::ReservedBitsViolation {
791                field: "messageId",
792                reason: "expected 0x1003 (DDB) on table_id 0x3C (ISO/IEC 13818-6 §7.3.7)",
793            });
794        }
795        let (ddb_hdr, _) =
796            payload
797                .split_first_chunk::<DDB_FIXED_LEN>()
798                .ok_or(Error::BufferTooShort {
799                    need: DDB_FIXED_LEN,
800                    have: payload.len(),
801                    what: "DownloadDataBlock body",
802                })?;
803        Ok(DownloadDataBlock {
804            download_id,
805            adaptation,
806            module_id: u16::from_be_bytes([ddb_hdr[0], ddb_hdr[1]]),
807            module_version: ddb_hdr[2],
808            block_number: u16::from_be_bytes([ddb_hdr[4], ddb_hdr[5]]),
809            block_data: &payload[DDB_FIXED_LEN..],
810        })
811    }
812}
813
814impl Serialize for DownloadDataBlock<'_> {
815    type Error = crate::error::Error;
816
817    fn serialized_len(&self) -> usize {
818        MESSAGE_HEADER_LEN + self.adaptation.len() + DDB_FIXED_LEN + self.block_data.len()
819    }
820
821    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
822        let len = self.serialized_len();
823        if buf.len() < len {
824            return Err(Error::OutputBufferTooSmall {
825                need: len,
826                have: buf.len(),
827            });
828        }
829        let payload_len = DDB_FIXED_LEN + self.block_data.len();
830        let pos = serialize_header(
831            buf,
832            MESSAGE_ID_DDB,
833            self.download_id,
834            self.adaptation,
835            payload_len,
836        )?;
837        buf[pos..pos + 2].copy_from_slice(&self.module_id.to_be_bytes());
838        buf[pos + 2] = self.module_version;
839        buf[pos + 3] = 0xFF; // reserved
840        buf[pos + 4..pos + 6].copy_from_slice(&self.block_number.to_be_bytes());
841        buf[pos + DDB_FIXED_LEN..pos + DDB_FIXED_LEN + self.block_data.len()]
842            .copy_from_slice(self.block_data);
843        Ok(len)
844    }
845}
846
847#[cfg(test)]
848mod tests {
849    use super::*;
850
851    fn sample_dsi() -> UnMessage<'static> {
852        UnMessage::Dsi(Dsi {
853            transaction_id: 0x8000_0000,
854            adaptation: &[],
855            server_id: [0xFF; 20],
856            compatibility_descriptor: CompatibilityDescriptor {
857                empty_count_present: false,
858                descriptors: vec![],
859            },
860            private_data: &[0x0A, 0x0B],
861        })
862    }
863
864    fn sample_dii() -> UnMessage<'static> {
865        UnMessage::Dii(Dii {
866            transaction_id: 0x8002_0002,
867            adaptation: &[],
868            download_id: 0x0000_00AB,
869            block_size: 4066,
870            window_size: 0,
871            ack_period: 0,
872            t_c_download_window: 0,
873            t_c_download_scenario: 0,
874            compatibility_descriptor: CompatibilityDescriptor {
875                empty_count_present: false,
876                descriptors: vec![],
877            },
878            modules: vec![
879                DiiModule {
880                    module_id: 1,
881                    module_size: 8000,
882                    module_version: 3,
883                    module_info: &[0xDE, 0xAD],
884                },
885                DiiModule {
886                    module_id: 2,
887                    module_size: 100,
888                    module_version: 1,
889                    module_info: &[],
890                },
891            ],
892            private_data: &[],
893        })
894    }
895
896    #[test]
897    fn dsi_round_trip() {
898        let msg = sample_dsi();
899        let mut buf = vec![0u8; msg.serialized_len()];
900        msg.serialize_into(&mut buf).unwrap();
901        assert_eq!(UnMessage::parse(&buf).unwrap(), msg);
902    }
903
904    #[test]
905    fn dii_round_trip() {
906        let msg = sample_dii();
907        let mut buf = vec![0u8; msg.serialized_len()];
908        msg.serialize_into(&mut buf).unwrap();
909        assert_eq!(UnMessage::parse(&buf).unwrap(), msg);
910    }
911
912    /// A non-empty `compatibilityDescriptor()` carrying one entry with a
913    /// sub-descriptor — exercises the full compat block in a DSI, not just the
914    /// empty `0x00 0x00` form the other tests use.
915    fn nonempty_compat() -> CompatibilityDescriptor<'static> {
916        CompatibilityDescriptor {
917            empty_count_present: false,
918            descriptors: vec![crate::compatibility::CompatibilityDescriptorEntry {
919                descriptor_type: crate::compatibility::DescriptorType::SystemHardware,
920                specifier_type: crate::compatibility::SpecifierType::IeeeOui,
921                specifier_data: [0x00, 0x15, 0x0A],
922                model: 0x1234,
923                version: 0x0001,
924                sub_descriptors: vec![crate::compatibility::SubDescriptor {
925                    sub_descriptor_type: crate::compatibility::SubDescriptorType::Unallocated(0x05),
926                    data: &[0xAA, 0xBB],
927                }],
928            }],
929        }
930    }
931
932    #[test]
933    fn dsi_with_compat_round_trip() {
934        let msg = UnMessage::Dsi(Dsi {
935            transaction_id: 0x8000_0000,
936            adaptation: &[],
937            server_id: [0xFF; 20],
938            compatibility_descriptor: nonempty_compat(),
939            private_data: &[0x0A, 0x0B],
940        });
941        let mut buf = vec![0u8; msg.serialized_len()];
942        msg.serialize_into(&mut buf).unwrap();
943        let re = UnMessage::parse(&buf).unwrap();
944        assert_eq!(re, msg);
945        let mut buf2 = vec![0u8; re.serialized_len()];
946        re.serialize_into(&mut buf2).unwrap();
947        assert_eq!(buf, buf2, "byte-exact re-serialize");
948    }
949
950    #[test]
951    fn dii_with_compat_round_trip() {
952        let msg = UnMessage::Dii(Dii {
953            transaction_id: 0x8002_0002,
954            adaptation: &[],
955            download_id: 0x0000_00AB,
956            block_size: 4066,
957            window_size: 0,
958            ack_period: 0,
959            t_c_download_window: 0,
960            t_c_download_scenario: 0,
961            compatibility_descriptor: nonempty_compat(),
962            modules: vec![DiiModule {
963                module_id: 1,
964                module_size: 8000,
965                module_version: 3,
966                module_info: &[0xDE, 0xAD],
967            }],
968            private_data: &[],
969        });
970        let mut buf = vec![0u8; msg.serialized_len()];
971        msg.serialize_into(&mut buf).unwrap();
972        let re = UnMessage::parse(&buf).unwrap();
973        assert_eq!(re, msg);
974        let mut buf2 = vec![0u8; re.serialized_len()];
975        re.serialize_into(&mut buf2).unwrap();
976        assert_eq!(buf, buf2, "byte-exact re-serialize");
977    }
978
979    #[test]
980    fn ddb_round_trip() {
981        let ddb = DownloadDataBlock {
982            download_id: 0xAB,
983            adaptation: &[],
984            module_id: 1,
985            module_version: 3,
986            block_number: 2,
987            block_data: &[0x55; 64],
988        };
989        let mut buf = vec![0u8; ddb.serialized_len()];
990        ddb.serialize_into(&mut buf).unwrap();
991        assert_eq!(DownloadDataBlock::parse(&buf).unwrap(), ddb);
992    }
993
994    #[test]
995    fn header_fields_on_wire() {
996        let msg = sample_dsi();
997        let mut buf = vec![0u8; msg.serialized_len()];
998        msg.serialize_into(&mut buf).unwrap();
999        assert_eq!(buf[0], 0x11); // protocolDiscriminator
1000        assert_eq!(buf[1], 0x03); // dsmccType
1001        assert_eq!(u16::from_be_bytes([buf[2], buf[3]]), MESSAGE_ID_DSI);
1002        assert_eq!(buf[8], 0xFF); // reserved
1003        // messageLength = bytes after the 12-byte header
1004        let ml = u16::from_be_bytes([buf[10], buf[11]]) as usize;
1005        assert_eq!(ml, buf.len() - 12);
1006    }
1007
1008    #[test]
1009    fn parse_rejects_wrong_protocol_discriminator() {
1010        let msg = sample_dsi();
1011        let mut buf = vec![0u8; msg.serialized_len()];
1012        msg.serialize_into(&mut buf).unwrap();
1013        buf[0] = 0x12;
1014        assert!(matches!(
1015            UnMessage::parse(&buf).unwrap_err(),
1016            Error::ReservedBitsViolation {
1017                field: "protocolDiscriminator",
1018                ..
1019            }
1020        ));
1021    }
1022
1023    #[test]
1024    fn parse_rejects_unknown_message_id() {
1025        let msg = sample_dsi();
1026        let mut buf = vec![0u8; msg.serialized_len()];
1027        msg.serialize_into(&mut buf).unwrap();
1028        buf[2] = 0x10;
1029        buf[3] = 0x01; // 0x1001 DownloadInfoRequest — not valid broadcast-side
1030        assert!(matches!(
1031            UnMessage::parse(&buf).unwrap_err(),
1032            Error::ReservedBitsViolation {
1033                field: "messageId",
1034                ..
1035            }
1036        ));
1037    }
1038
1039    #[test]
1040    fn parse_rejects_short_buffer() {
1041        assert!(matches!(
1042            UnMessage::parse(&[0x11, 0x03]).unwrap_err(),
1043            Error::BufferTooShort { .. }
1044        ));
1045    }
1046
1047    #[test]
1048    fn parse_rejects_message_length_overflow() {
1049        let msg = sample_dsi();
1050        let mut buf = vec![0u8; msg.serialized_len()];
1051        msg.serialize_into(&mut buf).unwrap();
1052        buf[10] = 0xFF;
1053        buf[11] = 0xFF; // declared messageLength way past the buffer
1054        assert!(matches!(
1055            UnMessage::parse(&buf).unwrap_err(),
1056            Error::SectionLengthOverflow { .. }
1057        ));
1058    }
1059
1060    #[test]
1061    fn dii_module_info_overflow_rejected() {
1062        let msg = sample_dii();
1063        let mut buf = vec![0u8; msg.serialized_len()];
1064        msg.serialize_into(&mut buf).unwrap();
1065        // First module's moduleInfoLength is at header(12) + fixed(16) +
1066        // compatLen(2) + numberOfModules(2) + moduleHeader-1 = byte 39.
1067        buf[39] = 0xFF;
1068        assert!(matches!(
1069            UnMessage::parse(&buf).unwrap_err(),
1070            Error::SectionLengthOverflow { .. }
1071        ));
1072    }
1073
1074    #[test]
1075    fn ddb_rejects_un_message_id() {
1076        let msg = sample_dsi();
1077        let mut buf = vec![0u8; msg.serialized_len()];
1078        msg.serialize_into(&mut buf).unwrap();
1079        assert!(matches!(
1080            DownloadDataBlock::parse(&buf).unwrap_err(),
1081            Error::ReservedBitsViolation {
1082                field: "messageId",
1083                ..
1084            }
1085        ));
1086    }
1087
1088    #[test]
1089    fn adaptation_bytes_round_trip() {
1090        let ddb = DownloadDataBlock {
1091            download_id: 1,
1092            adaptation: &[0x01, 0x02, 0x03],
1093            module_id: 9,
1094            module_version: 0,
1095            block_number: 0,
1096            block_data: &[0xAA],
1097        };
1098        let mut buf = vec![0u8; ddb.serialized_len()];
1099        ddb.serialize_into(&mut buf).unwrap();
1100        assert_eq!(buf[9], 3); // adaptationLength
1101        assert_eq!(DownloadDataBlock::parse(&buf).unwrap(), ddb);
1102    }
1103
1104    #[cfg(feature = "serde")]
1105    #[test]
1106    fn dii_serializes_to_valid_json() {
1107        let msg = sample_dii();
1108        let j = serde_json::to_string(&msg).unwrap();
1109        assert!(j.contains("\"download_id\":171"));
1110        assert!(j.contains("\"block_size\":4066"));
1111    }
1112
1113    // ── GroupInfoIndication tests ─────────────────────────────────────────────
1114
1115    /// Construct a minimal single-group GII (no group_info, no private_data,
1116    /// empty CompatibilityDescriptor).
1117    ///
1118    /// Hand-built wire layout (all offsets from byte 0 of the GII):
1119    ///
1120    /// ```text
1121    /// [0..2]  NumberOfGroups = 0x00 0x01 (1)
1122    /// [2..6]  GroupId        = 0x00 0x00 0x00 0x01
1123    /// [6..10] GroupSize      = 0x00 0x07 0xA1 0x20  (500 000 bytes)
1124    /// [10..12] CompatibilityDescriptorLength = 0x00 0x00 (empty)
1125    /// [12..14] GroupInfoLength = 0x00 0x02
1126    /// [14..16] GroupInfoByte  = 0xCA 0xFE
1127    /// [16..18] PrivateDataLength = 0x00 0x01
1128    /// [18]     PrivateDataByte   = 0xBB
1129    /// Total = 19 bytes
1130    /// ```
1131    fn sample_gii() -> GroupInfoIndication<'static> {
1132        GroupInfoIndication {
1133            groups: vec![GroupInfo {
1134                group_id: 0x0000_0001,
1135                group_size: 500_000,
1136                group_compatibility: CompatibilityDescriptor {
1137                    empty_count_present: false,
1138                    descriptors: vec![],
1139                },
1140                group_info: &[0xCA, 0xFE],
1141                private_data: &[0xBB],
1142            }],
1143        }
1144    }
1145
1146    #[test]
1147    fn gii_round_trip() {
1148        let gii = sample_gii();
1149        let mut buf = vec![0u8; gii.serialized_len()];
1150        gii.serialize_into(&mut buf).unwrap();
1151        let re = GroupInfoIndication::parse(&buf).unwrap();
1152        assert_eq!(re, gii);
1153        // Byte-identical re-serialize.
1154        let mut buf2 = vec![0u8; re.serialized_len()];
1155        re.serialize_into(&mut buf2).unwrap();
1156        assert_eq!(buf, buf2, "GII byte-exact re-serialize");
1157    }
1158
1159    /// Verify exact byte positions against the hand-computed layout comment in
1160    /// `sample_gii` — this test will catch a layout bug that a pure
1161    /// serialize→parse round-trip cannot.
1162    #[test]
1163    fn gii_hand_built_byte_anchor() {
1164        // NumberOfGroups=1, GroupId=1, GroupSize=500000=0x0007_A120,
1165        // CompatLen=0, GroupInfoLen=2, bytes CA FE,
1166        // PrivateDataLen=1, byte BB.
1167        #[rustfmt::skip]
1168        let expected: &[u8] = &[
1169            0x00, 0x01,             // NumberOfGroups = 1
1170            0x00, 0x00, 0x00, 0x01, // GroupId = 1
1171            0x00, 0x07, 0xA1, 0x20, // GroupSize = 500 000
1172            0x00, 0x00,             // CompatibilityDescriptorLength = 0 (empty)
1173            0x00, 0x02,             // GroupInfoLength = 2
1174            0xCA, 0xFE,             // GroupInfoByte × 2
1175            0x00, 0x01,             // PrivateDataLength = 1
1176            0xBB,                   // PrivateDataByte
1177        ];
1178        let gii = sample_gii();
1179        let mut buf = vec![0u8; gii.serialized_len()];
1180        gii.serialize_into(&mut buf).unwrap();
1181        assert_eq!(buf.as_slice(), expected);
1182        let re = GroupInfoIndication::parse(expected).unwrap();
1183        assert_eq!(re, gii);
1184    }
1185
1186    #[test]
1187    fn gii_empty_groups() {
1188        let gii = GroupInfoIndication { groups: vec![] };
1189        let mut buf = vec![0u8; gii.serialized_len()];
1190        gii.serialize_into(&mut buf).unwrap();
1191        assert_eq!(buf, &[0x00, 0x00]); // NumberOfGroups = 0
1192        let re = GroupInfoIndication::parse(&buf).unwrap();
1193        assert!(re.groups.is_empty());
1194    }
1195
1196    #[test]
1197    fn gii_with_compat_round_trip() {
1198        let gii = GroupInfoIndication {
1199            groups: vec![GroupInfo {
1200                group_id: 0xDEAD_BEEF,
1201                group_size: 0x0001_0000,
1202                group_compatibility: nonempty_compat(),
1203                group_info: &[0x01, 0x02, 0x03],
1204                private_data: &[],
1205            }],
1206        };
1207        let mut buf = vec![0u8; gii.serialized_len()];
1208        gii.serialize_into(&mut buf).unwrap();
1209        let re = GroupInfoIndication::parse(&buf).unwrap();
1210        assert_eq!(re, gii);
1211        let mut buf2 = vec![0u8; re.serialized_len()];
1212        re.serialize_into(&mut buf2).unwrap();
1213        assert_eq!(buf, buf2, "GII with compat byte-exact re-serialize");
1214    }
1215
1216    #[test]
1217    fn gii_parse_rejects_short_buffer() {
1218        assert!(matches!(
1219            GroupInfoIndication::parse(&[0x00]).unwrap_err(),
1220            Error::BufferTooShort { .. }
1221        ));
1222    }
1223
1224    #[test]
1225    fn gii_parse_rejects_truncated_group() {
1226        // NumberOfGroups=1 but only 3 bytes of body (needs at least 8 for id+size).
1227        let bytes = &[0x00, 0x01, 0x00, 0x00, 0x00];
1228        assert!(matches!(
1229            GroupInfoIndication::parse(bytes).unwrap_err(),
1230            Error::BufferTooShort { .. }
1231        ));
1232    }
1233
1234    #[cfg(feature = "serde")]
1235    #[test]
1236    fn gii_serde_round_trip() {
1237        let gii = sample_gii();
1238        let json = serde_json::to_string(&gii).unwrap();
1239        assert!(json.contains("\"group_id\":1"));
1240        assert!(json.contains("\"group_size\":500000"));
1241        assert!(json.contains("\"group_info\""));
1242    }
1243}