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