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