Skip to main content

dvb_si/descriptors/
carousel_identifier.rs

1//! Carousel Identifier Descriptor — ISO/IEC 13818-6 / ETSI TR 101 202 §4.7.7.1 (tag 0x13).
2//!
3//! Table 4.17 / 4.17a (TR 101 202 v1.2.1). Carried in the PMT `ES_info` loop
4//! to bind an elementary stream to a DVB object carousel.
5//!
6//! Wire layout:
7//!
8//! ```text
9//! carousel_identifier_descriptor() {
10//!   descriptor_tag       8   = 0x13
11//!   descriptor_length    8
12//!   carousel_id         32   uimsbf
13//!   FormatId             8   uimsbf   (selects the FormatSpecifier; Table 4.17a)
14//!   FormatSpecifier()    8×N2         (depends on FormatId; 0 bytes if 0x00)
15//!   private_data_byte    8×N1         (remainder of the descriptor body)
16//! }
17//! ```
18//!
19//! `FormatId` values: `0x00` → no specifier; `0x01` → aggregated specifier
20//! (full field set from Table 4.17a); `0x02`–`0xFF` → reserved/private
21//! (carried as opaque bytes in [`FormatSpecifier::Other`]).
22
23use super::descriptor_body;
24use crate::error::{Error, Result};
25use dvb_common::{Parse, Serialize};
26
27/// Descriptor tag for carousel_identifier_descriptor.
28pub const TAG: u8 = 0x13;
29
30// ── field-width constants ─────────────────────────────────────────────────────
31/// 2-byte descriptor outer header (tag + length).
32const HEADER_LEN: usize = 2;
33/// `carousel_id` field width in bytes (32-bit uimsbf).
34const CAROUSEL_ID_LEN: usize = 4;
35/// `FormatId` field width in bytes (8-bit uimsbf).
36const FORMAT_ID_LEN: usize = 1;
37/// Fixed prefix of every descriptor body: carousel_id + FormatId.
38const BODY_PREFIX_LEN: usize = CAROUSEL_ID_LEN + FORMAT_ID_LEN;
39
40// ── FormatId = 0x01 FormatSpecifier field widths ──────────────────────────────
41/// `ModuleVersion` field: 8-bit uimsbf.
42const FS1_MODULE_VERSION_LEN: usize = 1;
43/// `ModuleId` field: 16-bit uimsbf.
44const FS1_MODULE_ID_LEN: usize = 2;
45/// `BlockSize` field: 16-bit uimsbf.
46const FS1_BLOCK_SIZE_LEN: usize = 2;
47/// `ModuleSize` field: 32-bit uimsbf.
48const FS1_MODULE_SIZE_LEN: usize = 4;
49/// `CompressionMethod` field: 8-bit uimsbf.
50const FS1_COMPRESSION_METHOD_LEN: usize = 1;
51/// `OriginalSize` field: 32-bit uimsbf.
52const FS1_ORIGINAL_SIZE_LEN: usize = 4;
53/// `TimeOut` field: 8-bit uimsbf (TR 101 202 v1.2.1; 8-bit, not the 32-bit of
54/// later-edition documents).
55const FS1_TIMEOUT_LEN: usize = 1;
56/// `ObjectKeyLength` field: 8-bit uimsbf.
57const FS1_OBJECT_KEY_LENGTH_LEN: usize = 1;
58/// Total fixed-size bytes of the FormatId=0x01 specifier before `ObjectKeyData`.
59const FS1_FIXED_LEN: usize = FS1_MODULE_VERSION_LEN
60    + FS1_MODULE_ID_LEN
61    + FS1_BLOCK_SIZE_LEN
62    + FS1_MODULE_SIZE_LEN
63    + FS1_COMPRESSION_METHOD_LEN
64    + FS1_ORIGINAL_SIZE_LEN
65    + FS1_TIMEOUT_LEN
66    + FS1_OBJECT_KEY_LENGTH_LEN; // = 16
67
68// ── FormatId values ───────────────────────────────────────────────────────────
69/// `FormatId = 0x00`: no `FormatSpecifier` bytes present.
70const FORMAT_ID_NONE: u8 = 0x00;
71/// `FormatId = 0x01`: aggregated FormatSpecifier (Table 4.17a).
72const FORMAT_ID_AGGREGATED: u8 = 0x01;
73
74/// `FormatSpecifier` — the optional aggregated block in a
75/// `carousel_identifier_descriptor` (ISO/IEC 13818-6 / TR 101 202 Table 4.17a).
76///
77/// Dispatch on `FormatId`:
78///
79/// | FormatId | Variant | Meaning |
80/// |----------|---------|---------|
81/// | `0x00` | [`Absent`][FormatSpecifier::Absent] | No specifier bytes |
82/// | `0x01` | [`Aggregated`][FormatSpecifier::Aggregated] | Full field set (Table 4.17a) |
83/// | `0x02`–`0xFF` | [`Other`][FormatSpecifier::Other] | Reserved/private; raw bytes |
84///
85/// This is a data-carrying ADT — the variants hold payloads, not just labels.
86/// Use [`FormatSpecifier::format_id`] to retrieve the wire `FormatId` value.
87#[derive(Debug, Clone, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89#[non_exhaustive]
90pub enum FormatSpecifier<'a> {
91    /// `FormatId = 0x00`: no `FormatSpecifier` bytes.
92    Absent,
93    /// `FormatId = 0x01`: aggregated FormatSpecifier (TR 101 202 Table 4.17a).
94    ///
95    /// Wire layout of the specifier block (all uimsbf):
96    ///
97    /// ```text
98    /// ModuleVersion      8
99    /// ModuleId          16
100    /// BlockSize         16
101    /// ModuleSize        32
102    /// CompressionMethod  8
103    /// OriginalSize      32
104    /// TimeOut            8   (8-bit per TR 101 202 v1.2.1)
105    /// ObjectKeyLength    8   = N1
106    /// ObjectKeyData      8×N1
107    /// ```
108    Aggregated {
109        /// `ModuleVersion` `[7:0]` — version of the DSM-CC module.
110        module_version: u8,
111        /// `ModuleId` `[15:0]` — identity of the DSM-CC module.
112        module_id: u16,
113        /// `BlockSize` `[15:0]` — download block size in bytes.
114        block_size: u16,
115        /// `ModuleSize` `[31:0]` — total module size in bytes.
116        module_size: u32,
117        /// `CompressionMethod` `[7:0]` — 0x00 = none.
118        compression_method: u8,
119        /// `OriginalSize` `[31:0]` — uncompressed module size in bytes; only
120        /// meaningful when `compression_method != 0x00`.
121        original_size: u32,
122        /// `TimeOut` `[7:0]` — acquisition timeout (8-bit; TR 101 202 v1.2.1).
123        timeout: u8,
124        /// `ObjectKeyData` — the object key bytes (`ObjectKeyLength` bytes).
125        #[cfg_attr(feature = "serde", serde(borrow))]
126        object_key: &'a [u8],
127    },
128    /// `FormatId = 0x02`–`0xFF`: reserved or private format; carried opaque.
129    Other {
130        /// The raw `FormatId` byte.
131        format_id: u8,
132        /// Raw specifier bytes (remainder after the carousel_id + FormatId
133        /// prefix, before `private_data_byte`).
134        ///
135        /// For unknown `FormatId` values the caller cannot know where the
136        /// specifier ends and private data begins, so the entire body remainder
137        /// is treated as specifier bytes and `private_data` is empty.
138        #[cfg_attr(feature = "serde", serde(borrow))]
139        bytes: &'a [u8],
140    },
141}
142
143impl<'a> FormatSpecifier<'a> {
144    /// The wire `FormatId` byte for this variant.
145    #[must_use]
146    pub fn format_id(&self) -> u8 {
147        match self {
148            FormatSpecifier::Absent => FORMAT_ID_NONE,
149            FormatSpecifier::Aggregated { .. } => FORMAT_ID_AGGREGATED,
150            FormatSpecifier::Other { format_id, .. } => *format_id,
151        }
152    }
153
154    /// Byte length of the serialized specifier block (excluding the outer
155    /// descriptor tag/length/carousel_id/FormatId fields).
156    fn serialized_len(&self) -> usize {
157        match self {
158            FormatSpecifier::Absent => 0,
159            FormatSpecifier::Aggregated { object_key, .. } => FS1_FIXED_LEN + object_key.len(),
160            FormatSpecifier::Other { bytes, .. } => bytes.len(),
161        }
162    }
163}
164
165/// Carousel Identifier Descriptor (tag 0x13) —
166/// ISO/IEC 13818-6 / ETSI TR 101 202 §4.7.7.1.
167///
168/// Carried in the PMT `ES_info` loop to bind an elementary stream to a DVB
169/// object carousel. The `format` field selects the typed
170/// [`FormatSpecifier`] variant; the `private_data` tail follows.
171#[derive(Debug, Clone, PartialEq, Eq)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize))]
173#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
174pub struct CarouselIdentifierDescriptor<'a> {
175    /// `carousel_id` `[31:0]` — 32-bit carousel identity, unique per TS.
176    pub carousel_id: u32,
177    /// Parsed `FormatSpecifier` (dispatch on `FormatId`).
178    #[cfg_attr(feature = "serde", serde(borrow))]
179    pub format: FormatSpecifier<'a>,
180    /// `private_data_byte` tail — zero or more bytes after the `FormatSpecifier`.
181    ///
182    /// Empty for [`FormatSpecifier::Other`] (the whole remainder is in
183    /// [`FormatSpecifier::Other::bytes`] since the boundary is unknown).
184    #[cfg_attr(feature = "serde", serde(borrow))]
185    pub private_data: &'a [u8],
186}
187
188impl<'a> Parse<'a> for CarouselIdentifierDescriptor<'a> {
189    type Error = crate::error::Error;
190
191    fn parse(bytes: &'a [u8]) -> Result<Self> {
192        let body = descriptor_body(
193            bytes,
194            TAG,
195            "CarouselIdentifierDescriptor",
196            "unexpected tag for carousel_identifier_descriptor",
197        )?;
198        if body.len() < BODY_PREFIX_LEN {
199            return Err(Error::InvalidDescriptor {
200                tag: TAG,
201                reason: "carousel_identifier_descriptor body shorter than 5 bytes",
202            });
203        }
204        let carousel_id = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
205        let format_id = body[CAROUSEL_ID_LEN];
206        let after_prefix = &body[BODY_PREFIX_LEN..];
207
208        let (format, private_data) = match format_id {
209            FORMAT_ID_NONE => (FormatSpecifier::Absent, after_prefix),
210            FORMAT_ID_AGGREGATED => {
211                if after_prefix.len() < FS1_FIXED_LEN {
212                    return Err(Error::InvalidDescriptor {
213                        tag: TAG,
214                        reason: "FormatId=0x01 specifier truncated (insufficient fixed fields)",
215                    });
216                }
217                let mut pos = 0usize;
218                let module_version = after_prefix[pos];
219                pos += FS1_MODULE_VERSION_LEN;
220                let module_id = u16::from_be_bytes([after_prefix[pos], after_prefix[pos + 1]]);
221                pos += FS1_MODULE_ID_LEN;
222                let block_size = u16::from_be_bytes([after_prefix[pos], after_prefix[pos + 1]]);
223                pos += FS1_BLOCK_SIZE_LEN;
224                let module_size = u32::from_be_bytes([
225                    after_prefix[pos],
226                    after_prefix[pos + 1],
227                    after_prefix[pos + 2],
228                    after_prefix[pos + 3],
229                ]);
230                pos += FS1_MODULE_SIZE_LEN;
231                let compression_method = after_prefix[pos];
232                pos += FS1_COMPRESSION_METHOD_LEN;
233                let original_size = u32::from_be_bytes([
234                    after_prefix[pos],
235                    after_prefix[pos + 1],
236                    after_prefix[pos + 2],
237                    after_prefix[pos + 3],
238                ]);
239                pos += FS1_ORIGINAL_SIZE_LEN;
240                let timeout = after_prefix[pos];
241                pos += FS1_TIMEOUT_LEN;
242                let object_key_length = after_prefix[pos] as usize;
243                pos += FS1_OBJECT_KEY_LENGTH_LEN;
244                // pos == FS1_FIXED_LEN here
245                let specifier_end = pos + object_key_length;
246                if specifier_end > after_prefix.len() {
247                    return Err(Error::InvalidDescriptor {
248                        tag: TAG,
249                        reason: "FormatId=0x01 ObjectKeyData exceeds descriptor body",
250                    });
251                }
252                let object_key = &after_prefix[pos..specifier_end];
253                let private_data = &after_prefix[specifier_end..];
254                (
255                    FormatSpecifier::Aggregated {
256                        module_version,
257                        module_id,
258                        block_size,
259                        module_size,
260                        compression_method,
261                        original_size,
262                        timeout,
263                        object_key,
264                    },
265                    private_data,
266                )
267            }
268            other => {
269                // Unknown/private: the boundary between specifier and
270                // private_data is unknowable, so absorb the whole remainder
271                // into the specifier bytes.
272                (
273                    FormatSpecifier::Other {
274                        format_id: other,
275                        bytes: after_prefix,
276                    },
277                    &[][..],
278                )
279            }
280        };
281        Ok(Self {
282            carousel_id,
283            format,
284            private_data,
285        })
286    }
287}
288
289impl Serialize for CarouselIdentifierDescriptor<'_> {
290    type Error = crate::error::Error;
291
292    fn serialized_len(&self) -> usize {
293        let body = BODY_PREFIX_LEN + self.format.serialized_len() + self.private_data.len();
294        HEADER_LEN + body
295    }
296
297    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
298        let total = self.serialized_len();
299        let body_len = BODY_PREFIX_LEN + self.format.serialized_len() + self.private_data.len();
300        if body_len > u8::MAX as usize {
301            return Err(Error::InvalidDescriptor {
302                tag: TAG,
303                reason: "carousel_identifier_descriptor body exceeds 255 bytes",
304            });
305        }
306        if buf.len() < total {
307            return Err(Error::OutputBufferTooSmall {
308                need: total,
309                have: buf.len(),
310            });
311        }
312
313        buf[0] = TAG;
314        buf[1] = body_len as u8;
315        buf[HEADER_LEN..HEADER_LEN + CAROUSEL_ID_LEN]
316            .copy_from_slice(&self.carousel_id.to_be_bytes());
317        buf[HEADER_LEN + CAROUSEL_ID_LEN] = self.format.format_id();
318
319        let mut pos = HEADER_LEN + BODY_PREFIX_LEN;
320        match &self.format {
321            FormatSpecifier::Absent => {}
322            FormatSpecifier::Aggregated {
323                module_version,
324                module_id,
325                block_size,
326                module_size,
327                compression_method,
328                original_size,
329                timeout,
330                object_key,
331            } => {
332                if object_key.len() > u8::MAX as usize {
333                    return Err(Error::InvalidDescriptor {
334                        tag: TAG,
335                        reason: "FormatId=0x01 ObjectKeyData exceeds 255 bytes",
336                    });
337                }
338                buf[pos] = *module_version;
339                pos += FS1_MODULE_VERSION_LEN;
340                buf[pos..pos + FS1_MODULE_ID_LEN].copy_from_slice(&module_id.to_be_bytes());
341                pos += FS1_MODULE_ID_LEN;
342                buf[pos..pos + FS1_BLOCK_SIZE_LEN].copy_from_slice(&block_size.to_be_bytes());
343                pos += FS1_BLOCK_SIZE_LEN;
344                buf[pos..pos + FS1_MODULE_SIZE_LEN].copy_from_slice(&module_size.to_be_bytes());
345                pos += FS1_MODULE_SIZE_LEN;
346                buf[pos] = *compression_method;
347                pos += FS1_COMPRESSION_METHOD_LEN;
348                buf[pos..pos + FS1_ORIGINAL_SIZE_LEN].copy_from_slice(&original_size.to_be_bytes());
349                pos += FS1_ORIGINAL_SIZE_LEN;
350                buf[pos] = *timeout;
351                pos += FS1_TIMEOUT_LEN;
352                buf[pos] = object_key.len() as u8;
353                pos += FS1_OBJECT_KEY_LENGTH_LEN;
354                buf[pos..pos + object_key.len()].copy_from_slice(object_key);
355                pos += object_key.len();
356            }
357            FormatSpecifier::Other { bytes, .. } => {
358                buf[pos..pos + bytes.len()].copy_from_slice(bytes);
359                pos += bytes.len();
360            }
361        }
362        buf[pos..pos + self.private_data.len()].copy_from_slice(self.private_data);
363        Ok(total)
364    }
365}
366
367impl<'a> crate::traits::DescriptorDef<'a> for CarouselIdentifierDescriptor<'a> {
368    const TAG: u8 = TAG;
369    const NAME: &'static str = "CAROUSEL_IDENTIFIER";
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    // ── byte-anchor constants for hand-built test vector ─────────────────────
377    //
378    // Descriptor wire layout (FormatId=0x01, ObjectKeyLength=1, 2 private bytes):
379    //
380    // Offset  Field                 Value      Comment
381    //   [0]   descriptor_tag        0x13
382    //   [1]   descriptor_length     0x18       = 24 bytes body
383    //   [2]   carousel_id[31:24]    0x00
384    //   [3]   carousel_id[23:16]    0x00
385    //   [4]   carousel_id[15:8]     0x00
386    //   [5]   carousel_id[7:0]      0x01       carousel_id = 1
387    //   [6]   FormatId              0x01       = Aggregated
388    //   [7]   ModuleVersion         0x03
389    //   [8]   ModuleId[15:8]        0x00
390    //   [9]   ModuleId[7:0]         0x05       module_id = 5
391    //  [10]   BlockSize[15:8]       0x04
392    //  [11]   BlockSize[7:0]        0x00       block_size = 1024
393    //  [12]   ModuleSize[31:24]     0x00
394    //  [13]   ModuleSize[23:16]     0x00
395    //  [14]   ModuleSize[15:8]      0x10
396    //  [15]   ModuleSize[7:0]       0x00       module_size = 0x1000 = 4096
397    //  [16]   CompressionMethod     0x00       no compression
398    //  [17]   OriginalSize[31:24]   0x00
399    //  [18]   OriginalSize[23:16]   0x00
400    //  [19]   OriginalSize[15:8]    0x10
401    //  [20]   OriginalSize[7:0]     0x00       original_size = 4096
402    //  [21]   TimeOut               0x1E       = 30
403    //  [22]   ObjectKeyLength       0x01       N1 = 1
404    //  [23]   ObjectKeyData[0]      0xAB
405    //  [24]   private_data_byte[0]  0xDE
406    //  [25]   private_data_byte[1]  0xAD
407    //
408    // Total: 26 bytes; body = 24 (0x18).
409
410    #[rustfmt::skip]
411    const ANCHOR_BYTES: &[u8] = &[
412        0x13, 0x18,                         // tag, length=24
413        0x00, 0x00, 0x00, 0x01,             // carousel_id = 1
414        0x01,                               // FormatId = Aggregated
415        0x03,                               // ModuleVersion = 3
416        0x00, 0x05,                         // ModuleId = 5
417        0x04, 0x00,                         // BlockSize = 1024
418        0x00, 0x00, 0x10, 0x00,             // ModuleSize = 4096
419        0x00,                               // CompressionMethod = 0
420        0x00, 0x00, 0x10, 0x00,             // OriginalSize = 4096
421        0x1E,                               // TimeOut = 30
422        0x01,                               // ObjectKeyLength = 1
423        0xAB,                               // ObjectKeyData
424        0xDE, 0xAD,                         // private_data
425    ];
426
427    fn anchor_descriptor() -> CarouselIdentifierDescriptor<'static> {
428        CarouselIdentifierDescriptor {
429            carousel_id: 1,
430            format: FormatSpecifier::Aggregated {
431                module_version: 3,
432                module_id: 5,
433                block_size: 1024,
434                module_size: 4096,
435                compression_method: 0,
436                original_size: 4096,
437                timeout: 30,
438                object_key: &[0xAB],
439            },
440            private_data: &[0xDE, 0xAD],
441        }
442    }
443
444    // ── byte-anchor test ──────────────────────────────────────────────────────
445
446    #[test]
447    fn byte_anchor_serialize_matches_hand_built() {
448        let d = anchor_descriptor();
449        let mut buf = vec![0u8; d.serialized_len()];
450        d.serialize_into(&mut buf).unwrap();
451        assert_eq!(
452            buf.as_slice(),
453            ANCHOR_BYTES,
454            "serialized bytes differ from anchor"
455        );
456    }
457
458    #[test]
459    fn byte_anchor_parse_extracts_correct_fields() {
460        let d = CarouselIdentifierDescriptor::parse(ANCHOR_BYTES).unwrap();
461        assert_eq!(d.carousel_id, 1);
462        assert_eq!(d.private_data, &[0xDE, 0xAD]);
463        match &d.format {
464            FormatSpecifier::Aggregated {
465                module_version,
466                module_id,
467                block_size,
468                module_size,
469                compression_method,
470                original_size,
471                timeout,
472                object_key,
473            } => {
474                assert_eq!(*module_version, 3);
475                assert_eq!(*module_id, 5);
476                assert_eq!(*block_size, 1024);
477                assert_eq!(*module_size, 4096);
478                assert_eq!(*compression_method, 0);
479                assert_eq!(*original_size, 4096);
480                assert_eq!(*timeout, 30);
481                assert_eq!(*object_key, &[0xAB]);
482            }
483            other => panic!("expected Aggregated, got {other:?}"),
484        }
485    }
486
487    #[test]
488    fn byte_anchor_round_trip_byte_identical() {
489        // parse → serialize → byte-identical with the original anchor bytes.
490        let d = CarouselIdentifierDescriptor::parse(ANCHOR_BYTES).unwrap();
491        let mut buf = vec![0u8; d.serialized_len()];
492        d.serialize_into(&mut buf).unwrap();
493        assert_eq!(
494            buf.as_slice(),
495            ANCHOR_BYTES,
496            "round-trip not byte-identical"
497        );
498        // Second pass: serialize the struct literal, parse back, re-serialize.
499        let d2 = anchor_descriptor();
500        let mut buf2 = vec![0u8; d2.serialized_len()];
501        d2.serialize_into(&mut buf2).unwrap();
502        let d3 = CarouselIdentifierDescriptor::parse(&buf2).unwrap();
503        let mut buf3 = vec![0u8; d3.serialized_len()];
504        d3.serialize_into(&mut buf3).unwrap();
505        assert_eq!(buf2, buf3, "struct literal round-trip not byte-identical");
506    }
507
508    // ── FormatId=0x00 (Absent) ────────────────────────────────────────────────
509
510    #[test]
511    fn parse_format_absent_extracts_fields() {
512        // carousel_id=0xDEADBEEF, FormatId=0x00, 3 private bytes.
513        let bytes = [TAG, 0x08, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xAA, 0xBB, 0xCC];
514        let d = CarouselIdentifierDescriptor::parse(&bytes).unwrap();
515        assert_eq!(d.carousel_id, 0xDEAD_BEEF);
516        assert_eq!(d.format, FormatSpecifier::Absent);
517        assert_eq!(d.format.format_id(), 0x00);
518        assert_eq!(d.private_data, &[0xAA, 0xBB, 0xCC]);
519    }
520
521    #[test]
522    fn round_trip_format_absent() {
523        let d = CarouselIdentifierDescriptor {
524            carousel_id: 0x0000_0042,
525            format: FormatSpecifier::Absent,
526            private_data: &[0x01, 0x02, 0x03],
527        };
528        let mut buf = vec![0u8; d.serialized_len()];
529        d.serialize_into(&mut buf).unwrap();
530        let re = CarouselIdentifierDescriptor::parse(&buf).unwrap();
531        assert_eq!(re, d);
532    }
533
534    #[test]
535    fn format_absent_no_private_data() {
536        // Minimum body: 5 bytes (carousel_id + FormatId only).
537        let bytes = [TAG, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00];
538        let d = CarouselIdentifierDescriptor::parse(&bytes).unwrap();
539        assert_eq!(d.carousel_id, 0);
540        assert_eq!(d.format, FormatSpecifier::Absent);
541        assert!(d.private_data.is_empty());
542    }
543
544    // ── FormatId=0x02 (Other / private) ──────────────────────────────────────
545
546    #[test]
547    fn parse_format_other_carries_raw_bytes() {
548        // FormatId=0x42 (private), 3 opaque bytes.
549        let bytes = [TAG, 0x08, 0x00, 0x00, 0x00, 0x7F, 0x42, 0xAA, 0xBB, 0xCC];
550        let d = CarouselIdentifierDescriptor::parse(&bytes).unwrap();
551        assert_eq!(d.carousel_id, 0x7F);
552        assert_eq!(d.format.format_id(), 0x42);
553        match &d.format {
554            FormatSpecifier::Other { format_id, bytes } => {
555                assert_eq!(*format_id, 0x42);
556                assert_eq!(*bytes, &[0xAA, 0xBB, 0xCC]);
557            }
558            other => panic!("expected Other, got {other:?}"),
559        }
560        // private_data is always empty for Other (whole remainder in bytes).
561        assert!(d.private_data.is_empty());
562    }
563
564    #[test]
565    fn round_trip_format_other() {
566        let d = CarouselIdentifierDescriptor {
567            carousel_id: 0x0000_00FF,
568            format: FormatSpecifier::Other {
569                format_id: 0x05,
570                bytes: &[0x11, 0x22],
571            },
572            private_data: &[],
573        };
574        let mut buf = vec![0u8; d.serialized_len()];
575        d.serialize_into(&mut buf).unwrap();
576        let re = CarouselIdentifierDescriptor::parse(&buf).unwrap();
577        assert_eq!(re, d);
578    }
579
580    // ── error cases ──────────────────────────────────────────────────────────
581
582    #[test]
583    fn parse_rejects_wrong_tag() {
584        let err = CarouselIdentifierDescriptor::parse(&[0x14, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00])
585            .unwrap_err();
586        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x14, .. }));
587    }
588
589    #[test]
590    fn parse_rejects_short_buffer() {
591        // Only the tag byte — not even the length field present.
592        let err = CarouselIdentifierDescriptor::parse(&[TAG]).unwrap_err();
593        assert!(matches!(err, Error::BufferTooShort { .. }));
594    }
595
596    #[test]
597    fn parse_rejects_body_too_short() {
598        // length=4: 4-byte carousel_id but no FormatId.
599        let err =
600            CarouselIdentifierDescriptor::parse(&[TAG, 0x04, 0x00, 0x00, 0x00, 0x01]).unwrap_err();
601        assert!(matches!(err, Error::InvalidDescriptor { .. }));
602    }
603
604    #[test]
605    fn parse_rejects_format1_fixed_truncated() {
606        // FormatId=0x01 but only 3 bytes after the prefix (need FS1_FIXED_LEN=16).
607        let mut bytes = vec![TAG, 0x08, 0x00, 0x00, 0x00, 0x01, 0x01, 0xAA, 0xBB, 0xCC];
608        bytes[1] = (bytes.len() - 2) as u8;
609        let err = CarouselIdentifierDescriptor::parse(&bytes).unwrap_err();
610        assert!(matches!(err, Error::InvalidDescriptor { .. }));
611    }
612
613    #[test]
614    fn parse_rejects_format1_objectkey_overflow() {
615        // FormatId=0x01 with ObjectKeyLength claiming more bytes than available.
616        // Fixed fields (16 bytes) + ObjectKeyLength=0x0A (10), but no key bytes follow.
617        let body = vec![
618            0x00, 0x00, 0x00, 0x02, // carousel_id
619            0x01, // FormatId
620            0x01, // ModuleVersion
621            0x00, 0x01, // ModuleId
622            0x00, 0x80, // BlockSize
623            0x00, 0x00, 0x20, 0x00, // ModuleSize
624            0x00, // CompressionMethod
625            0x00, 0x00, 0x20, 0x00, // OriginalSize
626            0x05, // TimeOut
627            0x0A, // ObjectKeyLength=10, but no bytes follow
628        ];
629        let mut full = vec![TAG, body.len() as u8];
630        full.extend_from_slice(&body);
631        // Trim to only 1 key byte when 10 were declared.
632        full.push(0xEE);
633        full[1] = (full.len() - 2) as u8;
634        // Re-fix body length in the full buffer.
635        // Actually: full = [tag, len, ...body..., 0xEE] where len was set
636        // for body (22 bytes) but body claims 10 key bytes.  Parse should reject.
637        let _ = body; // already consumed
638        let err = CarouselIdentifierDescriptor::parse(&full).unwrap_err();
639        assert!(matches!(err, Error::InvalidDescriptor { .. }));
640    }
641
642    #[test]
643    fn parse_rejects_length_overrun() {
644        // Declared length=10 but only 4 bytes of body present.
645        let err =
646            CarouselIdentifierDescriptor::parse(&[TAG, 0x0A, 0x00, 0x00, 0x00, 0x01]).unwrap_err();
647        assert!(matches!(err, Error::BufferTooShort { .. }));
648    }
649
650    #[test]
651    fn serialize_rejects_too_small_buffer() {
652        let d = anchor_descriptor();
653        let mut tiny = [0u8; 2];
654        let err = d.serialize_into(&mut tiny).unwrap_err();
655        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
656    }
657
658    // ── serde tests ──────────────────────────────────────────────────────────
659
660    #[cfg(feature = "serde")]
661    #[test]
662    fn serde_serialize_aggregated_fields_present() {
663        let d = anchor_descriptor();
664        let json = serde_json::to_string(&d).unwrap();
665        assert!(json.contains("\"carousel_id\""));
666        assert!(json.contains("\"format\""));
667        // Aggregated variant key.
668        assert!(json.contains("Aggregated") || json.contains("aggregated"));
669        assert!(json.contains("\"module_version\""));
670        assert!(json.contains("\"timeout\""));
671    }
672
673    #[cfg(feature = "serde")]
674    #[test]
675    fn serde_serialize_absent_format() {
676        let d = CarouselIdentifierDescriptor {
677            carousel_id: 0xABCD,
678            format: FormatSpecifier::Absent,
679            private_data: &[],
680        };
681        let json = serde_json::to_string(&d).unwrap();
682        assert!(json.contains("\"carousel_id\""));
683        assert!(json.contains("Absent") || json.contains("absent"));
684    }
685}