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