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 @ 0x03..0x40 => Self::IsoReserved(v),
73            v @ 0x40..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 (b2, _) = bytes
250            .split_first_chunk::<2>()
251            .ok_or(Error::BufferTooShort {
252                need: COMPAT_DESC_LEN_FIELD,
253                have: bytes.len(),
254                what: "CompatibilityDescriptor length field",
255            })?;
256        let compat_desc_len = u16::from_be_bytes(*b2) as usize;
257        let body_end = COMPAT_DESC_LEN_FIELD + compat_desc_len;
258        if body_end > bytes.len() {
259            return Err(Error::SectionLengthOverflow {
260                declared: compat_desc_len,
261                available: bytes.len() - COMPAT_DESC_LEN_FIELD,
262            });
263        }
264        if compat_desc_len == 0 {
265            return Ok(CompatibilityDescriptor {
266                descriptors: Vec::new(),
267            });
268        }
269        if compat_desc_len < DESC_COUNT_FIELD {
270            return Err(Error::BufferTooShort {
271                need: COMPAT_DESC_LEN_FIELD + DESC_COUNT_FIELD,
272                have: bytes.len(),
273                what: "CompatibilityDescriptor descriptorCount",
274            });
275        }
276        let body = &bytes[COMPAT_DESC_LEN_FIELD..body_end];
277        let descriptor_count = u16::from_be_bytes(*body.first_chunk::<2>().unwrap()) as usize;
278        let mut pos = DESC_COUNT_FIELD;
279        let max_entries = (body.len() - DESC_COUNT_FIELD) / (DESC_HEADER_LEN + DESC_FIXED_LEN);
280        let mut descriptors = Vec::with_capacity(descriptor_count.min(max_entries));
281        for _ in 0..descriptor_count {
282            if pos + DESC_HEADER_LEN > body.len() {
283                return Err(Error::BufferTooShort {
284                    need: COMPAT_DESC_LEN_FIELD + pos + DESC_HEADER_LEN,
285                    have: COMPAT_DESC_LEN_FIELD + body.len(),
286                    what: "CompatibilityDescriptor entry header",
287                });
288            }
289            let descriptor_type = DescriptorType::from_u8(body[pos]);
290            let descriptor_length = body[pos + 1] as usize;
291            let entry_end = pos + DESC_HEADER_LEN + descriptor_length;
292            if entry_end > body.len() {
293                return Err(Error::SectionLengthOverflow {
294                    declared: descriptor_length,
295                    available: body.len() - pos - DESC_HEADER_LEN,
296                });
297            }
298            if descriptor_length < DESC_FIXED_LEN {
299                return Err(Error::InvalidDescriptor {
300                    tag: descriptor_type.to_u8(),
301                    reason: "descriptorLength shorter than fixed fields",
302                });
303            }
304            let specifier_type = SpecifierType::from_u8(body[pos + DESC_HEADER_LEN]);
305            let specifier_data = [
306                body[pos + DESC_HEADER_LEN + 1],
307                body[pos + DESC_HEADER_LEN + 2],
308                body[pos + DESC_HEADER_LEN + 3],
309            ];
310            let model = u16::from_be_bytes(
311                *body[pos + DESC_HEADER_LEN + 4..]
312                    .first_chunk::<2>()
313                    .unwrap(),
314            );
315            let version = u16::from_be_bytes(
316                *body[pos + DESC_HEADER_LEN + 6..]
317                    .first_chunk::<2>()
318                    .unwrap(),
319            );
320            let sub_descriptor_count = body[pos + DESC_HEADER_LEN + 8] as usize;
321            let sub_desc_start = pos + DESC_HEADER_LEN + DESC_FIXED_LEN;
322            let sub_desc_end = entry_end;
323            let sub_desc_region_len = sub_desc_end.saturating_sub(sub_desc_start);
324            let mut sub_descriptors = Vec::with_capacity(
325                sub_descriptor_count.min(sub_desc_region_len / SUB_DESC_HEADER_LEN),
326            );
327            let mut sub_pos = sub_desc_start;
328            for _ in 0..sub_descriptor_count {
329                if sub_pos + SUB_DESC_HEADER_LEN > sub_desc_end {
330                    return Err(Error::BufferTooShort {
331                        need: COMPAT_DESC_LEN_FIELD + sub_pos + SUB_DESC_HEADER_LEN,
332                        have: COMPAT_DESC_LEN_FIELD + sub_desc_end,
333                        what: "CompatibilityDescriptor subDescriptor header",
334                    });
335                }
336                let sub_descriptor_type = SubDescriptorType::from_u8(body[sub_pos]);
337                let sub_descriptor_length = body[sub_pos + 1] as usize;
338                sub_pos += SUB_DESC_HEADER_LEN;
339                if sub_pos + sub_descriptor_length > sub_desc_end {
340                    return Err(Error::SectionLengthOverflow {
341                        declared: sub_descriptor_length,
342                        available: sub_desc_end - sub_pos,
343                    });
344                }
345                sub_descriptors.push(SubDescriptor {
346                    sub_descriptor_type,
347                    data: &body[sub_pos..sub_pos + sub_descriptor_length],
348                });
349                sub_pos += sub_descriptor_length;
350            }
351            pos = entry_end;
352            descriptors.push(CompatibilityDescriptorEntry {
353                descriptor_type,
354                specifier_type,
355                specifier_data,
356                model,
357                version,
358                sub_descriptors,
359            });
360        }
361        // Reject slack inside compatibilityDescriptorLength — leftover bytes
362        // would be silently dropped and lost on re-serialize.
363        if pos != body.len() {
364            return Err(Error::InvalidDescriptor {
365                tag: 0,
366                reason: "trailing bytes after compatibility descriptor entries",
367            });
368        }
369        Ok(CompatibilityDescriptor { descriptors })
370    }
371}
372
373impl Serialize for CompatibilityDescriptor<'_> {
374    type Error = Error;
375
376    fn serialized_len(&self) -> usize {
377        if self.descriptors.is_empty() {
378            return COMPAT_DESC_LEN_FIELD;
379        }
380        COMPAT_DESC_LEN_FIELD
381            + DESC_COUNT_FIELD
382            + self
383                .descriptors
384                .iter()
385                .map(entry_serialized_len)
386                .sum::<usize>()
387    }
388
389    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
390        let len = self.serialized_len();
391        if buf.len() < len {
392            return Err(Error::OutputBufferTooSmall {
393                need: len,
394                have: buf.len(),
395            });
396        }
397        if self.descriptors.is_empty() {
398            buf[0] = 0x00;
399            buf[1] = 0x00;
400            return Ok(COMPAT_DESC_LEN_FIELD);
401        }
402        let body_len = len - COMPAT_DESC_LEN_FIELD;
403        if body_len > u16::MAX as usize {
404            return Err(Error::SectionLengthOverflow {
405                declared: body_len,
406                available: u16::MAX as usize,
407            });
408        }
409        if self.descriptors.len() > u16::MAX as usize {
410            return Err(Error::SectionLengthOverflow {
411                declared: self.descriptors.len(),
412                available: u16::MAX as usize,
413            });
414        }
415        buf[..COMPAT_DESC_LEN_FIELD].copy_from_slice(&(body_len as u16).to_be_bytes());
416        buf[COMPAT_DESC_LEN_FIELD..COMPAT_DESC_LEN_FIELD + DESC_COUNT_FIELD]
417            .copy_from_slice(&(self.descriptors.len() as u16).to_be_bytes());
418        let mut pos = COMPAT_DESC_LEN_FIELD + DESC_COUNT_FIELD;
419        for entry in &self.descriptors {
420            let entry_body_len = entry_serialized_len(entry) - DESC_HEADER_LEN;
421            if entry_body_len > u8::MAX as usize {
422                return Err(Error::SectionLengthOverflow {
423                    declared: entry_body_len,
424                    available: u8::MAX as usize,
425                });
426            }
427            buf[pos] = entry.descriptor_type.to_u8();
428            buf[pos + 1] = entry_body_len as u8;
429            buf[pos + DESC_HEADER_LEN] = entry.specifier_type.to_u8();
430            buf[pos + DESC_HEADER_LEN + 1..pos + DESC_HEADER_LEN + 4]
431                .copy_from_slice(&entry.specifier_data);
432            buf[pos + DESC_HEADER_LEN + 4..pos + DESC_HEADER_LEN + 6]
433                .copy_from_slice(&entry.model.to_be_bytes());
434            buf[pos + DESC_HEADER_LEN + 6..pos + DESC_HEADER_LEN + 8]
435                .copy_from_slice(&entry.version.to_be_bytes());
436            if entry.sub_descriptors.len() > u8::MAX as usize {
437                return Err(Error::SectionLengthOverflow {
438                    declared: entry.sub_descriptors.len(),
439                    available: u8::MAX as usize,
440                });
441            }
442            buf[pos + DESC_HEADER_LEN + 8] = entry.sub_descriptors.len() as u8;
443            pos += DESC_HEADER_LEN + DESC_FIXED_LEN;
444            for sd in &entry.sub_descriptors {
445                buf[pos] = sd.sub_descriptor_type.to_u8();
446                if sd.data.len() > u8::MAX as usize {
447                    return Err(Error::SectionLengthOverflow {
448                        declared: sd.data.len(),
449                        available: u8::MAX as usize,
450                    });
451                }
452                buf[pos + 1] = sd.data.len() as u8;
453                pos += SUB_DESC_HEADER_LEN;
454                buf[pos..pos + sd.data.len()].copy_from_slice(sd.data);
455                pos += sd.data.len();
456            }
457        }
458        Ok(len)
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn empty_round_trip() {
468        let cd = CompatibilityDescriptor {
469            descriptors: vec![],
470        };
471        let mut buf = vec![0u8; cd.serialized_len()];
472        cd.serialize_into(&mut buf).unwrap();
473        assert_eq!(buf, &[0x00, 0x00]);
474        let re = CompatibilityDescriptor::parse(&buf).unwrap();
475        assert!(re.descriptors.is_empty());
476    }
477
478    #[test]
479    fn empty_with_count_parses_to_empty() {
480        let bytes: &[u8] = &[0x00, 0x02, 0x00, 0x00];
481        let cd = CompatibilityDescriptor::parse(bytes).unwrap();
482        assert!(cd.descriptors.is_empty());
483        let mut buf = vec![0u8; cd.serialized_len()];
484        cd.serialize_into(&mut buf).unwrap();
485        assert_eq!(buf, &[0x00, 0x00]);
486    }
487
488    /// Hand-built wire bytes (not serializer-derived) pinning every field
489    /// position against TS 102 006 Table 15 — catches a mirrored read/write
490    /// layout bug that serializer-built round-trips cannot.
491    #[test]
492    fn hand_built_byte_anchor() {
493        // len=0x11(17), count=1; entry: type=0x01 len=0x0D(13),
494        // specifierType=0x01, OUI 00:15:0A, model 0x1234, version 0x0001,
495        // subCount=1, sub: type=0x05 len=0x02 data AA BB.
496        let bytes: &[u8] = &[
497            0x00, 0x11, 0x00, 0x01, 0x01, 0x0D, 0x01, 0x00, 0x15, 0x0A, 0x12, 0x34, 0x00, 0x01,
498            0x01, 0x05, 0x02, 0xAA, 0xBB,
499        ];
500        let cd = CompatibilityDescriptor::parse(bytes).unwrap();
501        assert_eq!(cd.descriptors.len(), 1);
502        let e = &cd.descriptors[0];
503        assert_eq!(e.descriptor_type, DescriptorType::SystemHardware);
504        assert_eq!(e.specifier_type, SpecifierType::IeeeOui);
505        assert_eq!(e.specifier_data, [0x00, 0x15, 0x0A]);
506        assert_eq!(e.model, 0x1234);
507        assert_eq!(e.version, 0x0001);
508        assert_eq!(e.sub_descriptors.len(), 1);
509        assert_eq!(
510            e.sub_descriptors[0].sub_descriptor_type,
511            SubDescriptorType::Unallocated(0x05)
512        );
513        assert_eq!(e.sub_descriptors[0].data, &[0xAA, 0xBB]);
514        // Byte-identical re-serialize against the hand-built wire.
515        let mut buf = vec![0u8; cd.serialized_len()];
516        cd.serialize_into(&mut buf).unwrap();
517        assert_eq!(buf, bytes);
518    }
519
520    #[test]
521    fn rejects_trailing_bytes() {
522        // len=0x03: count=0 (2 bytes) + 1 slack byte inside the declared length.
523        let bytes: &[u8] = &[0x00, 0x03, 0x00, 0x00, 0xFF];
524        assert!(matches!(
525            CompatibilityDescriptor::parse(bytes).unwrap_err(),
526            Error::InvalidDescriptor { .. }
527        ));
528    }
529
530    #[test]
531    fn rejects_truncated_entry_header() {
532        // count=1 but only 1 byte of the 2-byte entry header present.
533        let bytes: &[u8] = &[0x00, 0x03, 0x00, 0x01, 0x01];
534        assert!(CompatibilityDescriptor::parse(bytes).is_err());
535    }
536
537    #[test]
538    fn rejects_truncated_sub_descriptor() {
539        // descriptorLength=9 (fixed fields only) but subCount=1 → no room for
540        // the sub-descriptor header inside the entry.
541        let bytes: &[u8] = &[
542            0x00, 0x0D, 0x00, 0x01, 0x01, 0x09, 0x01, 0x00, 0x15, 0x0A, 0x12, 0x34, 0x00, 0x01,
543            0x01,
544        ];
545        assert!(CompatibilityDescriptor::parse(bytes).is_err());
546    }
547
548    #[test]
549    fn one_descriptor_with_sub_round_trip() {
550        let cd = CompatibilityDescriptor {
551            descriptors: vec![CompatibilityDescriptorEntry {
552                descriptor_type: DescriptorType::SystemHardware,
553                specifier_type: SpecifierType::IeeeOui,
554                specifier_data: [0x00, 0x15, 0x0A],
555                model: 0x1234,
556                version: 0x0001,
557                sub_descriptors: vec![
558                    SubDescriptor {
559                        sub_descriptor_type: SubDescriptorType::Unallocated(0x01),
560                        data: &[0xAA, 0xBB],
561                    },
562                    SubDescriptor {
563                        sub_descriptor_type: SubDescriptorType::Unallocated(0x02),
564                        data: &[0xCC],
565                    },
566                ],
567            }],
568        };
569        let mut buf = vec![0u8; cd.serialized_len()];
570        cd.serialize_into(&mut buf).unwrap();
571        let re = CompatibilityDescriptor::parse(&buf).unwrap();
572        assert_eq!(re.descriptors.len(), 1);
573        let e = &re.descriptors[0];
574        assert_eq!(e.descriptor_type, DescriptorType::SystemHardware);
575        assert_eq!(e.specifier_type, SpecifierType::IeeeOui);
576        assert_eq!(e.specifier_data, [0x00, 0x15, 0x0A]);
577        assert_eq!(e.model, 0x1234);
578        assert_eq!(e.version, 0x0001);
579        assert_eq!(e.sub_descriptors.len(), 2);
580        assert_eq!(
581            e.sub_descriptors[0].sub_descriptor_type,
582            SubDescriptorType::Unallocated(0x01)
583        );
584        assert_eq!(e.sub_descriptors[0].data, &[0xAA, 0xBB]);
585        assert_eq!(
586            e.sub_descriptors[1].sub_descriptor_type,
587            SubDescriptorType::Unallocated(0x02)
588        );
589        assert_eq!(e.sub_descriptors[1].data, &[0xCC]);
590        let mut buf2 = vec![0u8; cd.serialized_len()];
591        cd.serialize_into(&mut buf2).unwrap();
592        assert_eq!(buf, buf2, "byte-exact re-serialize");
593        assert_eq!(re, cd);
594    }
595
596    #[test]
597    fn two_descriptors_round_trip() {
598        let cd = CompatibilityDescriptor {
599            descriptors: vec![
600                CompatibilityDescriptorEntry {
601                    descriptor_type: DescriptorType::SystemHardware,
602                    specifier_type: SpecifierType::IeeeOui,
603                    specifier_data: [0x00, 0x00, 0x00],
604                    model: 0x0000,
605                    version: 0x0000,
606                    sub_descriptors: vec![],
607                },
608                CompatibilityDescriptorEntry {
609                    descriptor_type: DescriptorType::SystemSoftware,
610                    specifier_type: SpecifierType::IeeeOui,
611                    specifier_data: [0x00, 0x15, 0x5A],
612                    model: 0x0100,
613                    version: 0x0002,
614                    sub_descriptors: vec![SubDescriptor {
615                        sub_descriptor_type: SubDescriptorType::Unallocated(0x80),
616                        data: &[0xDE, 0xAD, 0xBE, 0xEF],
617                    }],
618                },
619            ],
620        };
621        let mut buf = vec![0u8; cd.serialized_len()];
622        cd.serialize_into(&mut buf).unwrap();
623        let re = CompatibilityDescriptor::parse(&buf).unwrap();
624        assert_eq!(re, cd);
625    }
626
627    #[test]
628    fn parse_rejects_short_buffer() {
629        assert!(matches!(
630            CompatibilityDescriptor::parse(&[0x00]).unwrap_err(),
631            Error::BufferTooShort { .. }
632        ));
633    }
634
635    #[test]
636    fn parse_rejects_truncated_body() {
637        assert!(matches!(
638            CompatibilityDescriptor::parse(&[0x00, 0x05, 0x00, 0x01]).unwrap_err(),
639            Error::SectionLengthOverflow { .. }
640        ));
641    }
642
643    #[test]
644    fn parse_rejects_descriptor_length_too_short() {
645        let bytes: &[u8] = &[
646            0x00, 0x06, // compatibilityDescriptorLength = 6
647            0x00, 0x01, // descriptorCount = 1
648            0x01, 0x02, // descriptorType=1, descriptorLength=2 (too short, need 9)
649            0xAA, 0xBB, // 2 bytes of entry body (to satisfy entry_end bounds)
650        ];
651        assert!(matches!(
652            CompatibilityDescriptor::parse(bytes).unwrap_err(),
653            Error::InvalidDescriptor { .. }
654        ));
655    }
656
657    #[test]
658    fn serialize_rejects_small_buffer() {
659        let cd = CompatibilityDescriptor {
660            descriptors: vec![],
661        };
662        assert!(matches!(
663            cd.serialize_into(&mut [0u8; 1]).unwrap_err(),
664            Error::OutputBufferTooSmall { .. }
665        ));
666    }
667}