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