1use crate::error::{Error, Result};
13use dvb_common::{Parse, Serialize};
14
15pub const PROTOCOL_DISCRIMINATOR: u8 = 0x11;
17pub const DSMCC_TYPE_UN_DOWNLOAD: u8 = 0x03;
19pub const MESSAGE_ID_DII: u16 = 0x1002;
21pub const MESSAGE_ID_DDB: u16 = 0x1003;
23pub const MESSAGE_ID_DSI: u16 = 0x1006;
25
26const MESSAGE_HEADER_LEN: usize = 12;
30const SERVER_ID_LEN: usize = 20;
32const COMPAT_LEN_FIELD: usize = 2;
34const PRIVATE_LEN_FIELD: usize = 2;
36const DII_FIXED_LEN: usize = 16;
40const MODULE_HEADER_LEN: usize = 8;
43const DDB_FIXED_LEN: usize = 6;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub struct Dsi<'a> {
51 pub transaction_id: u32,
54 #[cfg_attr(feature = "serde", serde(borrow))]
56 pub adaptation: &'a [u8],
57 pub server_id: [u8; SERVER_ID_LEN],
59 #[cfg_attr(feature = "serde", serde(borrow))]
62 pub compatibility_descriptor: &'a [u8],
63 #[cfg_attr(feature = "serde", serde(borrow))]
66 pub private_data: &'a [u8],
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct DiiModule<'a> {
73 pub module_id: u16,
75 pub module_size: u32,
77 pub module_version: u8,
79 #[cfg_attr(feature = "serde", serde(borrow))]
82 pub module_info: &'a [u8],
83}
84
85#[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 pub transaction_id: u32,
92 #[cfg_attr(feature = "serde", serde(borrow))]
94 pub adaptation: &'a [u8],
95 pub download_id: u32,
97 pub block_size: u16,
99 pub window_size: u8,
101 pub ack_period: u8,
103 pub t_c_download_window: u32,
105 pub t_c_download_scenario: u32,
107 #[cfg_attr(feature = "serde", serde(borrow))]
109 pub compatibility_descriptor: &'a [u8],
110 pub modules: Vec<DiiModule<'a>>,
112 #[cfg_attr(feature = "serde", serde(borrow))]
114 pub private_data: &'a [u8],
115}
116
117#[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 Dsi(#[cfg_attr(feature = "serde", serde(borrow))] Dsi<'a>),
125 Dii(#[cfg_attr(feature = "serde", serde(borrow))] Dii<'a>),
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
133pub struct DownloadDataBlock<'a> {
134 pub download_id: u32,
136 #[cfg_attr(feature = "serde", serde(borrow))]
138 pub adaptation: &'a [u8],
139 pub module_id: u16,
141 pub module_version: u8,
143 pub block_number: u16,
145 #[cfg_attr(feature = "serde", serde(borrow))]
147 pub block_data: &'a [u8],
148}
149
150fn 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
195fn 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; 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
228fn 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 + 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
464fn 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; 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); assert_eq!(buf[1], 0x03); assert_eq!(u16::from_be_bytes([buf[2], buf[3]]), MESSAGE_ID_DSI);
622 assert_eq!(buf[8], 0xFF); 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; 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; 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 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); 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}