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