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