Skip to main content

dvb_si/
compatibility.rs

1//! Compatibility Descriptor — ETSI TS 102 006 §9.4.2.2 Table 15 / ISO/IEC 13818-6.
2//!
3//! The `compatibilityDescriptor()` structure describes the hardware/software
4//! compatibility requirements of a DSM-CC download group. It is carried in
5//! DSI/DII messages (ISO/IEC 13818-6 §7.3) and UNT platform entries
6//! (TS 102 006 Table 11).
7//!
8//! Wire layout (Table 15):
9//!
10//! ```text
11//! compatibilityDescriptor() {
12//!   compatibilityDescriptorLength  [15:0]  16 bits  — byte count of everything after this field
13//!   descriptorCount                [15:0]  16 bits
14//!   for (i < descriptorCount) {
15//!     descriptorType                [7:0]   8 bits   — Table 16
16//!     descriptorLength              [7:0]   8 bits   — byte count of the rest of THIS descriptor
17//!     specifierType                 [7:0]   8 bits   — 0x01 = IEEE OUI
18//!     specifierData                 [23:0]  24 bits  — 3-byte IEEE OUI
19//!     model                         [15:0]  16 bits
20//!     version                       [15:0]  16 bits
21//!     subDescriptorCount            [7:0]   8 bits
22//!     for (j < subDescriptorCount) {
23//!       subDescriptorType           [7:0]   8 bits
24//!       subDescriptorLength         [7:0]   8 bits
25//!       subDescriptorData           (subDescriptorLength bytes)
26//!     }
27//!   }
28//! }
29//! ```
30//!
31//! An empty descriptor (`descriptorCount == 0`) is encoded as
32//! `compatibilityDescriptorLength = 0` (2 bytes on wire: `0x00 0x00`),
33//! matching the DSM-CC convention used by existing DVB broadcasts.
34
35use crate::error::{Error, Result};
36use alloc::vec::Vec;
37use dvb_common::{Parse, Serialize};
38
39pub(crate) const COMPAT_DESC_LEN_FIELD: usize = 2;
40const DESC_COUNT_FIELD: usize = 2;
41const DESC_HEADER_LEN: usize = 2;
42const DESC_FIXED_LEN: usize = 9;
43const SUB_DESC_HEADER_LEN: usize = 2;
44
45/// Compatibility descriptor type — TS 102 006 Table 16 / ISO/IEC 13818-6.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize))]
48#[non_exhaustive]
49pub enum DescriptorType {
50    /// 0x00 — pad descriptor.
51    Pad,
52    /// 0x01 — system hardware descriptor.
53    SystemHardware,
54    /// 0x02 — system software descriptor.
55    SystemSoftware,
56    /// 0x03..=0x3F — ISO/IEC 13818-6 reserved.
57    IsoReserved(u8),
58    /// 0x40..=0x7F — reserved for future use.
59    DvbReserved(u8),
60    /// 0x80..=0xFF — user defined.
61    UserDefined(u8),
62}
63
64impl DescriptorType {
65    #[must_use]
66    /// Decode from the wire value.  Every value maps (lossless).
67    pub fn from_u8(v: u8) -> Self {
68        match v {
69            0x00 => Self::Pad,
70            0x01 => Self::SystemHardware,
71            0x02 => Self::SystemSoftware,
72            v if v < 0x40 => Self::IsoReserved(v),
73            v if v < 0x80 => Self::DvbReserved(v),
74            v => Self::UserDefined(v),
75        }
76    }
77
78    #[must_use]
79    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
80    pub fn to_u8(self) -> u8 {
81        match self {
82            Self::Pad => 0x00,
83            Self::SystemHardware => 0x01,
84            Self::SystemSoftware => 0x02,
85            Self::IsoReserved(v) | Self::DvbReserved(v) | Self::UserDefined(v) => v,
86        }
87    }
88
89    #[must_use]
90    /// Human-readable spec display name.
91    pub fn name(self) -> &'static str {
92        match self {
93            Self::Pad => "Pad",
94            Self::SystemHardware => "System Hardware",
95            Self::SystemSoftware => "System Software",
96            Self::IsoReserved(_) => "ISO Reserved",
97            Self::DvbReserved(_) => "DVB Reserved",
98            Self::UserDefined(_) => "User Defined",
99        }
100    }
101}
102dvb_common::impl_spec_display!(DescriptorType, IsoReserved, DvbReserved, UserDefined);
103
104/// Compatibility specifier type — TS 102 006 Table 15 / ISO/IEC 13818-6.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize))]
107#[non_exhaustive]
108pub enum SpecifierType {
109    /// 0x01 — IEEE OUI.
110    IeeeOui,
111    /// Catch-all for other / reserved values.
112    Unallocated(u8),
113}
114
115impl SpecifierType {
116    #[must_use]
117    /// Decode from the wire value.  Every value maps (lossless).
118    pub fn from_u8(v: u8) -> Self {
119        match v {
120            0x01 => Self::IeeeOui,
121            v => Self::Unallocated(v),
122        }
123    }
124
125    #[must_use]
126    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
127    pub fn to_u8(self) -> u8 {
128        match self {
129            Self::IeeeOui => 0x01,
130            Self::Unallocated(v) => v,
131        }
132    }
133
134    #[must_use]
135    /// Human-readable spec display name.
136    pub fn name(self) -> &'static str {
137        match self {
138            Self::IeeeOui => "IEEE OUI",
139            Self::Unallocated(_) => "Unallocated",
140        }
141    }
142}
143dvb_common::impl_spec_display!(SpecifierType, Unallocated);
144
145/// Compatibility sub-descriptor type — ISO/IEC 13818-6.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize))]
148#[non_exhaustive]
149pub enum SubDescriptorType {
150    /// Catch-all carrying the raw byte value for round-trip fidelity.
151    Unallocated(u8),
152}
153
154impl SubDescriptorType {
155    #[must_use]
156    /// Decode from the wire value.  Every value maps (lossless).
157    pub fn from_u8(v: u8) -> Self {
158        Self::Unallocated(v)
159    }
160
161    #[must_use]
162    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
163    pub fn to_u8(self) -> u8 {
164        match self {
165            Self::Unallocated(v) => v,
166        }
167    }
168
169    #[must_use]
170    /// Human-readable spec display name.
171    pub fn name(self) -> &'static str {
172        match self {
173            Self::Unallocated(_) => "Unallocated",
174        }
175    }
176}
177dvb_common::impl_spec_display!(SubDescriptorType, Unallocated);
178
179/// Compatibility Descriptor — ETSI TS 102 006 §9.4.2.2 Table 15 / ISO/IEC
180/// 13818-6 `compatibilityDescriptor()`.
181///
182/// The wire form starts with a 16-bit `compatibilityDescriptorLength` field;
183/// [`Parse`] consumes the **full** block including this length prefix, and
184/// [`Serialize`] emits it. An empty descriptor (no entries) serialises as the
185/// 2-byte `0x00 0x00` length-only form, matching the DSM-CC convention for
186/// "no compatibility information".
187#[derive(Debug, Clone, PartialEq, Eq)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize))]
189pub struct CompatibilityDescriptor<'a> {
190    /// Descriptor entries (may be empty).
191    pub descriptors: Vec<CompatibilityDescriptorEntry<'a>>,
192}
193
194/// A single compatibility descriptor entry — TS 102 006 Table 15 / ISO/IEC
195/// 13818-6.
196///
197/// `descriptorType` values are defined in TS 102 006 Table 16
198/// (0x00 = pad, 0x01 = system hardware, 0x02 = system software,
199/// 0x03–0x3F ISO reserved, 0x40–0x7F reserved, 0x80–0xFF private).
200#[derive(Debug, Clone, PartialEq, Eq)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize))]
202pub struct CompatibilityDescriptorEntry<'a> {
203    /// `descriptorType` — TS 102 006 Table 16 / ISO/IEC 13818-6.
204    pub descriptor_type: DescriptorType,
205    /// `specifierType` — `0x01` = IEEE OUI (Table 15 remark).
206    pub specifier_type: SpecifierType,
207    /// `specifierData` — 3-byte IEEE OUI when `specifierType == 0x01`.
208    pub specifier_data: [u8; 3],
209    /// `model` — zero if transmitted in a manufacturer private location.
210    pub model: u16,
211    /// `version` — zero if transmitted in a manufacturer private location.
212    pub version: u16,
213    /// Sub-descriptor entries.
214    pub sub_descriptors: Vec<SubDescriptor<'a>>,
215}
216
217/// A sub-descriptor within a compatibility descriptor entry — ISO/IEC 13818-6.
218#[derive(Debug, Clone, PartialEq, Eq)]
219#[cfg_attr(feature = "serde", derive(serde::Serialize))]
220pub struct SubDescriptor<'a> {
221    /// `subDescriptorType`.
222    pub sub_descriptor_type: SubDescriptorType,
223    /// `subDescriptorData`.
224    #[cfg_attr(feature = "serde", serde(borrow))]
225    pub data: &'a [u8],
226}
227
228fn entry_serialized_len(entry: &CompatibilityDescriptorEntry) -> usize {
229    DESC_HEADER_LEN
230        + DESC_FIXED_LEN
231        + entry
232            .sub_descriptors
233            .iter()
234            .map(|sd| SUB_DESC_HEADER_LEN + sd.data.len())
235            .sum::<usize>()
236}
237
238impl<'a> Parse<'a> for CompatibilityDescriptor<'a> {
239    type Error = Error;
240
241    fn parse(bytes: &'a [u8]) -> Result<Self> {
242        if bytes.len() < COMPAT_DESC_LEN_FIELD {
243            return Err(Error::BufferTooShort {
244                need: COMPAT_DESC_LEN_FIELD,
245                have: bytes.len(),
246                what: "CompatibilityDescriptor length field",
247            });
248        }
249        let compat_desc_len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
250        let body_end = COMPAT_DESC_LEN_FIELD + compat_desc_len;
251        if body_end > bytes.len() {
252            return Err(Error::SectionLengthOverflow {
253                declared: compat_desc_len,
254                available: bytes.len() - COMPAT_DESC_LEN_FIELD,
255            });
256        }
257        if compat_desc_len == 0 {
258            return Ok(CompatibilityDescriptor {
259                descriptors: Vec::new(),
260            });
261        }
262        if compat_desc_len < DESC_COUNT_FIELD {
263            return Err(Error::BufferTooShort {
264                need: COMPAT_DESC_LEN_FIELD + DESC_COUNT_FIELD,
265                have: bytes.len(),
266                what: "CompatibilityDescriptor descriptorCount",
267            });
268        }
269        let body = &bytes[COMPAT_DESC_LEN_FIELD..body_end];
270        let descriptor_count = u16::from_be_bytes([body[0], body[1]]) as usize;
271        let mut pos = DESC_COUNT_FIELD;
272        let max_entries = (body.len() - DESC_COUNT_FIELD) / (DESC_HEADER_LEN + DESC_FIXED_LEN);
273        let mut descriptors = Vec::with_capacity(descriptor_count.min(max_entries));
274        for _ in 0..descriptor_count {
275            if pos + DESC_HEADER_LEN > body.len() {
276                return Err(Error::BufferTooShort {
277                    need: COMPAT_DESC_LEN_FIELD + pos + DESC_HEADER_LEN,
278                    have: COMPAT_DESC_LEN_FIELD + body.len(),
279                    what: "CompatibilityDescriptor entry header",
280                });
281            }
282            let descriptor_type = DescriptorType::from_u8(body[pos]);
283            let descriptor_length = body[pos + 1] as usize;
284            let entry_end = pos + DESC_HEADER_LEN + descriptor_length;
285            if entry_end > body.len() {
286                return Err(Error::SectionLengthOverflow {
287                    declared: descriptor_length,
288                    available: body.len() - pos - DESC_HEADER_LEN,
289                });
290            }
291            if descriptor_length < DESC_FIXED_LEN {
292                return Err(Error::InvalidDescriptor {
293                    tag: descriptor_type.to_u8(),
294                    reason: "descriptorLength shorter than fixed fields",
295                });
296            }
297            let specifier_type = SpecifierType::from_u8(body[pos + DESC_HEADER_LEN]);
298            let specifier_data = [
299                body[pos + DESC_HEADER_LEN + 1],
300                body[pos + DESC_HEADER_LEN + 2],
301                body[pos + DESC_HEADER_LEN + 3],
302            ];
303            let model = u16::from_be_bytes([
304                body[pos + DESC_HEADER_LEN + 4],
305                body[pos + DESC_HEADER_LEN + 5],
306            ]);
307            let version = u16::from_be_bytes([
308                body[pos + DESC_HEADER_LEN + 6],
309                body[pos + DESC_HEADER_LEN + 7],
310            ]);
311            let sub_descriptor_count = body[pos + DESC_HEADER_LEN + 8] as usize;
312            let sub_desc_start = pos + DESC_HEADER_LEN + DESC_FIXED_LEN;
313            let sub_desc_end = entry_end;
314            let sub_desc_region_len = sub_desc_end.saturating_sub(sub_desc_start);
315            let mut sub_descriptors = Vec::with_capacity(
316                sub_descriptor_count.min(sub_desc_region_len / SUB_DESC_HEADER_LEN),
317            );
318            let mut sub_pos = sub_desc_start;
319            for _ in 0..sub_descriptor_count {
320                if sub_pos + SUB_DESC_HEADER_LEN > sub_desc_end {
321                    return Err(Error::BufferTooShort {
322                        need: COMPAT_DESC_LEN_FIELD + sub_pos + SUB_DESC_HEADER_LEN,
323                        have: COMPAT_DESC_LEN_FIELD + sub_desc_end,
324                        what: "CompatibilityDescriptor subDescriptor header",
325                    });
326                }
327                let sub_descriptor_type = SubDescriptorType::from_u8(body[sub_pos]);
328                let sub_descriptor_length = body[sub_pos + 1] as usize;
329                sub_pos += SUB_DESC_HEADER_LEN;
330                if sub_pos + sub_descriptor_length > sub_desc_end {
331                    return Err(Error::SectionLengthOverflow {
332                        declared: sub_descriptor_length,
333                        available: sub_desc_end - sub_pos,
334                    });
335                }
336                sub_descriptors.push(SubDescriptor {
337                    sub_descriptor_type,
338                    data: &body[sub_pos..sub_pos + sub_descriptor_length],
339                });
340                sub_pos += sub_descriptor_length;
341            }
342            pos = entry_end;
343            descriptors.push(CompatibilityDescriptorEntry {
344                descriptor_type,
345                specifier_type,
346                specifier_data,
347                model,
348                version,
349                sub_descriptors,
350            });
351        }
352        // Reject slack inside compatibilityDescriptorLength — leftover bytes
353        // would be silently dropped and lost on re-serialize.
354        if pos != body.len() {
355            return Err(Error::InvalidDescriptor {
356                tag: 0,
357                reason: "trailing bytes after compatibility descriptor entries",
358            });
359        }
360        Ok(CompatibilityDescriptor { descriptors })
361    }
362}
363
364impl Serialize for CompatibilityDescriptor<'_> {
365    type Error = Error;
366
367    fn serialized_len(&self) -> usize {
368        if self.descriptors.is_empty() {
369            return COMPAT_DESC_LEN_FIELD;
370        }
371        COMPAT_DESC_LEN_FIELD
372            + DESC_COUNT_FIELD
373            + self
374                .descriptors
375                .iter()
376                .map(entry_serialized_len)
377                .sum::<usize>()
378    }
379
380    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
381        let len = self.serialized_len();
382        if buf.len() < len {
383            return Err(Error::OutputBufferTooSmall {
384                need: len,
385                have: buf.len(),
386            });
387        }
388        if self.descriptors.is_empty() {
389            buf[0] = 0x00;
390            buf[1] = 0x00;
391            return Ok(COMPAT_DESC_LEN_FIELD);
392        }
393        let body_len = len - COMPAT_DESC_LEN_FIELD;
394        if body_len > u16::MAX as usize {
395            return Err(Error::SectionLengthOverflow {
396                declared: body_len,
397                available: u16::MAX as usize,
398            });
399        }
400        if self.descriptors.len() > u16::MAX as usize {
401            return Err(Error::SectionLengthOverflow {
402                declared: self.descriptors.len(),
403                available: u16::MAX as usize,
404            });
405        }
406        buf[..COMPAT_DESC_LEN_FIELD].copy_from_slice(&(body_len as u16).to_be_bytes());
407        buf[COMPAT_DESC_LEN_FIELD..COMPAT_DESC_LEN_FIELD + DESC_COUNT_FIELD]
408            .copy_from_slice(&(self.descriptors.len() as u16).to_be_bytes());
409        let mut pos = COMPAT_DESC_LEN_FIELD + DESC_COUNT_FIELD;
410        for entry in &self.descriptors {
411            let entry_body_len = entry_serialized_len(entry) - DESC_HEADER_LEN;
412            if entry_body_len > u8::MAX as usize {
413                return Err(Error::SectionLengthOverflow {
414                    declared: entry_body_len,
415                    available: u8::MAX as usize,
416                });
417            }
418            buf[pos] = entry.descriptor_type.to_u8();
419            buf[pos + 1] = entry_body_len as u8;
420            buf[pos + DESC_HEADER_LEN] = entry.specifier_type.to_u8();
421            buf[pos + DESC_HEADER_LEN + 1..pos + DESC_HEADER_LEN + 4]
422                .copy_from_slice(&entry.specifier_data);
423            buf[pos + DESC_HEADER_LEN + 4..pos + DESC_HEADER_LEN + 6]
424                .copy_from_slice(&entry.model.to_be_bytes());
425            buf[pos + DESC_HEADER_LEN + 6..pos + DESC_HEADER_LEN + 8]
426                .copy_from_slice(&entry.version.to_be_bytes());
427            if entry.sub_descriptors.len() > u8::MAX as usize {
428                return Err(Error::SectionLengthOverflow {
429                    declared: entry.sub_descriptors.len(),
430                    available: u8::MAX as usize,
431                });
432            }
433            buf[pos + DESC_HEADER_LEN + 8] = entry.sub_descriptors.len() as u8;
434            pos += DESC_HEADER_LEN + DESC_FIXED_LEN;
435            for sd in &entry.sub_descriptors {
436                buf[pos] = sd.sub_descriptor_type.to_u8();
437                if sd.data.len() > u8::MAX as usize {
438                    return Err(Error::SectionLengthOverflow {
439                        declared: sd.data.len(),
440                        available: u8::MAX as usize,
441                    });
442                }
443                buf[pos + 1] = sd.data.len() as u8;
444                pos += SUB_DESC_HEADER_LEN;
445                buf[pos..pos + sd.data.len()].copy_from_slice(sd.data);
446                pos += sd.data.len();
447            }
448        }
449        Ok(len)
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn empty_round_trip() {
459        let cd = CompatibilityDescriptor {
460            descriptors: vec![],
461        };
462        let mut buf = vec![0u8; cd.serialized_len()];
463        cd.serialize_into(&mut buf).unwrap();
464        assert_eq!(buf, &[0x00, 0x00]);
465        let re = CompatibilityDescriptor::parse(&buf).unwrap();
466        assert!(re.descriptors.is_empty());
467    }
468
469    #[test]
470    fn empty_with_count_parses_to_empty() {
471        let bytes: &[u8] = &[0x00, 0x02, 0x00, 0x00];
472        let cd = CompatibilityDescriptor::parse(bytes).unwrap();
473        assert!(cd.descriptors.is_empty());
474        let mut buf = vec![0u8; cd.serialized_len()];
475        cd.serialize_into(&mut buf).unwrap();
476        assert_eq!(buf, &[0x00, 0x00]);
477    }
478
479    /// Hand-built wire bytes (not serializer-derived) pinning every field
480    /// position against TS 102 006 Table 15 — catches a mirrored read/write
481    /// layout bug that serializer-built round-trips cannot.
482    #[test]
483    fn hand_built_byte_anchor() {
484        // len=0x11(17), count=1; entry: type=0x01 len=0x0D(13),
485        // specifierType=0x01, OUI 00:15:0A, model 0x1234, version 0x0001,
486        // subCount=1, sub: type=0x05 len=0x02 data AA BB.
487        let bytes: &[u8] = &[
488            0x00, 0x11, 0x00, 0x01, 0x01, 0x0D, 0x01, 0x00, 0x15, 0x0A, 0x12, 0x34, 0x00, 0x01,
489            0x01, 0x05, 0x02, 0xAA, 0xBB,
490        ];
491        let cd = CompatibilityDescriptor::parse(bytes).unwrap();
492        assert_eq!(cd.descriptors.len(), 1);
493        let e = &cd.descriptors[0];
494        assert_eq!(e.descriptor_type, DescriptorType::SystemHardware);
495        assert_eq!(e.specifier_type, SpecifierType::IeeeOui);
496        assert_eq!(e.specifier_data, [0x00, 0x15, 0x0A]);
497        assert_eq!(e.model, 0x1234);
498        assert_eq!(e.version, 0x0001);
499        assert_eq!(e.sub_descriptors.len(), 1);
500        assert_eq!(
501            e.sub_descriptors[0].sub_descriptor_type,
502            SubDescriptorType::Unallocated(0x05)
503        );
504        assert_eq!(e.sub_descriptors[0].data, &[0xAA, 0xBB]);
505        // Byte-identical re-serialize against the hand-built wire.
506        let mut buf = vec![0u8; cd.serialized_len()];
507        cd.serialize_into(&mut buf).unwrap();
508        assert_eq!(buf, bytes);
509    }
510
511    #[test]
512    fn rejects_trailing_bytes() {
513        // len=0x03: count=0 (2 bytes) + 1 slack byte inside the declared length.
514        let bytes: &[u8] = &[0x00, 0x03, 0x00, 0x00, 0xFF];
515        assert!(matches!(
516            CompatibilityDescriptor::parse(bytes).unwrap_err(),
517            Error::InvalidDescriptor { .. }
518        ));
519    }
520
521    #[test]
522    fn rejects_truncated_entry_header() {
523        // count=1 but only 1 byte of the 2-byte entry header present.
524        let bytes: &[u8] = &[0x00, 0x03, 0x00, 0x01, 0x01];
525        assert!(CompatibilityDescriptor::parse(bytes).is_err());
526    }
527
528    #[test]
529    fn rejects_truncated_sub_descriptor() {
530        // descriptorLength=9 (fixed fields only) but subCount=1 → no room for
531        // the sub-descriptor header inside the entry.
532        let bytes: &[u8] = &[
533            0x00, 0x0D, 0x00, 0x01, 0x01, 0x09, 0x01, 0x00, 0x15, 0x0A, 0x12, 0x34, 0x00, 0x01,
534            0x01,
535        ];
536        assert!(CompatibilityDescriptor::parse(bytes).is_err());
537    }
538
539    #[test]
540    fn one_descriptor_with_sub_round_trip() {
541        let cd = CompatibilityDescriptor {
542            descriptors: vec![CompatibilityDescriptorEntry {
543                descriptor_type: DescriptorType::SystemHardware,
544                specifier_type: SpecifierType::IeeeOui,
545                specifier_data: [0x00, 0x15, 0x0A],
546                model: 0x1234,
547                version: 0x0001,
548                sub_descriptors: vec![
549                    SubDescriptor {
550                        sub_descriptor_type: SubDescriptorType::Unallocated(0x01),
551                        data: &[0xAA, 0xBB],
552                    },
553                    SubDescriptor {
554                        sub_descriptor_type: SubDescriptorType::Unallocated(0x02),
555                        data: &[0xCC],
556                    },
557                ],
558            }],
559        };
560        let mut buf = vec![0u8; cd.serialized_len()];
561        cd.serialize_into(&mut buf).unwrap();
562        let re = CompatibilityDescriptor::parse(&buf).unwrap();
563        assert_eq!(re.descriptors.len(), 1);
564        let e = &re.descriptors[0];
565        assert_eq!(e.descriptor_type, DescriptorType::SystemHardware);
566        assert_eq!(e.specifier_type, SpecifierType::IeeeOui);
567        assert_eq!(e.specifier_data, [0x00, 0x15, 0x0A]);
568        assert_eq!(e.model, 0x1234);
569        assert_eq!(e.version, 0x0001);
570        assert_eq!(e.sub_descriptors.len(), 2);
571        assert_eq!(
572            e.sub_descriptors[0].sub_descriptor_type,
573            SubDescriptorType::Unallocated(0x01)
574        );
575        assert_eq!(e.sub_descriptors[0].data, &[0xAA, 0xBB]);
576        assert_eq!(
577            e.sub_descriptors[1].sub_descriptor_type,
578            SubDescriptorType::Unallocated(0x02)
579        );
580        assert_eq!(e.sub_descriptors[1].data, &[0xCC]);
581        let mut buf2 = vec![0u8; cd.serialized_len()];
582        cd.serialize_into(&mut buf2).unwrap();
583        assert_eq!(buf, buf2, "byte-exact re-serialize");
584        assert_eq!(re, cd);
585    }
586
587    #[test]
588    fn two_descriptors_round_trip() {
589        let cd = CompatibilityDescriptor {
590            descriptors: vec![
591                CompatibilityDescriptorEntry {
592                    descriptor_type: DescriptorType::SystemHardware,
593                    specifier_type: SpecifierType::IeeeOui,
594                    specifier_data: [0x00, 0x00, 0x00],
595                    model: 0x0000,
596                    version: 0x0000,
597                    sub_descriptors: vec![],
598                },
599                CompatibilityDescriptorEntry {
600                    descriptor_type: DescriptorType::SystemSoftware,
601                    specifier_type: SpecifierType::IeeeOui,
602                    specifier_data: [0x00, 0x15, 0x5A],
603                    model: 0x0100,
604                    version: 0x0002,
605                    sub_descriptors: vec![SubDescriptor {
606                        sub_descriptor_type: SubDescriptorType::Unallocated(0x80),
607                        data: &[0xDE, 0xAD, 0xBE, 0xEF],
608                    }],
609                },
610            ],
611        };
612        let mut buf = vec![0u8; cd.serialized_len()];
613        cd.serialize_into(&mut buf).unwrap();
614        let re = CompatibilityDescriptor::parse(&buf).unwrap();
615        assert_eq!(re, cd);
616    }
617
618    #[test]
619    fn parse_rejects_short_buffer() {
620        assert!(matches!(
621            CompatibilityDescriptor::parse(&[0x00]).unwrap_err(),
622            Error::BufferTooShort { .. }
623        ));
624    }
625
626    #[test]
627    fn parse_rejects_truncated_body() {
628        assert!(matches!(
629            CompatibilityDescriptor::parse(&[0x00, 0x05, 0x00, 0x01]).unwrap_err(),
630            Error::SectionLengthOverflow { .. }
631        ));
632    }
633
634    #[test]
635    fn parse_rejects_descriptor_length_too_short() {
636        let bytes: &[u8] = &[
637            0x00, 0x06, // compatibilityDescriptorLength = 6
638            0x00, 0x01, // descriptorCount = 1
639            0x01, 0x02, // descriptorType=1, descriptorLength=2 (too short, need 9)
640            0xAA, 0xBB, // 2 bytes of entry body (to satisfy entry_end bounds)
641        ];
642        assert!(matches!(
643            CompatibilityDescriptor::parse(bytes).unwrap_err(),
644            Error::InvalidDescriptor { .. }
645        ));
646    }
647
648    #[test]
649    fn serialize_rejects_small_buffer() {
650        let cd = CompatibilityDescriptor {
651            descriptors: vec![],
652        };
653        assert!(matches!(
654            cd.serialize_into(&mut [0u8; 1]).unwrap_err(),
655            Error::OutputBufferTooSmall { .. }
656        ));
657    }
658}