1use super::{
15 BIOP_DELIVERY_PARA_USE, BYTE_ORDER_BIG_ENDIAN, TAG_BIOP, TAG_CONN_BINDER, TAG_LITE_OPTIONS,
16 TAG_OBJECT_LOCATION, TAG_SERVICE_LOCATION,
17};
18use crate::error::{Error, Result};
19use alloc::vec::Vec;
20use broadcast_common::{Parse, Serialize};
21
22const IOR_FIXED_LEN: usize = 8;
26const PROFILE_HEADER_LEN: usize = 8;
28const BIOP_BODY_FIXED_LEN: usize = 2;
30const COMPONENT_HEADER_LEN: usize = 5;
32const OBJECT_LOCATION_FIXED_LEN: usize = 9;
34const CONN_BINDER_FIXED_LEN: usize = 1;
36const TAP_FIXED_LEN: usize = 7;
38const LITE_OPTIONS_BODY_FIXED_LEN: usize = 2;
40const SERVICE_LOCATION_COMP_HEADER_LEN: usize = 8;
42const SERVICE_DOMAIN_LEN_FIELD: usize = 1;
44const NSAP_ADDRESS_LEN: usize = 20;
46const _NSAP_FIELDS_LEN: usize = NSAP_ADDRESS_LEN; const NAMING_COUNT_LEN: usize = 4;
50const NAMING_FIELD_LEN: usize = 4;
52const INITIAL_CONTEXT_LEN_FIELD: usize = 4;
54
55#[derive(Debug, Clone, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize))]
61#[non_exhaustive]
62pub enum ObjectKind {
63 Directory,
65 File,
67 Stream,
69 ServiceGateway,
71 StreamEvent,
73 Unknown([u8; 4]),
75}
76
77impl ObjectKind {
78 pub fn from_bytes(b: [u8; 4]) -> Self {
80 match &b {
81 b"dir\0" => Self::Directory,
82 b"fil\0" => Self::File,
83 b"str\0" => Self::Stream,
84 b"srg\0" => Self::ServiceGateway,
85 b"ste\0" => Self::StreamEvent,
86 _ => Self::Unknown(b),
87 }
88 }
89
90 pub fn to_bytes(&self) -> [u8; 4] {
92 match self {
93 Self::Directory => *b"dir\0",
94 Self::File => *b"fil\0",
95 Self::Stream => *b"str\0",
96 Self::ServiceGateway => *b"srg\0",
97 Self::StreamEvent => *b"ste\0",
98 Self::Unknown(b) => *b,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize))]
109pub struct Tap<'a> {
110 pub id: u16,
112 pub use_: u16,
114 pub association_tag: u16,
116 #[cfg_attr(feature = "serde", serde(borrow))]
118 pub selector: &'a [u8],
119}
120
121impl<'a> Tap<'a> {
122 pub fn transaction_id(&self) -> Option<u32> {
128 if self.use_ == BIOP_DELIVERY_PARA_USE && self.selector.len() >= 10 {
129 let (chunk, _) = self.selector[2..].split_first_chunk::<4>()?;
130 Some(u32::from_be_bytes(*chunk))
131 } else {
132 None
133 }
134 }
135
136 pub fn timeout(&self) -> Option<u32> {
139 if self.use_ == BIOP_DELIVERY_PARA_USE && self.selector.len() >= 10 {
140 let (chunk, _) = self.selector[6..].split_first_chunk::<4>()?;
141 Some(u32::from_be_bytes(*chunk))
142 } else {
143 None
144 }
145 }
146
147 pub(crate) fn serialized_len(&self) -> usize {
148 TAP_FIXED_LEN + self.selector.len()
149 }
150
151 pub(crate) fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
152 let (tap_hdr, _) =
153 bytes[pos..end]
154 .split_first_chunk::<TAP_FIXED_LEN>()
155 .ok_or(Error::BufferTooShort {
156 need: pos + TAP_FIXED_LEN,
157 have: end,
158 what: "BIOP Tap fixed fields",
159 })?;
160 let id = u16::from_be_bytes([tap_hdr[0], tap_hdr[1]]);
161 let use_ = u16::from_be_bytes([tap_hdr[2], tap_hdr[3]]);
162 let association_tag = u16::from_be_bytes([tap_hdr[4], tap_hdr[5]]);
163 let selector_length = tap_hdr[6] as usize;
164 let data_start = pos + TAP_FIXED_LEN;
165 if data_start + selector_length > end {
166 return Err(Error::SectionLengthOverflow {
167 declared: selector_length,
168 available: end - data_start,
169 });
170 }
171 let selector = &bytes[data_start..data_start + selector_length];
172 Ok((
173 Tap {
174 id,
175 use_,
176 association_tag,
177 selector,
178 },
179 data_start + selector_length,
180 ))
181 }
182
183 pub(crate) fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
184 let len = self.serialized_len();
185 if buf.len() < len {
186 return Err(Error::OutputBufferTooSmall {
187 need: len,
188 have: buf.len(),
189 });
190 }
191 if self.selector.len() > u8::MAX as usize {
192 return Err(Error::SectionLengthOverflow {
193 declared: self.selector.len(),
194 available: u8::MAX as usize,
195 });
196 }
197 buf[0..2].copy_from_slice(&self.id.to_be_bytes());
198 buf[2..4].copy_from_slice(&self.use_.to_be_bytes());
199 buf[4..6].copy_from_slice(&self.association_tag.to_be_bytes());
200 buf[6] = self.selector.len() as u8;
201 buf[7..7 + self.selector.len()].copy_from_slice(self.selector);
202 Ok(len)
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize))]
212pub struct ObjectLocation<'a> {
213 pub carousel_id: u32,
215 pub module_id: u16,
217 pub version_major: u8,
219 pub version_minor: u8,
221 #[cfg_attr(feature = "serde", serde(borrow))]
223 pub object_key: &'a [u8],
224}
225
226impl<'a> ObjectLocation<'a> {
227 fn serialized_len(&self) -> usize {
228 OBJECT_LOCATION_FIXED_LEN + self.object_key.len()
229 }
230
231 fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
232 let (ol_hdr, _) = bytes[pos..end]
233 .split_first_chunk::<OBJECT_LOCATION_FIXED_LEN>()
234 .ok_or(Error::BufferTooShort {
235 need: pos + OBJECT_LOCATION_FIXED_LEN,
236 have: end,
237 what: "BIOP ObjectLocation fixed fields",
238 })?;
239 let carousel_id = u32::from_be_bytes([ol_hdr[0], ol_hdr[1], ol_hdr[2], ol_hdr[3]]);
240 let module_id = u16::from_be_bytes([ol_hdr[4], ol_hdr[5]]);
241 let version_major = ol_hdr[6];
242 let version_minor = ol_hdr[7];
243 let object_key_length = ol_hdr[8] as usize;
244 let data_start = pos + OBJECT_LOCATION_FIXED_LEN;
245 if data_start + object_key_length > end {
246 return Err(Error::SectionLengthOverflow {
247 declared: object_key_length,
248 available: end - data_start,
249 });
250 }
251 Ok((
252 ObjectLocation {
253 carousel_id,
254 module_id,
255 version_major,
256 version_minor,
257 object_key: &bytes[data_start..data_start + object_key_length],
258 },
259 data_start + object_key_length,
260 ))
261 }
262
263 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
264 let len = self.serialized_len();
265 if buf.len() < len {
266 return Err(Error::OutputBufferTooSmall {
267 need: len,
268 have: buf.len(),
269 });
270 }
271 if self.object_key.len() > u8::MAX as usize {
272 return Err(Error::SectionLengthOverflow {
273 declared: self.object_key.len(),
274 available: u8::MAX as usize,
275 });
276 }
277 buf[0..4].copy_from_slice(&self.carousel_id.to_be_bytes());
278 buf[4..6].copy_from_slice(&self.module_id.to_be_bytes());
279 buf[6] = self.version_major;
280 buf[7] = self.version_minor;
281 buf[8] = self.object_key.len() as u8;
282 buf[9..9 + self.object_key.len()].copy_from_slice(self.object_key);
283 Ok(len)
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize))]
293pub struct ConnBinder<'a> {
294 #[cfg_attr(feature = "serde", serde(borrow))]
296 pub taps: Vec<Tap<'a>>,
297}
298
299impl<'a> ConnBinder<'a> {
300 fn serialized_len(&self) -> usize {
301 CONN_BINDER_FIXED_LEN + self.taps.iter().map(|t| t.serialized_len()).sum::<usize>()
302 }
303
304 fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
305 if pos + CONN_BINDER_FIXED_LEN > end {
306 return Err(Error::BufferTooShort {
307 need: pos + CONN_BINDER_FIXED_LEN,
308 have: end,
309 what: "BIOP ConnBinder taps_count",
310 });
311 }
312 let taps_count = bytes[pos] as usize;
313 let mut cur = pos + CONN_BINDER_FIXED_LEN;
314 let mut taps = Vec::with_capacity(taps_count.min(16));
315 for _ in 0..taps_count {
316 let (tap, next) = Tap::parse_from(bytes, cur, end)?;
317 taps.push(tap);
318 cur = next;
319 }
320 Ok((ConnBinder { taps }, cur))
321 }
322
323 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
324 let len = self.serialized_len();
325 if buf.len() < len {
326 return Err(Error::OutputBufferTooSmall {
327 need: len,
328 have: buf.len(),
329 });
330 }
331 if self.taps.len() > u8::MAX as usize {
332 return Err(Error::SectionLengthOverflow {
333 declared: self.taps.len(),
334 available: u8::MAX as usize,
335 });
336 }
337 buf[0] = self.taps.len() as u8;
338 let mut pos = CONN_BINDER_FIXED_LEN;
339 for tap in &self.taps {
340 let written = tap.serialize_into_buf(&mut buf[pos..])?;
341 pos += written;
342 }
343 Ok(len)
344 }
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
352#[cfg_attr(feature = "serde", derive(serde::Serialize))]
353pub struct LiteComponent<'a> {
354 pub tag: u32,
356 #[cfg_attr(feature = "serde", serde(borrow))]
358 pub data: &'a [u8],
359}
360
361impl LiteComponent<'_> {
362 fn serialized_len(&self) -> usize {
363 COMPONENT_HEADER_LEN + self.data.len()
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
372#[cfg_attr(feature = "serde", derive(serde::Serialize))]
373pub struct BiopProfileBody<'a> {
374 #[cfg_attr(feature = "serde", serde(borrow))]
376 pub object_location: ObjectLocation<'a>,
377 pub conn_binder: ConnBinder<'a>,
379 pub extra: Vec<LiteComponent<'a>>,
381}
382
383impl<'a> BiopProfileBody<'a> {
384 fn parse_from(bytes: &'a [u8]) -> Result<Self> {
386 let end = bytes.len();
387 if end < BIOP_BODY_FIXED_LEN {
388 return Err(Error::BufferTooShort {
389 need: BIOP_BODY_FIXED_LEN,
390 have: end,
391 what: "BIOP Profile Body fixed fields",
392 });
393 }
394 let byte_order = bytes[0];
395 if byte_order != BYTE_ORDER_BIG_ENDIAN {
396 return Err(Error::ReservedBitsViolation {
397 field: "profile_data_byte_order",
398 reason: "must be 0x00 (big-endian) per DVB mandatory constraint (TR 101 202 §4.7.3.2)",
399 });
400 }
401 let lite_components_count = bytes[1] as usize;
402 if lite_components_count < 2 {
403 return Err(Error::ValueOutOfRange {
404 field: "liteComponents_count",
405 reason: "BIOP Profile Body must have at least 2 components (ObjectLocation + ConnBinder)",
406 });
407 }
408 let mut pos = BIOP_BODY_FIXED_LEN;
409
410 let (ch0, _) = bytes[pos..end]
412 .split_first_chunk::<COMPONENT_HEADER_LEN>()
413 .ok_or(Error::BufferTooShort {
414 need: pos + COMPONENT_HEADER_LEN,
415 have: end,
416 what: "BIOP ObjectLocation component header",
417 })?;
418 let comp0_tag = u32::from_be_bytes([ch0[0], ch0[1], ch0[2], ch0[3]]);
419 if comp0_tag != TAG_OBJECT_LOCATION {
420 return Err(Error::ReservedBitsViolation {
421 field: "componentId_tag[0]",
422 reason: "first liteComponent must be TAG_ObjectLocation (0x49534F50)",
423 });
424 }
425 let comp0_len = ch0[4] as usize;
426 pos += COMPONENT_HEADER_LEN;
427 let comp0_end = pos + comp0_len;
428 if comp0_end > end {
429 return Err(Error::SectionLengthOverflow {
430 declared: comp0_len,
431 available: end - pos,
432 });
433 }
434 let (object_location, _) = ObjectLocation::parse_from(bytes, pos, comp0_end)?;
435 pos = comp0_end;
436
437 let (ch1, _) = bytes[pos..end]
439 .split_first_chunk::<COMPONENT_HEADER_LEN>()
440 .ok_or(Error::BufferTooShort {
441 need: pos + COMPONENT_HEADER_LEN,
442 have: end,
443 what: "BIOP ConnBinder component header",
444 })?;
445 let comp1_tag = u32::from_be_bytes([ch1[0], ch1[1], ch1[2], ch1[3]]);
446 if comp1_tag != TAG_CONN_BINDER {
447 return Err(Error::ReservedBitsViolation {
448 field: "componentId_tag[1]",
449 reason: "second liteComponent must be TAG_ConnBinder (0x49534F40)",
450 });
451 }
452 let comp1_len = ch1[4] as usize;
453 pos += COMPONENT_HEADER_LEN;
454 let comp1_end = pos + comp1_len;
455 if comp1_end > end {
456 return Err(Error::SectionLengthOverflow {
457 declared: comp1_len,
458 available: end - pos,
459 });
460 }
461 let (conn_binder, _) = ConnBinder::parse_from(bytes, pos, comp1_end)?;
462 pos = comp1_end;
463
464 let extra_count = lite_components_count - 2;
466 let mut extra = Vec::with_capacity(extra_count.min(8));
467 for _ in 0..extra_count {
468 let (ch_ex, _) = bytes[pos..end]
469 .split_first_chunk::<COMPONENT_HEADER_LEN>()
470 .ok_or(Error::BufferTooShort {
471 need: pos + COMPONENT_HEADER_LEN,
472 have: end,
473 what: "BIOP extra liteComponent header",
474 })?;
475 let tag = u32::from_be_bytes([ch_ex[0], ch_ex[1], ch_ex[2], ch_ex[3]]);
476 let data_len = ch_ex[4] as usize;
477 pos += COMPONENT_HEADER_LEN;
478 if pos + data_len > end {
479 return Err(Error::SectionLengthOverflow {
480 declared: data_len,
481 available: end - pos,
482 });
483 }
484 extra.push(LiteComponent {
485 tag,
486 data: &bytes[pos..pos + data_len],
487 });
488 pos += data_len;
489 }
490
491 Ok(BiopProfileBody {
492 object_location,
493 conn_binder,
494 extra,
495 })
496 }
497
498 fn serialized_len(&self) -> usize {
499 let ol_len = self.object_location.serialized_len();
500 let cb_len = self.conn_binder.serialized_len();
501 let extra_len: usize = self.extra.iter().map(|c| c.serialized_len()).sum();
502 BIOP_BODY_FIXED_LEN
503 + COMPONENT_HEADER_LEN
504 + ol_len
505 + COMPONENT_HEADER_LEN
506 + cb_len
507 + extra_len
508 }
509
510 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
511 let len = self.serialized_len();
512 if buf.len() < len {
513 return Err(Error::OutputBufferTooSmall {
514 need: len,
515 have: buf.len(),
516 });
517 }
518 let total_components = 2 + self.extra.len();
519 if total_components > u8::MAX as usize {
520 return Err(Error::SectionLengthOverflow {
521 declared: total_components,
522 available: u8::MAX as usize,
523 });
524 }
525 buf[0] = BYTE_ORDER_BIG_ENDIAN;
526 buf[1] = total_components as u8;
527 let mut pos = BIOP_BODY_FIXED_LEN;
528
529 let ol_data_len = self.object_location.serialized_len();
531 if ol_data_len > u8::MAX as usize {
532 return Err(Error::SectionLengthOverflow {
533 declared: ol_data_len,
534 available: u8::MAX as usize,
535 });
536 }
537 buf[pos..pos + 4].copy_from_slice(&TAG_OBJECT_LOCATION.to_be_bytes());
538 buf[pos + 4] = ol_data_len as u8;
539 pos += COMPONENT_HEADER_LEN;
540 let written = self.object_location.serialize_into_buf(&mut buf[pos..])?;
541 pos += written;
542
543 let cb_data_len = self.conn_binder.serialized_len();
545 if cb_data_len > u8::MAX as usize {
546 return Err(Error::SectionLengthOverflow {
547 declared: cb_data_len,
548 available: u8::MAX as usize,
549 });
550 }
551 buf[pos..pos + 4].copy_from_slice(&TAG_CONN_BINDER.to_be_bytes());
552 buf[pos + 4] = cb_data_len as u8;
553 pos += COMPONENT_HEADER_LEN;
554 let written = self.conn_binder.serialize_into_buf(&mut buf[pos..])?;
555 pos += written;
556
557 for comp in &self.extra {
559 let data_len = comp.data.len();
560 if data_len > u8::MAX as usize {
561 return Err(Error::SectionLengthOverflow {
562 declared: data_len,
563 available: u8::MAX as usize,
564 });
565 }
566 buf[pos..pos + 4].copy_from_slice(&comp.tag.to_be_bytes());
567 buf[pos + 4] = data_len as u8;
568 pos += COMPONENT_HEADER_LEN;
569 buf[pos..pos + data_len].copy_from_slice(comp.data);
570 pos += data_len;
571 }
572
573 Ok(len)
574 }
575}
576
577#[derive(Debug, Clone, PartialEq, Eq)]
584#[cfg_attr(feature = "serde", derive(serde::Serialize))]
585pub struct NsapAddress {
586 pub carousel_id: u32,
588 pub specifier_data: [u8; 3],
590 pub transport_stream_id: u16,
592 pub original_network_id: u16,
594 pub service_id: u16,
596}
597
598impl NsapAddress {
599 const AFI: u8 = 0x00;
601 const NSAP_TYPE: u8 = 0x00;
603 const SPECIFIER_TYPE: u8 = 0x01;
605 const RESERVED: u32 = 0xFFFF_FFFF;
607
608 fn parse_from(bytes: &[u8], pos: usize) -> Result<Self> {
609 let end = pos + NSAP_ADDRESS_LEN;
610 let (nsap, _) = bytes
611 .get(pos..)
612 .and_then(|s| s.split_first_chunk::<NSAP_ADDRESS_LEN>())
613 .ok_or(Error::BufferTooShort {
614 need: end,
615 have: bytes.len(),
616 what: "NSAP address (20 bytes)",
617 })?;
618 let carousel_id = u32::from_be_bytes([nsap[2], nsap[3], nsap[4], nsap[5]]);
621 let specifier_data = [nsap[7], nsap[8], nsap[9]];
623 let transport_stream_id = u16::from_be_bytes([nsap[10], nsap[11]]);
624 let original_network_id = u16::from_be_bytes([nsap[12], nsap[13]]);
625 let service_id = u16::from_be_bytes([nsap[14], nsap[15]]);
626 Ok(NsapAddress {
628 carousel_id,
629 specifier_data,
630 transport_stream_id,
631 original_network_id,
632 service_id,
633 })
634 }
635
636 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
637 if buf.len() < NSAP_ADDRESS_LEN {
638 return Err(Error::OutputBufferTooSmall {
639 need: NSAP_ADDRESS_LEN,
640 have: buf.len(),
641 });
642 }
643 buf[0] = Self::AFI;
644 buf[1] = Self::NSAP_TYPE;
645 buf[2..6].copy_from_slice(&self.carousel_id.to_be_bytes());
646 buf[6] = Self::SPECIFIER_TYPE;
647 buf[7..10].copy_from_slice(&self.specifier_data);
648 buf[10..12].copy_from_slice(&self.transport_stream_id.to_be_bytes());
649 buf[12..14].copy_from_slice(&self.original_network_id.to_be_bytes());
650 buf[14..16].copy_from_slice(&self.service_id.to_be_bytes());
651 buf[16..20].copy_from_slice(&Self::RESERVED.to_be_bytes());
652 Ok(NSAP_ADDRESS_LEN)
653 }
654}
655
656#[derive(Debug, Clone, PartialEq, Eq)]
661#[cfg_attr(feature = "serde", derive(serde::Serialize))]
662pub struct NameComponent<'a> {
663 #[cfg_attr(feature = "serde", serde(borrow))]
665 pub id: &'a [u8],
666 #[cfg_attr(feature = "serde", serde(borrow))]
668 pub kind: &'a [u8],
669}
670
671impl<'a> NameComponent<'a> {
672 pub(crate) fn serialized_len_32bit(&self) -> usize {
674 NAMING_FIELD_LEN + self.id.len() + NAMING_FIELD_LEN + self.kind.len()
675 }
676
677 pub(crate) fn parse_32bit(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
679 let (bid, _) = bytes[pos..end]
680 .split_first_chunk::<4>()
681 .ok_or(Error::BufferTooShort {
682 need: pos + NAMING_FIELD_LEN,
683 have: end,
684 what: "CosNaming id_length",
685 })?;
686 let id_len = u32::from_be_bytes(*bid) as usize;
687 let id_start = pos + NAMING_FIELD_LEN;
688 if id_start + id_len > end {
689 return Err(Error::SectionLengthOverflow {
690 declared: id_len,
691 available: end - id_start,
692 });
693 }
694 let id = &bytes[id_start..id_start + id_len];
695 let kind_pos = id_start + id_len;
696 let (bkind, _) =
697 bytes[kind_pos..end]
698 .split_first_chunk::<4>()
699 .ok_or(Error::BufferTooShort {
700 need: kind_pos + NAMING_FIELD_LEN,
701 have: end,
702 what: "CosNaming kind_length",
703 })?;
704 let kind_len = u32::from_be_bytes(*bkind) as usize;
705 let kind_start = kind_pos + NAMING_FIELD_LEN;
706 if kind_start + kind_len > end {
707 return Err(Error::SectionLengthOverflow {
708 declared: kind_len,
709 available: end - kind_start,
710 });
711 }
712 let kind = &bytes[kind_start..kind_start + kind_len];
713 Ok((NameComponent { id, kind }, kind_start + kind_len))
714 }
715
716 pub(crate) fn serialize_32bit(&self, buf: &mut [u8]) -> Result<usize> {
718 let len = self.serialized_len_32bit();
719 if buf.len() < len {
720 return Err(Error::OutputBufferTooSmall {
721 need: len,
722 have: buf.len(),
723 });
724 }
725 buf[0..4].copy_from_slice(&(self.id.len() as u32).to_be_bytes());
726 buf[4..4 + self.id.len()].copy_from_slice(self.id);
727 let kind_pos = 4 + self.id.len();
728 buf[kind_pos..kind_pos + 4].copy_from_slice(&(self.kind.len() as u32).to_be_bytes());
729 buf[kind_pos + 4..kind_pos + 4 + self.kind.len()].copy_from_slice(self.kind);
730 Ok(len)
731 }
732
733 pub(crate) fn serialized_len_8bit(&self) -> usize {
735 1 + self.id.len() + 1 + self.kind.len()
736 }
737
738 pub(crate) fn parse_8bit(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
740 if pos + 1 > end {
741 return Err(Error::BufferTooShort {
742 need: pos + 1,
743 have: end,
744 what: "BIOP NameComponent id_length (8-bit)",
745 });
746 }
747 let id_len = bytes[pos] as usize;
748 let id_start = pos + 1;
749 if id_start + id_len > end {
750 return Err(Error::SectionLengthOverflow {
751 declared: id_len,
752 available: end - id_start,
753 });
754 }
755 let id = &bytes[id_start..id_start + id_len];
756 let kind_pos = id_start + id_len;
757 if kind_pos + 1 > end {
758 return Err(Error::BufferTooShort {
759 need: kind_pos + 1,
760 have: end,
761 what: "BIOP NameComponent kind_length (8-bit)",
762 });
763 }
764 let kind_len = bytes[kind_pos] as usize;
765 let kind_start = kind_pos + 1;
766 if kind_start + kind_len > end {
767 return Err(Error::SectionLengthOverflow {
768 declared: kind_len,
769 available: end - kind_start,
770 });
771 }
772 let kind = &bytes[kind_start..kind_start + kind_len];
773 Ok((NameComponent { id, kind }, kind_start + kind_len))
774 }
775
776 pub(crate) fn serialize_8bit(&self, buf: &mut [u8]) -> Result<usize> {
778 let len = self.serialized_len_8bit();
779 if buf.len() < len {
780 return Err(Error::OutputBufferTooSmall {
781 need: len,
782 have: buf.len(),
783 });
784 }
785 if self.id.len() > u8::MAX as usize {
786 return Err(Error::SectionLengthOverflow {
787 declared: self.id.len(),
788 available: u8::MAX as usize,
789 });
790 }
791 if self.kind.len() > u8::MAX as usize {
792 return Err(Error::SectionLengthOverflow {
793 declared: self.kind.len(),
794 available: u8::MAX as usize,
795 });
796 }
797 buf[0] = self.id.len() as u8;
798 buf[1..1 + self.id.len()].copy_from_slice(self.id);
799 let kind_pos = 1 + self.id.len();
800 buf[kind_pos] = self.kind.len() as u8;
801 buf[kind_pos + 1..kind_pos + 1 + self.kind.len()].copy_from_slice(self.kind);
802 Ok(len)
803 }
804}
805
806#[derive(Debug, Clone, PartialEq, Eq)]
811#[cfg_attr(feature = "serde", derive(serde::Serialize))]
812pub struct ServiceLocation<'a> {
813 pub service_domain: NsapAddress,
815 #[cfg_attr(feature = "serde", serde(borrow))]
817 pub path: Vec<NameComponent<'a>>,
818 #[cfg_attr(feature = "serde", serde(borrow))]
820 pub initial_context: &'a [u8],
821}
822
823impl<'a> ServiceLocation<'a> {
824 fn serialized_len(&self) -> usize {
825 SERVICE_DOMAIN_LEN_FIELD
826 + NSAP_ADDRESS_LEN
827 + NAMING_COUNT_LEN
828 + self
829 .path
830 .iter()
831 .map(|c| c.serialized_len_32bit())
832 .sum::<usize>()
833 + INITIAL_CONTEXT_LEN_FIELD
834 + self.initial_context.len()
835 }
836
837 fn parse_from(bytes: &'a [u8], pos: usize, end: usize) -> Result<(Self, usize)> {
838 if pos + SERVICE_DOMAIN_LEN_FIELD > end {
839 return Err(Error::BufferTooShort {
840 need: pos + SERVICE_DOMAIN_LEN_FIELD,
841 have: end,
842 what: "ServiceLocation serviceDomain_length",
843 });
844 }
845 let sd_len = bytes[pos] as usize;
846 if sd_len != NSAP_ADDRESS_LEN {
847 return Err(Error::ValueOutOfRange {
848 field: "serviceDomain_length",
849 reason: "DVB Carousel NSAP address must be exactly 20 bytes (0x14)",
850 });
851 }
852 let sd_start = pos + SERVICE_DOMAIN_LEN_FIELD;
853 if sd_start + NSAP_ADDRESS_LEN > end {
854 return Err(Error::BufferTooShort {
855 need: sd_start + NSAP_ADDRESS_LEN,
856 have: end,
857 what: "ServiceLocation serviceDomain_data",
858 });
859 }
860 let service_domain = NsapAddress::parse_from(bytes, sd_start)?;
861 let mut cur = sd_start + NSAP_ADDRESS_LEN;
862
863 let (bnc, _) = bytes[cur..end]
864 .split_first_chunk::<4>()
865 .ok_or(Error::BufferTooShort {
866 need: cur + NAMING_COUNT_LEN,
867 have: end,
868 what: "ServiceLocation nameComponents_count",
869 })?;
870 let name_count = u32::from_be_bytes(*bnc) as usize;
871 cur += NAMING_COUNT_LEN;
872 let mut path = Vec::with_capacity(name_count.min(16));
873 for _ in 0..name_count {
874 let (nc, next) = NameComponent::parse_32bit(bytes, cur, end)?;
875 path.push(nc);
876 cur = next;
877 }
878
879 let (bic, _) = bytes[cur..end]
880 .split_first_chunk::<4>()
881 .ok_or(Error::BufferTooShort {
882 need: cur + INITIAL_CONTEXT_LEN_FIELD,
883 have: end,
884 what: "ServiceLocation initialContext_length",
885 })?;
886 let ic_len = u32::from_be_bytes(*bic) as usize;
887 cur += INITIAL_CONTEXT_LEN_FIELD;
888 if cur + ic_len > end {
889 return Err(Error::SectionLengthOverflow {
890 declared: ic_len,
891 available: end - cur,
892 });
893 }
894 let initial_context = &bytes[cur..cur + ic_len];
895 cur += ic_len;
896 Ok((
897 ServiceLocation {
898 service_domain,
899 path,
900 initial_context,
901 },
902 cur,
903 ))
904 }
905
906 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
907 let len = self.serialized_len();
908 if buf.len() < len {
909 return Err(Error::OutputBufferTooSmall {
910 need: len,
911 have: buf.len(),
912 });
913 }
914 buf[0] = NSAP_ADDRESS_LEN as u8; self.service_domain
916 .serialize_into_buf(&mut buf[1..1 + NSAP_ADDRESS_LEN])?;
917 let mut pos = SERVICE_DOMAIN_LEN_FIELD + NSAP_ADDRESS_LEN;
918 buf[pos..pos + 4].copy_from_slice(&(self.path.len() as u32).to_be_bytes());
919 pos += NAMING_COUNT_LEN;
920 for nc in &self.path {
921 let written = nc.serialize_32bit(&mut buf[pos..])?;
922 pos += written;
923 }
924 let ic_len = self.initial_context.len();
925 buf[pos..pos + 4].copy_from_slice(&(ic_len as u32).to_be_bytes());
926 pos += INITIAL_CONTEXT_LEN_FIELD;
927 buf[pos..pos + ic_len].copy_from_slice(self.initial_context);
928 pos += ic_len;
929 Ok(pos)
930 }
931}
932
933#[derive(Debug, Clone, PartialEq, Eq)]
938#[cfg_attr(feature = "serde", derive(serde::Serialize))]
939pub struct LiteOptionsProfileBody<'a> {
940 #[cfg_attr(feature = "serde", serde(borrow))]
942 pub service_location: ServiceLocation<'a>,
943 pub extra: Vec<LiteComponent<'a>>,
945}
946
947impl<'a> LiteOptionsProfileBody<'a> {
948 fn parse_from(bytes: &'a [u8]) -> Result<Self> {
949 let end = bytes.len();
950 if end < LITE_OPTIONS_BODY_FIXED_LEN {
951 return Err(Error::BufferTooShort {
952 need: LITE_OPTIONS_BODY_FIXED_LEN,
953 have: end,
954 what: "LiteOptions Profile Body fixed fields",
955 });
956 }
957 let byte_order = bytes[0];
958 if byte_order != BYTE_ORDER_BIG_ENDIAN {
959 return Err(Error::ReservedBitsViolation {
960 field: "profile_data_byte_order (LiteOptions)",
961 reason: "must be 0x00 (big-endian) per DVB mandatory constraint",
962 });
963 }
964 let component_count = bytes[1] as usize;
965 if component_count < 1 {
966 return Err(Error::ValueOutOfRange {
967 field: "component_count (LiteOptions)",
968 reason: "must have at least 1 component (ServiceLocation)",
969 });
970 }
971 let mut pos = LITE_OPTIONS_BODY_FIXED_LEN;
972
973 let (slch, _) = bytes[pos..end]
975 .split_first_chunk::<SERVICE_LOCATION_COMP_HEADER_LEN>()
976 .ok_or(Error::BufferTooShort {
977 need: pos + SERVICE_LOCATION_COMP_HEADER_LEN,
978 have: end,
979 what: "LiteOptions ServiceLocation component header",
980 })?;
981 let comp0_tag = u32::from_be_bytes([slch[0], slch[1], slch[2], slch[3]]);
982 if comp0_tag != TAG_SERVICE_LOCATION {
983 return Err(Error::ReservedBitsViolation {
984 field: "componentId_tag[0] (LiteOptions)",
985 reason: "first component must be TAG_ServiceLocation (0x49534F46)",
986 });
987 }
988 let comp0_len = u32::from_be_bytes([slch[4], slch[5], slch[6], slch[7]]) as usize;
989 pos += SERVICE_LOCATION_COMP_HEADER_LEN;
990 let comp0_end = pos + comp0_len;
991 if comp0_end > end {
992 return Err(Error::SectionLengthOverflow {
993 declared: comp0_len,
994 available: end - pos,
995 });
996 }
997 let (service_location, _) = ServiceLocation::parse_from(bytes, pos, comp0_end)?;
998 pos = comp0_end;
999
1000 let extra_count = component_count - 1;
1002 let mut extra = Vec::with_capacity(extra_count.min(8));
1003 for _ in 0..extra_count {
1004 let (ech, _) = bytes[pos..end]
1005 .split_first_chunk::<COMPONENT_HEADER_LEN>()
1006 .ok_or(Error::BufferTooShort {
1007 need: pos + COMPONENT_HEADER_LEN,
1008 have: end,
1009 what: "LiteOptions extra component header",
1010 })?;
1011 let tag = u32::from_be_bytes([ech[0], ech[1], ech[2], ech[3]]);
1012 let data_len = ech[4] as usize;
1013 pos += COMPONENT_HEADER_LEN;
1014 if pos + data_len > end {
1015 return Err(Error::SectionLengthOverflow {
1016 declared: data_len,
1017 available: end - pos,
1018 });
1019 }
1020 extra.push(LiteComponent {
1021 tag,
1022 data: &bytes[pos..pos + data_len],
1023 });
1024 pos += data_len;
1025 }
1026
1027 Ok(LiteOptionsProfileBody {
1028 service_location,
1029 extra,
1030 })
1031 }
1032
1033 fn serialized_len(&self) -> usize {
1034 let sl_data_len = self.service_location.serialized_len();
1035 let extra_len: usize = self.extra.iter().map(|c| c.serialized_len()).sum();
1036 LITE_OPTIONS_BODY_FIXED_LEN + SERVICE_LOCATION_COMP_HEADER_LEN + sl_data_len + extra_len
1037 }
1038
1039 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
1040 let len = self.serialized_len();
1041 if buf.len() < len {
1042 return Err(Error::OutputBufferTooSmall {
1043 need: len,
1044 have: buf.len(),
1045 });
1046 }
1047 let total_components = 1 + self.extra.len();
1048 if total_components > u8::MAX as usize {
1049 return Err(Error::SectionLengthOverflow {
1050 declared: total_components,
1051 available: u8::MAX as usize,
1052 });
1053 }
1054 buf[0] = BYTE_ORDER_BIG_ENDIAN;
1055 buf[1] = total_components as u8;
1056 let mut pos = LITE_OPTIONS_BODY_FIXED_LEN;
1057
1058 let sl_data_len = self.service_location.serialized_len();
1060 buf[pos..pos + 4].copy_from_slice(&TAG_SERVICE_LOCATION.to_be_bytes());
1061 buf[pos + 4..pos + 8].copy_from_slice(&(sl_data_len as u32).to_be_bytes());
1062 pos += SERVICE_LOCATION_COMP_HEADER_LEN;
1063 let written = self.service_location.serialize_into_buf(&mut buf[pos..])?;
1064 pos += written;
1065
1066 for comp in &self.extra {
1068 let data_len = comp.data.len();
1069 if data_len > u8::MAX as usize {
1070 return Err(Error::SectionLengthOverflow {
1071 declared: data_len,
1072 available: u8::MAX as usize,
1073 });
1074 }
1075 buf[pos..pos + 4].copy_from_slice(&comp.tag.to_be_bytes());
1076 buf[pos + 4] = data_len as u8;
1077 pos += COMPONENT_HEADER_LEN;
1078 buf[pos..pos + data_len].copy_from_slice(comp.data);
1079 pos += data_len;
1080 }
1081
1082 Ok(len)
1083 }
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Eq)]
1091#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1092#[non_exhaustive]
1093pub enum TaggedProfile<'a> {
1094 Biop(BiopProfileBody<'a>),
1096 LiteOptions(LiteOptionsProfileBody<'a>),
1098 Unknown {
1100 tag: u32,
1102 #[cfg_attr(feature = "serde", serde(borrow))]
1104 data: &'a [u8],
1105 },
1106}
1107
1108impl TaggedProfile<'_> {
1109 fn serialized_len(&self) -> usize {
1110 let data_len = match self {
1111 Self::Biop(b) => b.serialized_len(),
1112 Self::LiteOptions(l) => l.serialized_len(),
1113 Self::Unknown { data, .. } => data.len(),
1114 };
1115 PROFILE_HEADER_LEN + data_len
1116 }
1117
1118 fn serialize_into_buf(&self, buf: &mut [u8]) -> Result<usize> {
1119 let len = self.serialized_len();
1120 if buf.len() < len {
1121 return Err(Error::OutputBufferTooSmall {
1122 need: len,
1123 have: buf.len(),
1124 });
1125 }
1126 let (tag, data_len) = match self {
1127 Self::Biop(b) => (TAG_BIOP, b.serialized_len()),
1128 Self::LiteOptions(l) => (TAG_LITE_OPTIONS, l.serialized_len()),
1129 Self::Unknown { tag, data } => (*tag, data.len()),
1130 };
1131 buf[0..4].copy_from_slice(&tag.to_be_bytes());
1132 buf[4..8].copy_from_slice(&(data_len as u32).to_be_bytes());
1133 let pos = PROFILE_HEADER_LEN;
1134 match self {
1135 Self::Biop(b) => {
1136 b.serialize_into_buf(&mut buf[pos..])?;
1137 }
1138 Self::LiteOptions(l) => {
1139 l.serialize_into_buf(&mut buf[pos..])?;
1140 }
1141 Self::Unknown { data, .. } => {
1142 buf[pos..pos + data.len()].copy_from_slice(data);
1143 }
1144 }
1145 Ok(len)
1146 }
1147}
1148
1149#[derive(Debug, Clone, PartialEq, Eq)]
1157#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1158pub struct Ior<'a> {
1159 #[cfg_attr(feature = "serde", serde(borrow))]
1161 pub type_id: &'a [u8],
1162 pub profiles: Vec<TaggedProfile<'a>>,
1164}
1165
1166impl<'a> Ior<'a> {
1167 pub fn object_kind(&self) -> ObjectKind {
1169 if self.type_id.len() == 4 {
1170 let mut arr = [0u8; 4];
1171 arr.copy_from_slice(self.type_id);
1172 ObjectKind::from_bytes(arr)
1173 } else {
1174 ObjectKind::Unknown([0; 4])
1175 }
1176 }
1177
1178 pub fn biop_profile(&self) -> Option<&BiopProfileBody<'a>> {
1180 for p in &self.profiles {
1181 if let TaggedProfile::Biop(b) = p {
1182 return Some(b);
1183 }
1184 }
1185 None
1186 }
1187}
1188
1189impl<'a> Parse<'a> for Ior<'a> {
1190 type Error = crate::error::Error;
1191
1192 fn parse(bytes: &'a [u8]) -> Result<Self> {
1193 let end = bytes.len();
1194 let (ior_hdr, _) =
1195 bytes
1196 .split_first_chunk::<IOR_FIXED_LEN>()
1197 .ok_or(Error::BufferTooShort {
1198 need: IOR_FIXED_LEN,
1199 have: end,
1200 what: "IOP::IOR fixed fields",
1201 })?;
1202 let type_id_length =
1203 u32::from_be_bytes([ior_hdr[0], ior_hdr[1], ior_hdr[2], ior_hdr[3]]) as usize;
1204 if type_id_length % 4 != 0 {
1206 return Err(Error::ValueOutOfRange {
1207 field: "IOR.type_id_length",
1208 reason: "type_id_length must be a multiple of 4 (DVB alias type_ids only — \
1209 non-aligned type_ids are not supported per TR 101 202 §4.7.3.1)",
1210 });
1211 }
1212 let mut pos = 4;
1213 if pos + type_id_length > end {
1214 return Err(Error::SectionLengthOverflow {
1215 declared: type_id_length,
1216 available: end - pos,
1217 });
1218 }
1219 let type_id = &bytes[pos..pos + type_id_length];
1220 pos += type_id_length;
1221
1222 let (bpc, _) = bytes[pos..end]
1223 .split_first_chunk::<4>()
1224 .ok_or(Error::BufferTooShort {
1225 need: pos + 4,
1226 have: end,
1227 what: "IOR taggedProfiles_count",
1228 })?;
1229 let profiles_count = u32::from_be_bytes(*bpc) as usize;
1230 pos += 4;
1231
1232 let mut profiles = Vec::with_capacity(profiles_count.min(8));
1233 for _ in 0..profiles_count {
1234 let (phdr, _) = bytes[pos..end]
1235 .split_first_chunk::<PROFILE_HEADER_LEN>()
1236 .ok_or(Error::BufferTooShort {
1237 need: pos + PROFILE_HEADER_LEN,
1238 have: end,
1239 what: "TaggedProfile header",
1240 })?;
1241 let tag = u32::from_be_bytes([phdr[0], phdr[1], phdr[2], phdr[3]]);
1242 let data_len = u32::from_be_bytes([phdr[4], phdr[5], phdr[6], phdr[7]]) as usize;
1243 pos += PROFILE_HEADER_LEN;
1244 if pos + data_len > end {
1245 return Err(Error::SectionLengthOverflow {
1246 declared: data_len,
1247 available: end - pos,
1248 });
1249 }
1250 let profile_data = &bytes[pos..pos + data_len];
1251 let profile = match tag {
1252 TAG_BIOP => TaggedProfile::Biop(BiopProfileBody::parse_from(profile_data)?),
1253 TAG_LITE_OPTIONS => {
1254 TaggedProfile::LiteOptions(LiteOptionsProfileBody::parse_from(profile_data)?)
1255 }
1256 _ => TaggedProfile::Unknown {
1257 tag,
1258 data: profile_data,
1259 },
1260 };
1261 profiles.push(profile);
1262 pos += data_len;
1263 }
1264
1265 Ok(Ior { type_id, profiles })
1266 }
1267}
1268
1269impl Serialize for Ior<'_> {
1270 type Error = crate::error::Error;
1271
1272 fn serialized_len(&self) -> usize {
1273 let type_id_len = self.type_id.len();
1274 let profiles_len: usize = self.profiles.iter().map(|p| p.serialized_len()).sum();
1275 4 + type_id_len
1277 + 4 + profiles_len
1279 }
1280
1281 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1282 let len = self.serialized_len();
1283 if buf.len() < len {
1284 return Err(Error::OutputBufferTooSmall {
1285 need: len,
1286 have: buf.len(),
1287 });
1288 }
1289 if self.type_id.len() % 4 != 0 {
1290 return Err(Error::ValueOutOfRange {
1291 field: "IOR.type_id_length",
1292 reason: "type_id_length must be a multiple of 4 (DVB alias type_ids only)",
1293 });
1294 }
1295 buf[0..4].copy_from_slice(&(self.type_id.len() as u32).to_be_bytes());
1296 buf[4..4 + self.type_id.len()].copy_from_slice(self.type_id);
1297 let mut pos = 4 + self.type_id.len();
1298 buf[pos..pos + 4].copy_from_slice(&(self.profiles.len() as u32).to_be_bytes());
1299 pos += 4;
1300 for profile in &self.profiles {
1301 let written = profile.serialize_into_buf(&mut buf[pos..])?;
1302 pos += written;
1303 }
1304 Ok(len)
1305 }
1306}
1307
1308#[cfg(test)]
1311mod tests {
1312 use super::*;
1313 use broadcast_common::Parse;
1314
1315 fn sample_ior() -> Vec<u8> {
1316 #[rustfmt::skip]
1321 let bytes: &[u8] = &[
1322 0x00, 0x00, 0x00, 0x04,
1324 0x73, 0x72, 0x67, 0x00,
1326 0x00, 0x00, 0x00, 0x01,
1328 0x49, 0x53, 0x4F, 0x06,
1330 0x00, 0x00, 0x00, 0x28,
1332 0x00, 0x02,
1334 0x49, 0x53, 0x4F, 0x50, 0x0A,
1336 0x00, 0x00, 0x00, 0xAB, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01,
1338 0x49, 0x53, 0x4F, 0x40, 0x12,
1340 0x01, 0x00, 0x00, 0x00, 0x16, 0x00, 0x47, 0x0A,
1342 0x00, 0x01, 0x80, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF,
1343 ];
1344 bytes.to_vec()
1345 }
1346
1347 #[test]
1348 fn ior_round_trip() {
1349 let raw = sample_ior();
1350 let ior = Ior::parse(&raw).unwrap();
1351 let mut out = vec![0u8; ior.serialized_len()];
1352 ior.serialize_into(&mut out).unwrap();
1353 assert_eq!(out, raw, "IOR round-trip byte-exact");
1354 }
1355
1356 #[test]
1357 fn ior_byte_anchor_m6_sgw() {
1358 let raw = sample_ior();
1359 let ior = Ior::parse(&raw).unwrap();
1360
1361 assert_eq!(ior.type_id, b"srg\0");
1362 assert_eq!(ior.object_kind(), ObjectKind::ServiceGateway);
1363 assert_eq!(ior.profiles.len(), 1);
1364
1365 let bp = ior.biop_profile().unwrap();
1366 assert_eq!(bp.object_location.carousel_id, 0xAB);
1367 assert_eq!(bp.object_location.module_id, 1);
1368 assert_eq!(bp.object_location.version_major, 1);
1369 assert_eq!(bp.object_location.version_minor, 0);
1370 assert_eq!(bp.object_location.object_key, &[0x01]);
1371
1372 assert_eq!(bp.conn_binder.taps.len(), 1);
1373 let tap = &bp.conn_binder.taps[0];
1374 assert_eq!(tap.id, 0);
1375 assert_eq!(tap.use_, 0x0016);
1376 assert_eq!(tap.association_tag, 0x47);
1377 assert_eq!(
1378 tap.selector,
1379 &[0x00, 0x01, 0x80, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF]
1380 );
1381 assert_eq!(tap.transaction_id(), Some(0x80000002));
1382 assert_eq!(tap.timeout(), Some(0xFFFFFFFF));
1383 }
1384
1385 #[test]
1386 fn ior_rejects_non_aligned_type_id() {
1387 let bytes: &[u8] = &[
1389 0x00, 0x00, 0x00, 0x03, 0x64, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00,
1390 ];
1391 assert!(matches!(
1392 Ior::parse(bytes).unwrap_err(),
1393 crate::error::Error::ValueOutOfRange {
1394 field: "IOR.type_id_length",
1395 ..
1396 }
1397 ));
1398 }
1399
1400 #[test]
1401 fn object_kind_roundtrip() {
1402 let kinds = [
1403 ObjectKind::Directory,
1404 ObjectKind::File,
1405 ObjectKind::Stream,
1406 ObjectKind::ServiceGateway,
1407 ObjectKind::StreamEvent,
1408 ObjectKind::Unknown([0x01, 0x02, 0x03, 0x04]),
1409 ];
1410 for k in &kinds {
1411 let b = k.to_bytes();
1412 assert_eq!(ObjectKind::from_bytes(b), *k);
1413 }
1414 }
1415
1416 #[cfg(feature = "serde")]
1417 #[test]
1418 fn ior_serde_round_trip() {
1419 let raw = sample_ior();
1420 let ior = Ior::parse(&raw).unwrap();
1421 let json = serde_json::to_string(&ior).unwrap();
1422 assert!(
1424 json.contains("carousel_id"),
1425 "JSON must contain carousel_id field"
1426 );
1427 assert!(
1428 json.contains("\"Biop\""),
1429 "JSON must contain Biop profile variant"
1430 );
1431 }
1432}