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
12use crate::error::{Error, Result};
13use dvb_common::{Parse, Serialize};
14
15/// `protocolDiscriminator` — always 0x11 for MPEG-2 DSM-CC.
16pub const PROTOCOL_DISCRIMINATOR: u8 = 0x11;
17/// `dsmccType` for U-N download messages (§7.2: 0x03).
18pub const DSMCC_TYPE_UN_DOWNLOAD: u8 = 0x03;
19/// `messageId` of DownloadInfoIndication.
20pub const MESSAGE_ID_DII: u16 = 0x1002;
21/// `messageId` of DownloadDataBlock.
22pub const MESSAGE_ID_DDB: u16 = 0x1003;
23/// `messageId` of DownloadServerInitiate.
24pub const MESSAGE_ID_DSI: u16 = 0x1006;
25
26/// Bytes of dsmccMessageHeader / dsmccDownloadDataHeader before the
27/// adaptation header: pd(1) + type(1) + messageId(2) + transactionId-or-
28/// downloadId(4) + reserved(1) + adaptationLength(1) + messageLength(2).
29const MESSAGE_HEADER_LEN: usize = 12;
30/// serverId is a fixed 20-byte field in the DSI (DVB: all 0xFF).
31const SERVER_ID_LEN: usize = 20;
32/// 16-bit compatibilityDescriptorLength field.
33const COMPAT_LEN_FIELD: usize = 2;
34/// 16-bit privateDataLength field.
35const PRIVATE_LEN_FIELD: usize = 2;
36/// Fixed DII body bytes before the compatibilityDescriptor: downloadId(4) +
37/// blockSize(2) + windowSize(1) + ackPeriod(1) + tCDownloadWindow(4) +
38/// tCDownloadScenario(4).
39const DII_FIXED_LEN: usize = 16;
40/// Per-module fixed bytes: moduleId(2) + moduleSize(4) + moduleVersion(1) +
41/// moduleInfoLength(1).
42const MODULE_HEADER_LEN: usize = 8;
43/// DDB body bytes before blockData: moduleId(2) + moduleVersion(1) +
44/// reserved(1) + blockNumber(2).
45const DDB_FIXED_LEN: usize = 6;
46
47/// DownloadServerInitiate (§7.3.6, messageId 0x1006).
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub struct Dsi<'a> {
51    /// 32-bit transactionId. DVB (TR 101 202 §4.7.9): the 2 LSBs are 0x0000
52    /// for a DSI; bit 31 toggles on update.
53    pub transaction_id: u32,
54    /// Raw dsmccAdaptationHeader bytes (usually empty).
55    #[cfg_attr(feature = "serde", serde(borrow))]
56    pub adaptation: &'a [u8],
57    /// 20-byte serverId — all 0xFF under the DVB profile.
58    pub server_id: [u8; SERVER_ID_LEN],
59    /// compatibilityDescriptor() body after its 16-bit length field, raw
60    /// (TS 102 006 Table 15 documents the structure).
61    #[cfg_attr(feature = "serde", serde(borrow))]
62    pub compatibility_descriptor: &'a [u8],
63    /// privateData, raw. SSU: GroupInfoIndication (TS 102 006 Table 6);
64    /// object carousel: ServiceGatewayInfo (TR 101 202 Table 4.15).
65    #[cfg_attr(feature = "serde", serde(borrow))]
66    pub private_data: &'a [u8],
67}
68
69/// One module entry in a DII (§7.3.3).
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct DiiModule<'a> {
73    /// moduleId referenced by DDB messages.
74    pub module_id: u16,
75    /// Total module size in bytes.
76    pub module_size: u32,
77    /// moduleVersion; DDBs must match.
78    pub module_version: u8,
79    /// moduleInfo, raw (object carousel: BIOP::ModuleInfo, TR 101 202
80    /// Table 4.14).
81    #[cfg_attr(feature = "serde", serde(borrow))]
82    pub module_info: &'a [u8],
83}
84
85/// DownloadInfoIndication (§7.3.3, messageId 0x1002).
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
89pub struct Dii<'a> {
90    /// 32-bit transactionId (TR 101 202 Table 4.1 sub-fields).
91    pub transaction_id: u32,
92    /// Raw dsmccAdaptationHeader bytes (usually empty).
93    #[cfg_attr(feature = "serde", serde(borrow))]
94    pub adaptation: &'a [u8],
95    /// downloadId — links this DII to its DDB messages.
96    pub download_id: u32,
97    /// Bytes per DDB block (every block except possibly the last).
98    pub block_size: u16,
99    /// windowSize — 0 under the DVB profile.
100    pub window_size: u8,
101    /// ackPeriod — 0 under the DVB profile.
102    pub ack_period: u8,
103    /// tCDownloadWindow — 0 under the DVB profile.
104    pub t_c_download_window: u32,
105    /// tCDownloadScenario.
106    pub t_c_download_scenario: u32,
107    /// compatibilityDescriptor() body after its 16-bit length field, raw.
108    #[cfg_attr(feature = "serde", serde(borrow))]
109    pub compatibility_descriptor: &'a [u8],
110    /// Module entries in wire order.
111    pub modules: Vec<DiiModule<'a>>,
112    /// privateData, raw.
113    #[cfg_attr(feature = "serde", serde(borrow))]
114    pub private_data: &'a [u8],
115}
116
117/// A U-N download control message — payload of a table_id 0x3B DSM-CC
118/// section, discriminated by `messageId`.
119#[derive(Debug, Clone, PartialEq, Eq)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
122pub enum UnMessage<'a> {
123    /// DownloadServerInitiate (messageId 0x1006).
124    Dsi(#[cfg_attr(feature = "serde", serde(borrow))] Dsi<'a>),
125    /// DownloadInfoIndication (messageId 0x1002).
126    Dii(#[cfg_attr(feature = "serde", serde(borrow))] Dii<'a>),
127}
128
129/// DownloadDataBlock (§7.3.7.1, messageId 0x1003) — payload of a table_id
130/// 0x3C DSM-CC section, including its dsmccDownloadDataHeader.
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
133pub struct DownloadDataBlock<'a> {
134    /// downloadId from the dsmccDownloadDataHeader — matches the DII.
135    pub download_id: u32,
136    /// Raw dsmccAdaptationHeader bytes (usually empty).
137    #[cfg_attr(feature = "serde", serde(borrow))]
138    pub adaptation: &'a [u8],
139    /// moduleId of the module this block belongs to.
140    pub module_id: u16,
141    /// moduleVersion — must match the DII module entry.
142    pub module_version: u8,
143    /// Block index; byte offset within the module = blockNumber × blockSize.
144    pub block_number: u16,
145    /// The block payload.
146    #[cfg_attr(feature = "serde", serde(borrow))]
147    pub block_data: &'a [u8],
148}
149
150/// Parse the 12-byte dsmccMessageHeader / dsmccDownloadDataHeader common
151/// shape. Returns (messageId, transaction_or_download_id, adaptation,
152/// payload) where `payload` is bounded by `messageLength`.
153fn parse_header<'a>(bytes: &'a [u8], what: &'static str) -> Result<(u16, u32, &'a [u8], &'a [u8])> {
154    if bytes.len() < MESSAGE_HEADER_LEN {
155        return Err(Error::BufferTooShort {
156            need: MESSAGE_HEADER_LEN,
157            have: bytes.len(),
158            what,
159        });
160    }
161    if bytes[0] != PROTOCOL_DISCRIMINATOR {
162        return Err(Error::ReservedBitsViolation {
163            field: "protocolDiscriminator",
164            reason: "must be 0x11 (ISO/IEC 13818-6 §7.2)",
165        });
166    }
167    if bytes[1] != DSMCC_TYPE_UN_DOWNLOAD {
168        return Err(Error::ReservedBitsViolation {
169            field: "dsmccType",
170            reason: "must be 0x03 — U-N download (ISO/IEC 13818-6 §7.2)",
171        });
172    }
173    let message_id = u16::from_be_bytes([bytes[2], bytes[3]]);
174    let id = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
175    let adaptation_length = bytes[9] as usize;
176    let message_length = u16::from_be_bytes([bytes[10], bytes[11]]) as usize;
177    let total = MESSAGE_HEADER_LEN + message_length;
178    if bytes.len() < total {
179        return Err(Error::SectionLengthOverflow {
180            declared: message_length,
181            available: bytes.len() - MESSAGE_HEADER_LEN,
182        });
183    }
184    if adaptation_length > message_length {
185        return Err(Error::SectionLengthOverflow {
186            declared: adaptation_length,
187            available: message_length,
188        });
189    }
190    let adaptation = &bytes[MESSAGE_HEADER_LEN..MESSAGE_HEADER_LEN + adaptation_length];
191    let payload = &bytes[MESSAGE_HEADER_LEN + adaptation_length..total];
192    Ok((message_id, id, adaptation, payload))
193}
194
195/// Serialize the common 12-byte header followed by the adaptation bytes.
196/// `payload_len` is the body length after the adaptation header.
197fn serialize_header(
198    buf: &mut [u8],
199    message_id: u16,
200    id: u32,
201    adaptation: &[u8],
202    payload_len: usize,
203) -> Result<usize> {
204    let message_length = adaptation.len() + payload_len;
205    if adaptation.len() > u8::MAX as usize {
206        return Err(Error::SectionLengthOverflow {
207            declared: adaptation.len(),
208            available: u8::MAX as usize,
209        });
210    }
211    if message_length > u16::MAX as usize {
212        return Err(Error::SectionLengthOverflow {
213            declared: message_length,
214            available: u16::MAX as usize,
215        });
216    }
217    buf[0] = PROTOCOL_DISCRIMINATOR;
218    buf[1] = DSMCC_TYPE_UN_DOWNLOAD;
219    buf[2..4].copy_from_slice(&message_id.to_be_bytes());
220    buf[4..8].copy_from_slice(&id.to_be_bytes());
221    buf[8] = 0xFF; // reserved
222    buf[9] = adaptation.len() as u8;
223    buf[10..12].copy_from_slice(&(message_length as u16).to_be_bytes());
224    buf[MESSAGE_HEADER_LEN..MESSAGE_HEADER_LEN + adaptation.len()].copy_from_slice(adaptation);
225    Ok(MESSAGE_HEADER_LEN + adaptation.len())
226}
227
228/// Read a 16-bit-length-prefixed slice at `pos`, bounds-checked against `end`.
229fn length_prefixed(bytes: &[u8], pos: usize, end: usize) -> Result<(&[u8], usize)> {
230    if pos + 2 > end {
231        return Err(Error::BufferTooShort {
232            need: pos + 2,
233            have: end,
234            what: "DSM-CC 16-bit length field",
235        });
236    }
237    let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
238    let start = pos + 2;
239    if start + len > end {
240        return Err(Error::SectionLengthOverflow {
241            declared: len,
242            available: end - start,
243        });
244    }
245    Ok((&bytes[start..start + len], start + len))
246}
247
248impl<'a> Parse<'a> for UnMessage<'a> {
249    type Error = crate::error::Error;
250
251    fn parse(bytes: &'a [u8]) -> Result<Self> {
252        let (message_id, transaction_id, adaptation, payload) =
253            parse_header(bytes, "UnMessage header")?;
254        let end = payload.len();
255        match message_id {
256            MESSAGE_ID_DSI => {
257                if end < SERVER_ID_LEN + COMPAT_LEN_FIELD + PRIVATE_LEN_FIELD {
258                    return Err(Error::BufferTooShort {
259                        need: SERVER_ID_LEN + COMPAT_LEN_FIELD + PRIVATE_LEN_FIELD,
260                        have: end,
261                        what: "Dsi body",
262                    });
263                }
264                let mut server_id = [0u8; SERVER_ID_LEN];
265                server_id.copy_from_slice(&payload[..SERVER_ID_LEN]);
266                let (compatibility_descriptor, pos) = length_prefixed(payload, SERVER_ID_LEN, end)?;
267                let (private_data, _pos) = length_prefixed(payload, pos, end)?;
268                Ok(UnMessage::Dsi(Dsi {
269                    transaction_id,
270                    adaptation,
271                    server_id,
272                    compatibility_descriptor,
273                    private_data,
274                }))
275            }
276            MESSAGE_ID_DII => {
277                if end < DII_FIXED_LEN + COMPAT_LEN_FIELD {
278                    return Err(Error::BufferTooShort {
279                        need: DII_FIXED_LEN + COMPAT_LEN_FIELD,
280                        have: end,
281                        what: "Dii body",
282                    });
283                }
284                let download_id =
285                    u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
286                let block_size = u16::from_be_bytes([payload[4], payload[5]]);
287                let window_size = payload[6];
288                let ack_period = payload[7];
289                let t_c_download_window =
290                    u32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
291                let t_c_download_scenario =
292                    u32::from_be_bytes([payload[12], payload[13], payload[14], payload[15]]);
293                let (compatibility_descriptor, mut pos) =
294                    length_prefixed(payload, DII_FIXED_LEN, end)?;
295                if pos + 2 > end {
296                    return Err(Error::BufferTooShort {
297                        need: pos + 2,
298                        have: end,
299                        what: "Dii numberOfModules",
300                    });
301                }
302                let number_of_modules =
303                    u16::from_be_bytes([payload[pos], payload[pos + 1]]) as usize;
304                pos += 2;
305                let mut modules = Vec::with_capacity(number_of_modules.min(256));
306                for _ in 0..number_of_modules {
307                    if pos + MODULE_HEADER_LEN > end {
308                        return Err(Error::BufferTooShort {
309                            need: pos + MODULE_HEADER_LEN,
310                            have: end,
311                            what: "Dii module entry",
312                        });
313                    }
314                    let module_id = u16::from_be_bytes([payload[pos], payload[pos + 1]]);
315                    let module_size = u32::from_be_bytes([
316                        payload[pos + 2],
317                        payload[pos + 3],
318                        payload[pos + 4],
319                        payload[pos + 5],
320                    ]);
321                    let module_version = payload[pos + 6];
322                    let module_info_length = payload[pos + 7] as usize;
323                    let info_start = pos + MODULE_HEADER_LEN;
324                    if info_start + module_info_length > end {
325                        return Err(Error::SectionLengthOverflow {
326                            declared: module_info_length,
327                            available: end - info_start,
328                        });
329                    }
330                    modules.push(DiiModule {
331                        module_id,
332                        module_size,
333                        module_version,
334                        module_info: &payload[info_start..info_start + module_info_length],
335                    });
336                    pos = info_start + module_info_length;
337                }
338                let (private_data, _pos) = length_prefixed(payload, pos, end)?;
339                Ok(UnMessage::Dii(Dii {
340                    transaction_id,
341                    adaptation,
342                    download_id,
343                    block_size,
344                    window_size,
345                    ack_period,
346                    t_c_download_window,
347                    t_c_download_scenario,
348                    compatibility_descriptor,
349                    modules,
350                    private_data,
351                }))
352            }
353            _ => Err(Error::ReservedBitsViolation {
354                field: "messageId",
355                reason: "expected 0x1002 (DII) or 0x1006 (DSI) on table_id 0x3B \
356                         (ISO/IEC 13818-6 §7.3)",
357            }),
358        }
359    }
360}
361
362impl Serialize for UnMessage<'_> {
363    type Error = crate::error::Error;
364
365    fn serialized_len(&self) -> usize {
366        match self {
367            UnMessage::Dsi(dsi) => {
368                MESSAGE_HEADER_LEN
369                    + dsi.adaptation.len()
370                    + SERVER_ID_LEN
371                    + COMPAT_LEN_FIELD
372                    + dsi.compatibility_descriptor.len()
373                    + PRIVATE_LEN_FIELD
374                    + dsi.private_data.len()
375            }
376            UnMessage::Dii(dii) => {
377                MESSAGE_HEADER_LEN
378                    + dii.adaptation.len()
379                    + DII_FIXED_LEN
380                    + COMPAT_LEN_FIELD
381                    + dii.compatibility_descriptor.len()
382                    + 2 // numberOfModules
383                    + dii
384                        .modules
385                        .iter()
386                        .map(|m| MODULE_HEADER_LEN + m.module_info.len())
387                        .sum::<usize>()
388                    + PRIVATE_LEN_FIELD
389                    + dii.private_data.len()
390            }
391        }
392    }
393
394    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
395        let len = self.serialized_len();
396        if buf.len() < len {
397            return Err(Error::OutputBufferTooSmall {
398                need: len,
399                have: buf.len(),
400            });
401        }
402        match self {
403            UnMessage::Dsi(dsi) => {
404                let payload_len = len - MESSAGE_HEADER_LEN - dsi.adaptation.len();
405                let mut pos = serialize_header(
406                    buf,
407                    MESSAGE_ID_DSI,
408                    dsi.transaction_id,
409                    dsi.adaptation,
410                    payload_len,
411                )?;
412                buf[pos..pos + SERVER_ID_LEN].copy_from_slice(&dsi.server_id);
413                pos += SERVER_ID_LEN;
414                pos = put_length_prefixed(buf, pos, dsi.compatibility_descriptor)?;
415                put_length_prefixed(buf, pos, dsi.private_data)?;
416            }
417            UnMessage::Dii(dii) => {
418                let payload_len = len - MESSAGE_HEADER_LEN - dii.adaptation.len();
419                let mut pos = serialize_header(
420                    buf,
421                    MESSAGE_ID_DII,
422                    dii.transaction_id,
423                    dii.adaptation,
424                    payload_len,
425                )?;
426                buf[pos..pos + 4].copy_from_slice(&dii.download_id.to_be_bytes());
427                buf[pos + 4..pos + 6].copy_from_slice(&dii.block_size.to_be_bytes());
428                buf[pos + 6] = dii.window_size;
429                buf[pos + 7] = dii.ack_period;
430                buf[pos + 8..pos + 12].copy_from_slice(&dii.t_c_download_window.to_be_bytes());
431                buf[pos + 12..pos + 16].copy_from_slice(&dii.t_c_download_scenario.to_be_bytes());
432                pos += DII_FIXED_LEN;
433                pos = put_length_prefixed(buf, pos, dii.compatibility_descriptor)?;
434                if dii.modules.len() > u16::MAX as usize {
435                    return Err(Error::SectionLengthOverflow {
436                        declared: dii.modules.len(),
437                        available: u16::MAX as usize,
438                    });
439                }
440                buf[pos..pos + 2].copy_from_slice(&(dii.modules.len() as u16).to_be_bytes());
441                pos += 2;
442                for m in &dii.modules {
443                    if m.module_info.len() > u8::MAX as usize {
444                        return Err(Error::SectionLengthOverflow {
445                            declared: m.module_info.len(),
446                            available: u8::MAX as usize,
447                        });
448                    }
449                    buf[pos..pos + 2].copy_from_slice(&m.module_id.to_be_bytes());
450                    buf[pos + 2..pos + 6].copy_from_slice(&m.module_size.to_be_bytes());
451                    buf[pos + 6] = m.module_version;
452                    buf[pos + 7] = m.module_info.len() as u8;
453                    pos += MODULE_HEADER_LEN;
454                    buf[pos..pos + m.module_info.len()].copy_from_slice(m.module_info);
455                    pos += m.module_info.len();
456                }
457                put_length_prefixed(buf, pos, dii.private_data)?;
458            }
459        }
460        Ok(len)
461    }
462}
463
464/// Write a 16-bit length then the slice; returns the new position.
465fn put_length_prefixed(buf: &mut [u8], pos: usize, data: &[u8]) -> Result<usize> {
466    if data.len() > u16::MAX as usize {
467        return Err(Error::SectionLengthOverflow {
468            declared: data.len(),
469            available: u16::MAX as usize,
470        });
471    }
472    buf[pos..pos + 2].copy_from_slice(&(data.len() as u16).to_be_bytes());
473    buf[pos + 2..pos + 2 + data.len()].copy_from_slice(data);
474    Ok(pos + 2 + data.len())
475}
476
477impl<'a> Parse<'a> for DownloadDataBlock<'a> {
478    type Error = crate::error::Error;
479
480    fn parse(bytes: &'a [u8]) -> Result<Self> {
481        let (message_id, download_id, adaptation, payload) =
482            parse_header(bytes, "DownloadDataBlock header")?;
483        if message_id != MESSAGE_ID_DDB {
484            return Err(Error::ReservedBitsViolation {
485                field: "messageId",
486                reason: "expected 0x1003 (DDB) on table_id 0x3C (ISO/IEC 13818-6 §7.3.7)",
487            });
488        }
489        if payload.len() < DDB_FIXED_LEN {
490            return Err(Error::BufferTooShort {
491                need: DDB_FIXED_LEN,
492                have: payload.len(),
493                what: "DownloadDataBlock body",
494            });
495        }
496        Ok(DownloadDataBlock {
497            download_id,
498            adaptation,
499            module_id: u16::from_be_bytes([payload[0], payload[1]]),
500            module_version: payload[2],
501            block_number: u16::from_be_bytes([payload[4], payload[5]]),
502            block_data: &payload[DDB_FIXED_LEN..],
503        })
504    }
505}
506
507impl Serialize for DownloadDataBlock<'_> {
508    type Error = crate::error::Error;
509
510    fn serialized_len(&self) -> usize {
511        MESSAGE_HEADER_LEN + self.adaptation.len() + DDB_FIXED_LEN + self.block_data.len()
512    }
513
514    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
515        let len = self.serialized_len();
516        if buf.len() < len {
517            return Err(Error::OutputBufferTooSmall {
518                need: len,
519                have: buf.len(),
520            });
521        }
522        let payload_len = DDB_FIXED_LEN + self.block_data.len();
523        let pos = serialize_header(
524            buf,
525            MESSAGE_ID_DDB,
526            self.download_id,
527            self.adaptation,
528            payload_len,
529        )?;
530        buf[pos..pos + 2].copy_from_slice(&self.module_id.to_be_bytes());
531        buf[pos + 2] = self.module_version;
532        buf[pos + 3] = 0xFF; // reserved
533        buf[pos + 4..pos + 6].copy_from_slice(&self.block_number.to_be_bytes());
534        buf[pos + DDB_FIXED_LEN..pos + DDB_FIXED_LEN + self.block_data.len()]
535            .copy_from_slice(self.block_data);
536        Ok(len)
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    fn sample_dsi() -> UnMessage<'static> {
545        UnMessage::Dsi(Dsi {
546            transaction_id: 0x8000_0000,
547            adaptation: &[],
548            server_id: [0xFF; 20],
549            compatibility_descriptor: &[],
550            private_data: &[0x0A, 0x0B],
551        })
552    }
553
554    fn sample_dii() -> UnMessage<'static> {
555        UnMessage::Dii(Dii {
556            transaction_id: 0x8002_0002,
557            adaptation: &[],
558            download_id: 0x0000_00AB,
559            block_size: 4066,
560            window_size: 0,
561            ack_period: 0,
562            t_c_download_window: 0,
563            t_c_download_scenario: 0,
564            compatibility_descriptor: &[],
565            modules: vec![
566                DiiModule {
567                    module_id: 1,
568                    module_size: 8000,
569                    module_version: 3,
570                    module_info: &[0xDE, 0xAD],
571                },
572                DiiModule {
573                    module_id: 2,
574                    module_size: 100,
575                    module_version: 1,
576                    module_info: &[],
577                },
578            ],
579            private_data: &[],
580        })
581    }
582
583    #[test]
584    fn dsi_round_trip() {
585        let msg = sample_dsi();
586        let mut buf = vec![0u8; msg.serialized_len()];
587        msg.serialize_into(&mut buf).unwrap();
588        assert_eq!(UnMessage::parse(&buf).unwrap(), msg);
589    }
590
591    #[test]
592    fn dii_round_trip() {
593        let msg = sample_dii();
594        let mut buf = vec![0u8; msg.serialized_len()];
595        msg.serialize_into(&mut buf).unwrap();
596        assert_eq!(UnMessage::parse(&buf).unwrap(), msg);
597    }
598
599    #[test]
600    fn ddb_round_trip() {
601        let ddb = DownloadDataBlock {
602            download_id: 0xAB,
603            adaptation: &[],
604            module_id: 1,
605            module_version: 3,
606            block_number: 2,
607            block_data: &[0x55; 64],
608        };
609        let mut buf = vec![0u8; ddb.serialized_len()];
610        ddb.serialize_into(&mut buf).unwrap();
611        assert_eq!(DownloadDataBlock::parse(&buf).unwrap(), ddb);
612    }
613
614    #[test]
615    fn header_fields_on_wire() {
616        let msg = sample_dsi();
617        let mut buf = vec![0u8; msg.serialized_len()];
618        msg.serialize_into(&mut buf).unwrap();
619        assert_eq!(buf[0], 0x11); // protocolDiscriminator
620        assert_eq!(buf[1], 0x03); // dsmccType
621        assert_eq!(u16::from_be_bytes([buf[2], buf[3]]), MESSAGE_ID_DSI);
622        assert_eq!(buf[8], 0xFF); // reserved
623                                  // messageLength = bytes after the 12-byte header
624        let ml = u16::from_be_bytes([buf[10], buf[11]]) as usize;
625        assert_eq!(ml, buf.len() - 12);
626    }
627
628    #[test]
629    fn parse_rejects_wrong_protocol_discriminator() {
630        let msg = sample_dsi();
631        let mut buf = vec![0u8; msg.serialized_len()];
632        msg.serialize_into(&mut buf).unwrap();
633        buf[0] = 0x12;
634        assert!(matches!(
635            UnMessage::parse(&buf).unwrap_err(),
636            Error::ReservedBitsViolation {
637                field: "protocolDiscriminator",
638                ..
639            }
640        ));
641    }
642
643    #[test]
644    fn parse_rejects_unknown_message_id() {
645        let msg = sample_dsi();
646        let mut buf = vec![0u8; msg.serialized_len()];
647        msg.serialize_into(&mut buf).unwrap();
648        buf[2] = 0x10;
649        buf[3] = 0x01; // 0x1001 DownloadInfoRequest — not valid broadcast-side
650        assert!(matches!(
651            UnMessage::parse(&buf).unwrap_err(),
652            Error::ReservedBitsViolation {
653                field: "messageId",
654                ..
655            }
656        ));
657    }
658
659    #[test]
660    fn parse_rejects_short_buffer() {
661        assert!(matches!(
662            UnMessage::parse(&[0x11, 0x03]).unwrap_err(),
663            Error::BufferTooShort { .. }
664        ));
665    }
666
667    #[test]
668    fn parse_rejects_message_length_overflow() {
669        let msg = sample_dsi();
670        let mut buf = vec![0u8; msg.serialized_len()];
671        msg.serialize_into(&mut buf).unwrap();
672        buf[10] = 0xFF;
673        buf[11] = 0xFF; // declared messageLength way past the buffer
674        assert!(matches!(
675            UnMessage::parse(&buf).unwrap_err(),
676            Error::SectionLengthOverflow { .. }
677        ));
678    }
679
680    #[test]
681    fn dii_module_info_overflow_rejected() {
682        let msg = sample_dii();
683        let mut buf = vec![0u8; msg.serialized_len()];
684        msg.serialize_into(&mut buf).unwrap();
685        // First module's moduleInfoLength is at header(12) + fixed(16) +
686        // compatLen(2) + numberOfModules(2) + moduleHeader-1 = byte 39.
687        buf[39] = 0xFF;
688        assert!(matches!(
689            UnMessage::parse(&buf).unwrap_err(),
690            Error::SectionLengthOverflow { .. }
691        ));
692    }
693
694    #[test]
695    fn ddb_rejects_un_message_id() {
696        let msg = sample_dsi();
697        let mut buf = vec![0u8; msg.serialized_len()];
698        msg.serialize_into(&mut buf).unwrap();
699        assert!(matches!(
700            DownloadDataBlock::parse(&buf).unwrap_err(),
701            Error::ReservedBitsViolation {
702                field: "messageId",
703                ..
704            }
705        ));
706    }
707
708    #[test]
709    fn adaptation_bytes_round_trip() {
710        let ddb = DownloadDataBlock {
711            download_id: 1,
712            adaptation: &[0x01, 0x02, 0x03],
713            module_id: 9,
714            module_version: 0,
715            block_number: 0,
716            block_data: &[0xAA],
717        };
718        let mut buf = vec![0u8; ddb.serialized_len()];
719        ddb.serialize_into(&mut buf).unwrap();
720        assert_eq!(buf[9], 3); // adaptationLength
721        assert_eq!(DownloadDataBlock::parse(&buf).unwrap(), ddb);
722    }
723
724    #[cfg(feature = "serde")]
725    #[test]
726    fn dii_serializes_to_valid_json() {
727        let msg = sample_dii();
728        let j = serde_json::to_string(&msg).unwrap();
729        assert!(j.contains("\"download_id\":171"));
730        assert!(j.contains("\"block_size\":4066"));
731    }
732}