Skip to main content

dvb_si/tables/
protection_message.rs

1//! Protection message sections — ETSI TS 102 809 v1.3.1 §9 (table_id 0x7B).
2//!
3//! Protection messages are MPEG-2 private sections (ISO/IEC 13818-1) used by
4//! the application-signalling authentication scheme. They share one table_id
5//! (0x7B) and are discriminated by the 16-bit `table_id_extension`
6//! ([`ProtectionMessageBody`], §9.3.4 Table 41, PDF p. 66):
7//!
8//! - `0x0000..=0x00FF` — **authentication message** (§9.4.3 Table 42, PDF
9//!   pp. 70-71). The extension *is* the `authentication_group_id`. Carries a
10//!   loop of section hashes plus a detached signature.
11//! - `0x0100` — **certificate collection message** (§9.5.4.9 Table 51, PDF
12//!   p. 91). The extension is the fixed `trust_message_id` 0x0100. Carries a
13//!   count-prefixed list of DER-encoded DVBCertificates.
14//! - `0x0101..=0xFFFF` — reserved for future use; preserved as a raw body so
15//!   parse → serialize stays byte-exact ([`ProtectionMessageBody::Raw`]).
16//!
17//! Mirrors the SAT precedent (`sat.rs`): a typed common section header plus a
18//! discriminated typed body. Variable-length inner loops (hash entries,
19//! certificate slices) are exposed as borrowed slices.
20//!
21//! Per crate contract this parser does NOT verify CRC_32 (use
22//! `Section::validate_crc`). Reserved bits are ignored on parse and emitted as
23//! 1s on serialize, except spec-mandated zero fields which are emitted 0.
24
25use crate::error::{Error, Result};
26use alloc::vec::Vec;
27use dvb_common::{Parse, Serialize};
28
29/// table_id for all protection messages (TS 102 809 §9.3.4; coded per EN 300 468 §5.1.3).
30pub const TABLE_ID: u8 = 0x7B;
31/// Protection messages have no well-known PID — they are carried on the PID(s)
32/// of the protectable elementary stream, signalled via the protection message
33/// descriptor (§9.3.3). Mirrors the `dsmcc.rs` "no fixed PID" convention.
34pub const PID: u16 = 0x0000;
35
36/// `table_id_extension` value (inclusive) of the first authentication message (§9.3.4 Table 41).
37pub const AUTH_EXTENSION_FIRST: u16 = 0x0000;
38/// `table_id_extension` value (inclusive) of the last authentication message (§9.3.4 Table 41).
39pub const AUTH_EXTENSION_LAST: u16 = 0x00FF;
40/// `table_id_extension` (`trust_message_id`) of the certificate collection message (§9.3.4 Table 41).
41pub const CERTIFICATE_COLLECTION_EXTENSION: u16 = 0x0100;
42
43/// Reference type coding — ETSI TS 102 809 §9.4.3 Table 45.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize))]
46#[non_exhaustive]
47pub enum ReferenceType {
48    /// 0x0 — reserved for future use.
49    Reserved,
50    /// 0x1 — same ES (table_id of the payload section).
51    SameEs,
52    /// 0x2 — component tag ES (component_tag of the payload section).
53    ComponentTagEs,
54    /// 0x3..=0xF — reserved.
55    Unallocated(u8),
56}
57
58impl ReferenceType {
59    #[must_use]
60    /// Decode from the wire value.  Every value maps (lossless).
61    pub fn from_u8(v: u8) -> Self {
62        match v & 0x0F {
63            0x0 => Self::Reserved,
64            0x1 => Self::SameEs,
65            0x2 => Self::ComponentTagEs,
66            v => Self::Unallocated(v),
67        }
68    }
69
70    #[must_use]
71    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
72    pub fn to_u8(self) -> u8 {
73        match self {
74            Self::Reserved => 0x0,
75            Self::SameEs => 0x1,
76            Self::ComponentTagEs => 0x2,
77            Self::Unallocated(v) => v,
78        }
79    }
80
81    #[must_use]
82    /// Human-readable spec display name.
83    pub fn name(self) -> &'static str {
84        match self {
85            Self::Reserved => "Reserved",
86            Self::SameEs => "Same ES",
87            Self::ComponentTagEs => "Component Tag ES",
88            Self::Unallocated(_) => "Unallocated",
89        }
90    }
91}
92dvb_common::impl_spec_display!(ReferenceType, Unallocated);
93
94/// table_id(1) + section_length hi/lo(2) + extension(2) + version/cni(1)
95/// + section_number(1) + last_section_number(1) = 8-byte common header.
96const HEADER_LEN: usize = 8;
97/// `section_length` counts from just after the field (byte 3) to end of section.
98const SECTION_LENGTH_PREFIX: usize = 3;
99/// CRC_32 trailer.
100const CRC_LEN: usize = 4;
101
102/// Authentication-message fixed body bytes after the common header, before the
103/// hash loop: section_hash_algorithm_identifier(1) + section_hash_length(1)
104/// + signature_algorithm_identifier(1) + reserved(4)|section_hashes_loop_length(12)(2).
105const AUTH_FIXED_PREFIX: usize = 5;
106
107/// One entry in the authentication message section-hash loop (§9.4.3 Table 42).
108///
109/// Each entry pairs a reference (locating the payload section the hash covers)
110/// with the truncated hash itself. `reference` length is the 4-bit
111/// `reference_length`; `hash` length is the section-wide `section_hash_length`.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
115pub struct SectionHashEntry<'a> {
116    /// 4-bit `reference_type` (§9.4.3 Table 45): 1 = same ES, 2 = component_tag ES.
117    pub reference_type: ReferenceType,
118    /// `reference_byte` field — its semantics depend on `reference_type`.
119    pub reference: &'a [u8],
120    /// The (possibly truncated) section hash, `section_hash_length` bytes.
121    pub hash: &'a [u8],
122}
123
124/// Discriminated protection-message body, selected by `table_id_extension`.
125#[derive(Debug, Clone, PartialEq, Eq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize))]
127#[non_exhaustive]
128pub enum ProtectionMessageBody<'a> {
129    /// Authentication message (extension `0x0000..=0x00FF`; §9.4.3 Table 42).
130    AuthenticationMessage {
131        /// `section_hash_algorithm_identifier` (§9.4.3 Table 43): 0 = SHA-256, 1 = SHA-512.
132        section_hash_algorithm_identifier: u8,
133        /// `section_hash_length` — bytes per hash in each loop entry.
134        section_hash_length: u8,
135        /// `signature_algorithm_identifier` (§9.4.3 Table 44).
136        signature_algorithm_identifier: u8,
137        /// Section-hash loop entries in wire order.
138        hashes: Vec<SectionHashEntry<'a>>,
139        /// `extension_byte` payload (length-prefixed by `extension_bytes_length`).
140        extension_bytes: &'a [u8],
141        /// `signature_key_identifier_byte` payload (length-prefixed).
142        signature_key_identifier: &'a [u8],
143        /// Detached signature — runs from after the key identifier to the CRC.
144        signature: &'a [u8],
145    },
146    /// Certificate collection message (extension `0x0100`; §9.5.4.9 Table 51).
147    CertificateCollection {
148        /// DER-encoded DVBCertificate byte runs, one slice per `certificate_length` loop entry.
149        certificates: Vec<&'a [u8]>,
150    },
151    /// Reserved extension (`0x0101..=0xFFFF`) — body preserved verbatim.
152    Raw(&'a [u8]),
153}
154
155/// Protection message section (TS 102 809 §9; Tables 42 / 51).
156///
157/// Typed fields cover the common section header; [`ProtectionMessageSection::body`]
158/// carries the typed, discriminated body.
159#[derive(Debug, Clone, PartialEq, Eq)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize))]
161#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
162pub struct ProtectionMessageSection<'a> {
163    /// `table_id_extension` — `authentication_group_id` for authentication
164    /// messages, `trust_message_id` (0x0100) for certificate collections.
165    pub table_id_extension: u16,
166    /// 5-bit version_number.
167    pub version_number: u8,
168    /// current_next_indicator bit (spec mandates 1).
169    pub current_next_indicator: bool,
170    /// section_number.
171    pub section_number: u8,
172    /// last_section_number.
173    pub last_section_number: u8,
174    /// Discriminated body, selected by `table_id_extension`.
175    pub body: ProtectionMessageBody<'a>,
176}
177
178impl<'a> Parse<'a> for ProtectionMessageSection<'a> {
179    type Error = crate::error::Error;
180    fn parse(bytes: &'a [u8]) -> Result<Self> {
181        let min_len = HEADER_LEN + CRC_LEN;
182        if bytes.len() < min_len {
183            return Err(Error::BufferTooShort {
184                need: min_len,
185                have: bytes.len(),
186                what: "ProtectionMessageSection",
187            });
188        }
189        if bytes[0] != TABLE_ID {
190            return Err(Error::UnexpectedTableId {
191                table_id: bytes[0],
192                what: "ProtectionMessageSection",
193                expected: &[TABLE_ID],
194            });
195        }
196        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
197        let total = super::check_section_length(
198            bytes.len(),
199            SECTION_LENGTH_PREFIX,
200            section_length,
201            HEADER_LEN + CRC_LEN,
202        )?;
203
204        let table_id_extension = u16::from_be_bytes([bytes[3], bytes[4]]);
205        let version_number = (bytes[5] >> 1) & 0x1F;
206        let current_next_indicator = bytes[5] & 0x01 != 0;
207        let section_number = bytes[6];
208        let last_section_number = bytes[7];
209
210        let body_bytes = &bytes[HEADER_LEN..total - CRC_LEN];
211        let body = match table_id_extension {
212            AUTH_EXTENSION_FIRST..=AUTH_EXTENSION_LAST => parse_authentication_message(body_bytes)?,
213            CERTIFICATE_COLLECTION_EXTENSION => parse_certificate_collection(body_bytes)?,
214            _ => ProtectionMessageBody::Raw(body_bytes),
215        };
216
217        Ok(ProtectionMessageSection {
218            table_id_extension,
219            version_number,
220            current_next_indicator,
221            section_number,
222            last_section_number,
223            body,
224        })
225    }
226}
227
228/// Parse the authentication-message body (§9.4.3 Table 42, PDF pp. 70-71).
229fn parse_authentication_message(body: &[u8]) -> Result<ProtectionMessageBody<'_>> {
230    if body.len() < AUTH_FIXED_PREFIX {
231        return Err(Error::BufferTooShort {
232            need: AUTH_FIXED_PREFIX,
233            have: body.len(),
234            what: "ProtectionMessageSection::AuthenticationMessage",
235        });
236    }
237    let section_hash_algorithm_identifier = body[0];
238    let section_hash_length = body[1];
239    let signature_algorithm_identifier = body[2];
240    // bytes[3] high nibble = reserved; section_hashes_loop_length is 12 bits.
241    let section_hashes_loop_length = (((body[3] & 0x0F) as usize) << 8) | body[4] as usize;
242
243    let loop_start = AUTH_FIXED_PREFIX;
244    let loop_end = loop_start + section_hashes_loop_length;
245    if loop_end > body.len() {
246        return Err(Error::SectionLengthOverflow {
247            declared: section_hashes_loop_length,
248            available: body.len() - loop_start,
249        });
250    }
251
252    let hash_len = section_hash_length as usize;
253    let mut hashes = Vec::new();
254    let mut pos = loop_start;
255    while pos < loop_end {
256        // reference_type(4) | reference_length(4)
257        let lead = body[pos];
258        let reference_type = ReferenceType::from_u8(lead >> 4);
259        let reference_length = (lead & 0x0F) as usize;
260        let ref_start = pos + 1;
261        let ref_end = ref_start + reference_length;
262        let hash_end = ref_end + hash_len;
263        if hash_end > loop_end {
264            return Err(Error::SectionLengthOverflow {
265                declared: reference_length + hash_len,
266                available: loop_end - ref_start,
267            });
268        }
269        hashes.push(SectionHashEntry {
270            reference_type,
271            reference: &body[ref_start..ref_end],
272            hash: &body[ref_end..hash_end],
273        });
274        pos = hash_end;
275    }
276
277    // extension_bytes_length(8) + extension bytes (§9.4.3 Table 42 tail).
278    if loop_end >= body.len() {
279        return Err(Error::BufferTooShort {
280            need: loop_end + 1,
281            have: body.len(),
282            what: "ProtectionMessageSection::extension_bytes_length",
283        });
284    }
285    let extension_bytes_length = body[loop_end] as usize;
286    let ext_start = loop_end + 1;
287    let ext_end = ext_start + extension_bytes_length;
288    if ext_end > body.len() {
289        return Err(Error::SectionLengthOverflow {
290            declared: extension_bytes_length,
291            available: body.len() - ext_start,
292        });
293    }
294
295    // signature_key_identifier_length(8) + key id bytes (Table 43 spillover, PDF p. 71).
296    if ext_end >= body.len() {
297        return Err(Error::BufferTooShort {
298            need: ext_end + 1,
299            have: body.len(),
300            what: "ProtectionMessageSection::signature_key_identifier_length",
301        });
302    }
303    let key_id_length = body[ext_end] as usize;
304    let key_start = ext_end + 1;
305    let key_end = key_start + key_id_length;
306    if key_end > body.len() {
307        return Err(Error::SectionLengthOverflow {
308            declared: key_id_length,
309            available: body.len() - key_start,
310        });
311    }
312
313    // signature_byte loop runs to the end of the body (i.e. up to the CRC_32).
314    let signature = &body[key_end..];
315
316    Ok(ProtectionMessageBody::AuthenticationMessage {
317        section_hash_algorithm_identifier,
318        section_hash_length,
319        signature_algorithm_identifier,
320        hashes,
321        extension_bytes: &body[ext_start..ext_end],
322        signature_key_identifier: &body[key_start..key_end],
323        signature,
324    })
325}
326
327/// Parse the certificate-collection body (§9.5.4.9 Table 51, PDF p. 91).
328fn parse_certificate_collection(body: &[u8]) -> Result<ProtectionMessageBody<'_>> {
329    if body.is_empty() {
330        return Err(Error::BufferTooShort {
331            need: 1,
332            have: 0,
333            what: "ProtectionMessageSection::CertificateCollection",
334        });
335    }
336    // byte 0: reserved(4) | certificate_count(4)
337    let certificate_count = (body[0] & 0x0F) as usize;
338    let mut certificates = Vec::with_capacity(certificate_count);
339    let mut pos = 1;
340    for _ in 0..certificate_count {
341        if pos + 2 > body.len() {
342            return Err(Error::BufferTooShort {
343                need: pos + 2,
344                have: body.len(),
345                what: "ProtectionMessageSection::certificate_length",
346            });
347        }
348        // reserved(4) | certificate_length(12)
349        let certificate_length = (((body[pos] & 0x0F) as usize) << 8) | body[pos + 1] as usize;
350        let cert_start = pos + 2;
351        let cert_end = cert_start + certificate_length;
352        if cert_end > body.len() {
353            return Err(Error::SectionLengthOverflow {
354                declared: certificate_length,
355                available: body.len() - cert_start,
356            });
357        }
358        certificates.push(&body[cert_start..cert_end]);
359        pos = cert_end;
360    }
361    Ok(ProtectionMessageBody::CertificateCollection { certificates })
362}
363
364impl ProtectionMessageBody<'_> {
365    /// Serialized length of the body (excluding common header and CRC).
366    fn body_len(&self) -> usize {
367        match self {
368            ProtectionMessageBody::AuthenticationMessage {
369                hashes,
370                extension_bytes,
371                signature_key_identifier,
372                signature,
373                ..
374            } => {
375                let loop_bytes: usize = hashes
376                    .iter()
377                    .map(|h| 1 + h.reference.len() + h.hash.len())
378                    .sum();
379                AUTH_FIXED_PREFIX
380                    + loop_bytes
381                    + 1
382                    + extension_bytes.len()
383                    + 1
384                    + signature_key_identifier.len()
385                    + signature.len()
386            }
387            ProtectionMessageBody::CertificateCollection { certificates } => {
388                1 + certificates.iter().map(|c| 2 + c.len()).sum::<usize>()
389            }
390            ProtectionMessageBody::Raw(raw) => raw.len(),
391        }
392    }
393
394    /// Write the body into `buf`, returning the number of bytes written.
395    fn write_into(&self, buf: &mut [u8]) -> Result<usize> {
396        match self {
397            ProtectionMessageBody::AuthenticationMessage {
398                section_hash_algorithm_identifier,
399                section_hash_length,
400                signature_algorithm_identifier,
401                hashes,
402                extension_bytes,
403                signature_key_identifier,
404                signature,
405            } => {
406                buf[0] = *section_hash_algorithm_identifier;
407                buf[1] = *section_hash_length;
408                buf[2] = *signature_algorithm_identifier;
409                let loop_bytes: usize = hashes
410                    .iter()
411                    .map(|h| 1 + h.reference.len() + h.hash.len())
412                    .sum();
413                if loop_bytes > 0x0FFF {
414                    return Err(Error::SectionLengthOverflow {
415                        declared: loop_bytes,
416                        available: 0x0FFF,
417                    });
418                }
419                if extension_bytes.len() > u8::MAX as usize {
420                    return Err(Error::SectionLengthOverflow {
421                        declared: extension_bytes.len(),
422                        available: u8::MAX as usize,
423                    });
424                }
425                if signature_key_identifier.len() > u8::MAX as usize {
426                    return Err(Error::SectionLengthOverflow {
427                        declared: signature_key_identifier.len(),
428                        available: u8::MAX as usize,
429                    });
430                }
431                // reserved(4) emitted 1s | section_hashes_loop_length(12).
432                buf[3] = 0xF0 | ((loop_bytes >> 8) as u8 & 0x0F);
433                buf[4] = (loop_bytes & 0xFF) as u8;
434                let mut pos = AUTH_FIXED_PREFIX;
435                for h in hashes {
436                    if h.reference.len() > 0x0F {
437                        return Err(Error::SectionLengthOverflow {
438                            declared: h.reference.len(),
439                            available: 0x0F,
440                        });
441                    }
442                    buf[pos] = (h.reference_type.to_u8() << 4) | (h.reference.len() as u8 & 0x0F);
443                    pos += 1;
444                    buf[pos..pos + h.reference.len()].copy_from_slice(h.reference);
445                    pos += h.reference.len();
446                    buf[pos..pos + h.hash.len()].copy_from_slice(h.hash);
447                    pos += h.hash.len();
448                }
449                buf[pos] = extension_bytes.len() as u8;
450                pos += 1;
451                buf[pos..pos + extension_bytes.len()].copy_from_slice(extension_bytes);
452                pos += extension_bytes.len();
453                buf[pos] = signature_key_identifier.len() as u8;
454                pos += 1;
455                buf[pos..pos + signature_key_identifier.len()]
456                    .copy_from_slice(signature_key_identifier);
457                pos += signature_key_identifier.len();
458                buf[pos..pos + signature.len()].copy_from_slice(signature);
459                pos += signature.len();
460                Ok(pos)
461            }
462            ProtectionMessageBody::CertificateCollection { certificates } => {
463                if certificates.len() > 0x0F {
464                    return Err(Error::SectionLengthOverflow {
465                        declared: certificates.len(),
466                        available: 0x0F,
467                    });
468                }
469                // reserved(4) emitted 1s | certificate_count(4).
470                buf[0] = 0xF0 | (certificates.len() as u8 & 0x0F);
471                let mut pos = 1;
472                for c in certificates {
473                    if c.len() > 0x0FFF {
474                        return Err(Error::SectionLengthOverflow {
475                            declared: c.len(),
476                            available: 0x0FFF,
477                        });
478                    }
479                    // reserved(4) emitted 1s | certificate_length(12).
480                    buf[pos] = 0xF0 | ((c.len() >> 8) as u8 & 0x0F);
481                    buf[pos + 1] = (c.len() & 0xFF) as u8;
482                    pos += 2;
483                    buf[pos..pos + c.len()].copy_from_slice(c);
484                    pos += c.len();
485                }
486                Ok(pos)
487            }
488            ProtectionMessageBody::Raw(raw) => {
489                buf[..raw.len()].copy_from_slice(raw);
490                Ok(raw.len())
491            }
492        }
493    }
494}
495
496impl Serialize for ProtectionMessageSection<'_> {
497    type Error = crate::error::Error;
498    fn serialized_len(&self) -> usize {
499        HEADER_LEN + self.body.body_len() + CRC_LEN
500    }
501    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
502        let len = self.serialized_len();
503        if buf.len() < len {
504            return Err(Error::OutputBufferTooSmall {
505                need: len,
506                have: buf.len(),
507            });
508        }
509        let section_length = (len - SECTION_LENGTH_PREFIX) as u16;
510        buf[0] = TABLE_ID;
511        buf[1] = super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F);
512        buf[2] = (section_length & 0xFF) as u8;
513        buf[3..5].copy_from_slice(&self.table_id_extension.to_be_bytes());
514        // reserved(2)=11, version_number(5), current_next_indicator(1).
515        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
516        buf[6] = self.section_number;
517        buf[7] = self.last_section_number;
518        let body_written = self.body.write_into(&mut buf[HEADER_LEN..])?;
519        let body_end = HEADER_LEN + body_written;
520        let crc = dvb_common::crc32_mpeg2::compute(&buf[..body_end]);
521        buf[body_end..len].copy_from_slice(&crc.to_be_bytes());
522        Ok(len)
523    }
524}
525impl<'a> crate::traits::TableDef<'a> for ProtectionMessageSection<'a> {
526    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
527    const NAME: &'static str = "PROTECTION_MESSAGE";
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    /// Wrap a body in the 8-byte common header + placeholder CRC.
535    fn build_section(extension: u16, version: u8, body: &[u8]) -> Vec<u8> {
536        let section_length = (HEADER_LEN - SECTION_LENGTH_PREFIX + body.len() + CRC_LEN) as u16;
537        let mut v = vec![
538            TABLE_ID,
539            super::super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F),
540            (section_length & 0xFF) as u8,
541            (extension >> 8) as u8,
542            (extension & 0xFF) as u8,
543            0xC0 | (version << 1) | 0x01,
544            0x00,
545            0x00,
546        ];
547        v.extend_from_slice(body);
548        v.extend_from_slice(&[0, 0, 0, 0]);
549        v
550    }
551
552    /// Build an authentication-message body: one hash entry + ext + key id + signature.
553    fn auth_body() -> Vec<u8> {
554        let reference = [0x01]; // reference_type 1 => table_id of payload section
555        let hash = [0xAA, 0xBB, 0xCC, 0xDD]; // section_hash_length = 4
556        let mut hashes_loop = vec![(1u8 << 4) | (reference.len() as u8)]; // reference_type 1, length 1
557        hashes_loop.extend_from_slice(&reference);
558        hashes_loop.extend_from_slice(&hash);
559        let loop_len = hashes_loop.len();
560
561        let mut b = vec![
562            0x00,                                  // section_hash_algorithm_identifier = SHA-256
563            hash.len() as u8,                      // section_hash_length = 4
564            0x01,                                  // signature_algorithm_identifier = ed25519
565            0xF0 | ((loop_len >> 8) as u8 & 0x0F), // reserved | loop_length hi
566            (loop_len & 0xFF) as u8,               // loop_length lo
567        ];
568        b.extend_from_slice(&hashes_loop);
569        // extension_bytes_length + bytes
570        b.push(2);
571        b.extend_from_slice(&[0xDE, 0xAD]);
572        // signature_key_identifier_length + bytes
573        b.push(3);
574        b.extend_from_slice(&[0x11, 0x22, 0x33]);
575        // signature (runs to CRC)
576        b.extend_from_slice(&[0x90, 0x91, 0x92, 0x93, 0x94, 0x95]);
577        b
578    }
579
580    /// Build a certificate-collection body with two certificate slices.
581    fn cert_body() -> Vec<u8> {
582        let c0: &[u8] = &[0x30, 0x82, 0x01, 0x02];
583        let c1: &[u8] = &[0xAB, 0xCD];
584        let mut b = vec![0xF0 | 0x02]; // reserved | certificate_count = 2
585        b.push(0xF0 | ((c0.len() >> 8) as u8 & 0x0F));
586        b.push((c0.len() & 0xFF) as u8);
587        b.extend_from_slice(c0);
588        b.push(0xF0 | ((c1.len() >> 8) as u8 & 0x0F));
589        b.push((c1.len() & 0xFF) as u8);
590        b.extend_from_slice(c1);
591        b
592    }
593
594    #[test]
595    fn parse_authentication_message() {
596        let bytes = build_section(0x0042, 5, &auth_body());
597        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
598        assert_eq!(sec.table_id_extension, 0x0042);
599        assert_eq!(sec.version_number, 5);
600        assert!(sec.current_next_indicator);
601        match sec.body {
602            ProtectionMessageBody::AuthenticationMessage {
603                section_hash_algorithm_identifier,
604                section_hash_length,
605                signature_algorithm_identifier,
606                hashes,
607                extension_bytes,
608                signature_key_identifier,
609                signature,
610            } => {
611                assert_eq!(section_hash_algorithm_identifier, 0x00);
612                assert_eq!(section_hash_length, 4);
613                assert_eq!(signature_algorithm_identifier, 0x01);
614                assert_eq!(hashes.len(), 1);
615                assert_eq!(hashes[0].reference_type, ReferenceType::SameEs);
616                assert_eq!(hashes[0].reference, &[0x01]);
617                assert_eq!(hashes[0].hash, &[0xAA, 0xBB, 0xCC, 0xDD]);
618                assert_eq!(extension_bytes, &[0xDE, 0xAD]);
619                assert_eq!(signature_key_identifier, &[0x11, 0x22, 0x33]);
620                assert_eq!(signature, &[0x90, 0x91, 0x92, 0x93, 0x94, 0x95]);
621            }
622            other => panic!("expected AuthenticationMessage, got {other:?}"),
623        }
624    }
625
626    #[test]
627    fn parse_certificate_collection() {
628        let bytes = build_section(CERTIFICATE_COLLECTION_EXTENSION, 0, &cert_body());
629        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
630        assert_eq!(sec.table_id_extension, 0x0100);
631        match sec.body {
632            ProtectionMessageBody::CertificateCollection { certificates } => {
633                assert_eq!(certificates.len(), 2);
634                assert_eq!(certificates[0], &[0x30, 0x82, 0x01, 0x02]);
635                assert_eq!(certificates[1], &[0xAB, 0xCD]);
636            }
637            other => panic!("expected CertificateCollection, got {other:?}"),
638        }
639    }
640
641    #[test]
642    fn reserved_extension_kept_raw() {
643        let raw = [0x01, 0x02, 0x03, 0x04];
644        let bytes = build_section(0x0200, 0, &raw);
645        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
646        assert!(matches!(sec.body, ProtectionMessageBody::Raw(b) if b == raw));
647    }
648
649    #[test]
650    fn parse_rejects_wrong_tag() {
651        let mut bytes = build_section(0x0000, 0, &auth_body());
652        bytes[0] = 0x4D;
653        assert!(matches!(
654            ProtectionMessageSection::parse(&bytes).unwrap_err(),
655            Error::UnexpectedTableId { table_id: 0x4D, .. }
656        ));
657    }
658
659    #[test]
660    fn rejects_short_buffer() {
661        assert!(matches!(
662            ProtectionMessageSection::parse(&[0x7B, 0xB0]).unwrap_err(),
663            Error::BufferTooShort {
664                what: "ProtectionMessageSection",
665                ..
666            }
667        ));
668    }
669
670    #[test]
671    fn auth_loop_overflow_rejected() {
672        let mut body = vec![0x00, 0x04, 0x01, 0xF0, 0xFF];
673        body.extend_from_slice(&[0x00]);
674        let bytes = build_section(0x0000, 0, &body);
675        assert!(matches!(
676            ProtectionMessageSection::parse(&bytes).unwrap_err(),
677            Error::SectionLengthOverflow { .. }
678        ));
679    }
680
681    #[test]
682    fn cert_length_overflow_rejected() {
683        let body = vec![0xF0 | 0x01, 0x00, 0x10, 0x01];
684        let bytes = build_section(CERTIFICATE_COLLECTION_EXTENSION, 0, &body);
685        assert!(matches!(
686            ProtectionMessageSection::parse(&bytes).unwrap_err(),
687            Error::SectionLengthOverflow { .. }
688        ));
689    }
690
691    #[test]
692    fn round_trip_authentication_message() {
693        let bytes = build_section(0x0042, 7, &auth_body());
694        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
695        let mut buf = vec![0u8; sec.serialized_len()];
696        sec.serialize_into(&mut buf).unwrap();
697        let re = ProtectionMessageSection::parse(&buf).unwrap();
698        assert_eq!(sec, re);
699    }
700
701    #[test]
702    fn round_trip_certificate_collection() {
703        let bytes = build_section(CERTIFICATE_COLLECTION_EXTENSION, 3, &cert_body());
704        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
705        let mut buf = vec![0u8; sec.serialized_len()];
706        sec.serialize_into(&mut buf).unwrap();
707        let re = ProtectionMessageSection::parse(&buf).unwrap();
708        assert_eq!(sec, re);
709    }
710
711    #[test]
712    fn round_trip_raw_reserved() {
713        let bytes = build_section(0xABCD, 1, &[0xDE, 0xAD, 0xBE, 0xEF]);
714        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
715        let mut buf = vec![0u8; sec.serialized_len()];
716        sec.serialize_into(&mut buf).unwrap();
717        let re = ProtectionMessageSection::parse(&buf).unwrap();
718        assert_eq!(sec, re);
719    }
720
721    #[test]
722    fn table_trait_constants() {
723        assert_eq!(TABLE_ID, 0x7B);
724        assert_eq!(PID, 0x0000);
725    }
726
727    #[test]
728    #[cfg(feature = "serde")]
729    fn serde_json_round_trip() {
730        let bytes = build_section(0x0042, 5, &auth_body());
731        let sec = ProtectionMessageSection::parse(&bytes).unwrap();
732        let j = serde_json::to_string(&sec).unwrap();
733        let reparsed = ProtectionMessageSection::parse(&bytes).unwrap();
734        assert_eq!(serde_json::to_string(&reparsed).unwrap(), j);
735        assert!(j.contains("\"signature_algorithm_identifier\":1"));
736    }
737
738    #[test]
739    fn parse_rejects_zero_section_length() {
740        let mut buf = vec![0u8; 64];
741        buf[0] = TABLE_ID;
742        buf[1] = 0xF0;
743        buf[2] = 0x00;
744        for b in &mut buf[3..] {
745            *b = 0xFF;
746        }
747        assert!(matches!(
748            ProtectionMessageSection::parse(&buf).unwrap_err(),
749            Error::SectionLengthOverflow { .. }
750        ));
751    }
752
753    #[test]
754    fn reference_type_full_range_round_trip() {
755        for v in 0u8..=0x0F {
756            let rt = ReferenceType::from_u8(v);
757            assert_eq!(
758                rt.to_u8(),
759                v,
760                "ReferenceType round-trip failed for {v:#04x}"
761            );
762        }
763    }
764}