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        let (prefix, after_prefix) =
199            body.split_first_chunk::<BODY_PREFIX_LEN>()
200                .ok_or(Error::InvalidDescriptor {
201                    tag: TAG,
202                    reason: "carousel_identifier_descriptor body shorter than 5 bytes",
203                })?;
204        let carousel_id = u32::from_be_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]);
205        let format_id = prefix[CAROUSEL_ID_LEN];
206
207        let (format, private_data) = match format_id {
208            FORMAT_ID_NONE => (FormatSpecifier::Absent, after_prefix),
209            FORMAT_ID_AGGREGATED => {
210                let (fs1, after_fixed) = after_prefix.split_first_chunk::<FS1_FIXED_LEN>().ok_or(
211                    Error::InvalidDescriptor {
212                        tag: TAG,
213                        reason: "FormatId=0x01 specifier truncated (insufficient fixed fields)",
214                    },
215                )?;
216                // fs1 layout: [0]=module_version, [1..3]=module_id, [3..5]=block_size,
217                // [5..9]=module_size, [9]=compression_method, [10..14]=original_size,
218                // [14]=timeout, [15]=object_key_length
219                let module_version = fs1[0];
220                let module_id = u16::from_be_bytes([fs1[1], fs1[2]]);
221                let block_size = u16::from_be_bytes([fs1[3], fs1[4]]);
222                let module_size = u32::from_be_bytes([fs1[5], fs1[6], fs1[7], fs1[8]]);
223                let compression_method = fs1[9];
224                let original_size = u32::from_be_bytes([fs1[10], fs1[11], fs1[12], fs1[13]]);
225                let timeout = fs1[14];
226                let object_key_length = fs1[15] as usize;
227                if object_key_length > after_fixed.len() {
228                    return Err(Error::InvalidDescriptor {
229                        tag: TAG,
230                        reason: "FormatId=0x01 ObjectKeyData exceeds descriptor body",
231                    });
232                }
233                let object_key = &after_fixed[..object_key_length];
234                let private_data = &after_fixed[object_key_length..];
235                (
236                    FormatSpecifier::Aggregated {
237                        module_version,
238                        module_id,
239                        block_size,
240                        module_size,
241                        compression_method,
242                        original_size,
243                        timeout,
244                        object_key,
245                    },
246                    private_data,
247                )
248            }
249            other => {
250                // Unknown/private: the boundary between specifier and
251                // private_data is unknowable, so absorb the whole remainder
252                // into the specifier bytes.
253                (
254                    FormatSpecifier::Other {
255                        format_id: other,
256                        bytes: after_prefix,
257                    },
258                    &[][..],
259                )
260            }
261        };
262        Ok(Self {
263            carousel_id,
264            format,
265            private_data,
266        })
267    }
268}
269
270impl Serialize for CarouselIdentifierDescriptor<'_> {
271    type Error = crate::error::Error;
272
273    fn serialized_len(&self) -> usize {
274        let body = BODY_PREFIX_LEN + self.format.serialized_len() + self.private_data.len();
275        HEADER_LEN + body
276    }
277
278    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
279        let total = self.serialized_len();
280        let body_len = BODY_PREFIX_LEN + self.format.serialized_len() + self.private_data.len();
281        if body_len > u8::MAX as usize {
282            return Err(Error::InvalidDescriptor {
283                tag: TAG,
284                reason: "carousel_identifier_descriptor body exceeds 255 bytes",
285            });
286        }
287        if buf.len() < total {
288            return Err(Error::OutputBufferTooSmall {
289                need: total,
290                have: buf.len(),
291            });
292        }
293
294        buf[0] = TAG;
295        buf[1] = body_len as u8;
296        buf[HEADER_LEN..HEADER_LEN + CAROUSEL_ID_LEN]
297            .copy_from_slice(&self.carousel_id.to_be_bytes());
298        buf[HEADER_LEN + CAROUSEL_ID_LEN] = self.format.format_id();
299
300        let mut pos = HEADER_LEN + BODY_PREFIX_LEN;
301        match &self.format {
302            FormatSpecifier::Absent => {}
303            FormatSpecifier::Aggregated {
304                module_version,
305                module_id,
306                block_size,
307                module_size,
308                compression_method,
309                original_size,
310                timeout,
311                object_key,
312            } => {
313                if object_key.len() > u8::MAX as usize {
314                    return Err(Error::InvalidDescriptor {
315                        tag: TAG,
316                        reason: "FormatId=0x01 ObjectKeyData exceeds 255 bytes",
317                    });
318                }
319                buf[pos] = *module_version;
320                pos += FS1_MODULE_VERSION_LEN;
321                buf[pos..pos + FS1_MODULE_ID_LEN].copy_from_slice(&module_id.to_be_bytes());
322                pos += FS1_MODULE_ID_LEN;
323                buf[pos..pos + FS1_BLOCK_SIZE_LEN].copy_from_slice(&block_size.to_be_bytes());
324                pos += FS1_BLOCK_SIZE_LEN;
325                buf[pos..pos + FS1_MODULE_SIZE_LEN].copy_from_slice(&module_size.to_be_bytes());
326                pos += FS1_MODULE_SIZE_LEN;
327                buf[pos] = *compression_method;
328                pos += FS1_COMPRESSION_METHOD_LEN;
329                buf[pos..pos + FS1_ORIGINAL_SIZE_LEN].copy_from_slice(&original_size.to_be_bytes());
330                pos += FS1_ORIGINAL_SIZE_LEN;
331                buf[pos] = *timeout;
332                pos += FS1_TIMEOUT_LEN;
333                buf[pos] = object_key.len() as u8;
334                pos += FS1_OBJECT_KEY_LENGTH_LEN;
335                buf[pos..pos + object_key.len()].copy_from_slice(object_key);
336                pos += object_key.len();
337            }
338            FormatSpecifier::Other { bytes, .. } => {
339                buf[pos..pos + bytes.len()].copy_from_slice(bytes);
340                pos += bytes.len();
341            }
342        }
343        buf[pos..pos + self.private_data.len()].copy_from_slice(self.private_data);
344        Ok(total)
345    }
346}
347
348impl<'a> crate::traits::DescriptorDef<'a> for CarouselIdentifierDescriptor<'a> {
349    const TAG: u8 = TAG;
350    const NAME: &'static str = "CAROUSEL_IDENTIFIER";
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    // ── byte-anchor constants for hand-built test vector ─────────────────────
358    //
359    // Descriptor wire layout (FormatId=0x01, ObjectKeyLength=1, 2 private bytes):
360    //
361    // Offset  Field                 Value      Comment
362    //   [0]   descriptor_tag        0x13
363    //   [1]   descriptor_length     0x18       = 24 bytes body
364    //   [2]   carousel_id[31:24]    0x00
365    //   [3]   carousel_id[23:16]    0x00
366    //   [4]   carousel_id[15:8]     0x00
367    //   [5]   carousel_id[7:0]      0x01       carousel_id = 1
368    //   [6]   FormatId              0x01       = Aggregated
369    //   [7]   ModuleVersion         0x03
370    //   [8]   ModuleId[15:8]        0x00
371    //   [9]   ModuleId[7:0]         0x05       module_id = 5
372    //  [10]   BlockSize[15:8]       0x04
373    //  [11]   BlockSize[7:0]        0x00       block_size = 1024
374    //  [12]   ModuleSize[31:24]     0x00
375    //  [13]   ModuleSize[23:16]     0x00
376    //  [14]   ModuleSize[15:8]      0x10
377    //  [15]   ModuleSize[7:0]       0x00       module_size = 0x1000 = 4096
378    //  [16]   CompressionMethod     0x00       no compression
379    //  [17]   OriginalSize[31:24]   0x00
380    //  [18]   OriginalSize[23:16]   0x00
381    //  [19]   OriginalSize[15:8]    0x10
382    //  [20]   OriginalSize[7:0]     0x00       original_size = 4096
383    //  [21]   TimeOut               0x1E       = 30
384    //  [22]   ObjectKeyLength       0x01       N1 = 1
385    //  [23]   ObjectKeyData[0]      0xAB
386    //  [24]   private_data_byte[0]  0xDE
387    //  [25]   private_data_byte[1]  0xAD
388    //
389    // Total: 26 bytes; body = 24 (0x18).
390
391    #[rustfmt::skip]
392    const ANCHOR_BYTES: &[u8] = &[
393        0x13, 0x18,                         // tag, length=24
394        0x00, 0x00, 0x00, 0x01,             // carousel_id = 1
395        0x01,                               // FormatId = Aggregated
396        0x03,                               // ModuleVersion = 3
397        0x00, 0x05,                         // ModuleId = 5
398        0x04, 0x00,                         // BlockSize = 1024
399        0x00, 0x00, 0x10, 0x00,             // ModuleSize = 4096
400        0x00,                               // CompressionMethod = 0
401        0x00, 0x00, 0x10, 0x00,             // OriginalSize = 4096
402        0x1E,                               // TimeOut = 30
403        0x01,                               // ObjectKeyLength = 1
404        0xAB,                               // ObjectKeyData
405        0xDE, 0xAD,                         // private_data
406    ];
407
408    fn anchor_descriptor() -> CarouselIdentifierDescriptor<'static> {
409        CarouselIdentifierDescriptor {
410            carousel_id: 1,
411            format: FormatSpecifier::Aggregated {
412                module_version: 3,
413                module_id: 5,
414                block_size: 1024,
415                module_size: 4096,
416                compression_method: 0,
417                original_size: 4096,
418                timeout: 30,
419                object_key: &[0xAB],
420            },
421            private_data: &[0xDE, 0xAD],
422        }
423    }
424
425    // ── byte-anchor test ──────────────────────────────────────────────────────
426
427    #[test]
428    fn byte_anchor_serialize_matches_hand_built() {
429        let d = anchor_descriptor();
430        let mut buf = vec![0u8; d.serialized_len()];
431        d.serialize_into(&mut buf).unwrap();
432        assert_eq!(
433            buf.as_slice(),
434            ANCHOR_BYTES,
435            "serialized bytes differ from anchor"
436        );
437    }
438
439    #[test]
440    fn byte_anchor_parse_extracts_correct_fields() {
441        let d = CarouselIdentifierDescriptor::parse(ANCHOR_BYTES).unwrap();
442        assert_eq!(d.carousel_id, 1);
443        assert_eq!(d.private_data, &[0xDE, 0xAD]);
444        match &d.format {
445            FormatSpecifier::Aggregated {
446                module_version,
447                module_id,
448                block_size,
449                module_size,
450                compression_method,
451                original_size,
452                timeout,
453                object_key,
454            } => {
455                assert_eq!(*module_version, 3);
456                assert_eq!(*module_id, 5);
457                assert_eq!(*block_size, 1024);
458                assert_eq!(*module_size, 4096);
459                assert_eq!(*compression_method, 0);
460                assert_eq!(*original_size, 4096);
461                assert_eq!(*timeout, 30);
462                assert_eq!(*object_key, &[0xAB]);
463            }
464            other => panic!("expected Aggregated, got {other:?}"),
465        }
466    }
467
468    #[test]
469    fn byte_anchor_round_trip_byte_identical() {
470        // parse → serialize → byte-identical with the original anchor bytes.
471        let d = CarouselIdentifierDescriptor::parse(ANCHOR_BYTES).unwrap();
472        let mut buf = vec![0u8; d.serialized_len()];
473        d.serialize_into(&mut buf).unwrap();
474        assert_eq!(
475            buf.as_slice(),
476            ANCHOR_BYTES,
477            "round-trip not byte-identical"
478        );
479        // Second pass: serialize the struct literal, parse back, re-serialize.
480        let d2 = anchor_descriptor();
481        let mut buf2 = vec![0u8; d2.serialized_len()];
482        d2.serialize_into(&mut buf2).unwrap();
483        let d3 = CarouselIdentifierDescriptor::parse(&buf2).unwrap();
484        let mut buf3 = vec![0u8; d3.serialized_len()];
485        d3.serialize_into(&mut buf3).unwrap();
486        assert_eq!(buf2, buf3, "struct literal round-trip not byte-identical");
487    }
488
489    // ── FormatId=0x00 (Absent) ────────────────────────────────────────────────
490
491    #[test]
492    fn parse_format_absent_extracts_fields() {
493        // carousel_id=0xDEADBEEF, FormatId=0x00, 3 private bytes.
494        let bytes = [TAG, 0x08, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xAA, 0xBB, 0xCC];
495        let d = CarouselIdentifierDescriptor::parse(&bytes).unwrap();
496        assert_eq!(d.carousel_id, 0xDEAD_BEEF);
497        assert_eq!(d.format, FormatSpecifier::Absent);
498        assert_eq!(d.format.format_id(), 0x00);
499        assert_eq!(d.private_data, &[0xAA, 0xBB, 0xCC]);
500    }
501
502    #[test]
503    fn round_trip_format_absent() {
504        let d = CarouselIdentifierDescriptor {
505            carousel_id: 0x0000_0042,
506            format: FormatSpecifier::Absent,
507            private_data: &[0x01, 0x02, 0x03],
508        };
509        let mut buf = vec![0u8; d.serialized_len()];
510        d.serialize_into(&mut buf).unwrap();
511        let re = CarouselIdentifierDescriptor::parse(&buf).unwrap();
512        assert_eq!(re, d);
513    }
514
515    #[test]
516    fn format_absent_no_private_data() {
517        // Minimum body: 5 bytes (carousel_id + FormatId only).
518        let bytes = [TAG, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00];
519        let d = CarouselIdentifierDescriptor::parse(&bytes).unwrap();
520        assert_eq!(d.carousel_id, 0);
521        assert_eq!(d.format, FormatSpecifier::Absent);
522        assert!(d.private_data.is_empty());
523    }
524
525    // ── FormatId=0x02 (Other / private) ──────────────────────────────────────
526
527    #[test]
528    fn parse_format_other_carries_raw_bytes() {
529        // FormatId=0x42 (private), 3 opaque bytes.
530        let bytes = [TAG, 0x08, 0x00, 0x00, 0x00, 0x7F, 0x42, 0xAA, 0xBB, 0xCC];
531        let d = CarouselIdentifierDescriptor::parse(&bytes).unwrap();
532        assert_eq!(d.carousel_id, 0x7F);
533        assert_eq!(d.format.format_id(), 0x42);
534        match &d.format {
535            FormatSpecifier::Other { format_id, bytes } => {
536                assert_eq!(*format_id, 0x42);
537                assert_eq!(*bytes, &[0xAA, 0xBB, 0xCC]);
538            }
539            other => panic!("expected Other, got {other:?}"),
540        }
541        // private_data is always empty for Other (whole remainder in bytes).
542        assert!(d.private_data.is_empty());
543    }
544
545    #[test]
546    fn round_trip_format_other() {
547        let d = CarouselIdentifierDescriptor {
548            carousel_id: 0x0000_00FF,
549            format: FormatSpecifier::Other {
550                format_id: 0x05,
551                bytes: &[0x11, 0x22],
552            },
553            private_data: &[],
554        };
555        let mut buf = vec![0u8; d.serialized_len()];
556        d.serialize_into(&mut buf).unwrap();
557        let re = CarouselIdentifierDescriptor::parse(&buf).unwrap();
558        assert_eq!(re, d);
559    }
560
561    // ── error cases ──────────────────────────────────────────────────────────
562
563    #[test]
564    fn parse_rejects_wrong_tag() {
565        let err = CarouselIdentifierDescriptor::parse(&[0x14, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00])
566            .unwrap_err();
567        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x14, .. }));
568    }
569
570    #[test]
571    fn parse_rejects_short_buffer() {
572        // Only the tag byte — not even the length field present.
573        let err = CarouselIdentifierDescriptor::parse(&[TAG]).unwrap_err();
574        assert!(matches!(err, Error::BufferTooShort { .. }));
575    }
576
577    #[test]
578    fn parse_rejects_body_too_short() {
579        // length=4: 4-byte carousel_id but no FormatId.
580        let err =
581            CarouselIdentifierDescriptor::parse(&[TAG, 0x04, 0x00, 0x00, 0x00, 0x01]).unwrap_err();
582        assert!(matches!(err, Error::InvalidDescriptor { .. }));
583    }
584
585    #[test]
586    fn parse_rejects_format1_fixed_truncated() {
587        // FormatId=0x01 but only 3 bytes after the prefix (need FS1_FIXED_LEN=16).
588        let mut bytes = vec![TAG, 0x08, 0x00, 0x00, 0x00, 0x01, 0x01, 0xAA, 0xBB, 0xCC];
589        bytes[1] = (bytes.len() - 2) as u8;
590        let err = CarouselIdentifierDescriptor::parse(&bytes).unwrap_err();
591        assert!(matches!(err, Error::InvalidDescriptor { .. }));
592    }
593
594    #[test]
595    fn parse_rejects_format1_objectkey_overflow() {
596        // FormatId=0x01 with ObjectKeyLength claiming more bytes than available.
597        // Fixed fields (16 bytes) + ObjectKeyLength=0x0A (10), but no key bytes follow.
598        let body = vec![
599            0x00, 0x00, 0x00, 0x02, // carousel_id
600            0x01, // FormatId
601            0x01, // ModuleVersion
602            0x00, 0x01, // ModuleId
603            0x00, 0x80, // BlockSize
604            0x00, 0x00, 0x20, 0x00, // ModuleSize
605            0x00, // CompressionMethod
606            0x00, 0x00, 0x20, 0x00, // OriginalSize
607            0x05, // TimeOut
608            0x0A, // ObjectKeyLength=10, but no bytes follow
609        ];
610        let mut full = vec![TAG, body.len() as u8];
611        full.extend_from_slice(&body);
612        // Trim to only 1 key byte when 10 were declared.
613        full.push(0xEE);
614        full[1] = (full.len() - 2) as u8;
615        // Re-fix body length in the full buffer.
616        // Actually: full = [tag, len, ...body..., 0xEE] where len was set
617        // for body (22 bytes) but body claims 10 key bytes.  Parse should reject.
618        let _ = body; // already consumed
619        let err = CarouselIdentifierDescriptor::parse(&full).unwrap_err();
620        assert!(matches!(err, Error::InvalidDescriptor { .. }));
621    }
622
623    #[test]
624    fn parse_rejects_length_overrun() {
625        // Declared length=10 but only 4 bytes of body present.
626        let err =
627            CarouselIdentifierDescriptor::parse(&[TAG, 0x0A, 0x00, 0x00, 0x00, 0x01]).unwrap_err();
628        assert!(matches!(err, Error::BufferTooShort { .. }));
629    }
630
631    #[test]
632    fn serialize_rejects_too_small_buffer() {
633        let d = anchor_descriptor();
634        let mut tiny = [0u8; 2];
635        let err = d.serialize_into(&mut tiny).unwrap_err();
636        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
637    }
638
639    // ── serde tests ──────────────────────────────────────────────────────────
640
641    #[cfg(feature = "serde")]
642    #[test]
643    fn serde_serialize_aggregated_fields_present() {
644        let d = anchor_descriptor();
645        let json = serde_json::to_string(&d).unwrap();
646        assert!(json.contains("\"carousel_id\""));
647        assert!(json.contains("\"format\""));
648        // Aggregated variant key.
649        assert!(json.contains("Aggregated") || json.contains("aggregated"));
650        assert!(json.contains("\"module_version\""));
651        assert!(json.contains("\"timeout\""));
652    }
653
654    #[cfg(feature = "serde")]
655    #[test]
656    fn serde_serialize_absent_format() {
657        let d = CarouselIdentifierDescriptor {
658            carousel_id: 0xABCD,
659            format: FormatSpecifier::Absent,
660            private_data: &[],
661        };
662        let json = serde_json::to_string(&d).unwrap();
663        assert!(json.contains("\"carousel_id\""));
664        assert!(json.contains("Absent") || json.contains("absent"));
665    }
666}