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