Skip to main content

dvb_ci/objects/
mod.rs

1//! Application-layer APDU objects (resource APDUs) — ETSI EN 50221 §8.4-§8.6.
2//!
3//! Each object implements [`broadcast_common::Parse`] / [`broadcast_common::Serialize`] over
4//! the **whole** APDU including its `apdu_tag` (3 bytes) + `length_field`
5//! header, so dispatch routes on the header and round-trips are byte-symmetric.
6//! The shared header helpers here keep every object's length field computed from
7//! its content.
8
9use crate::error::{Error, Result};
10use crate::length;
11use crate::tag::ApduTag;
12
13pub mod application_info;
14pub mod ca_info;
15pub mod ca_pmt;
16pub mod ca_pmt_reply;
17pub mod date_time;
18pub mod host_control;
19pub mod low_speed_comms;
20pub mod mmi_close;
21pub mod mmi_display;
22pub mod mmi_high;
23pub mod resource_manager;
24
25/// Reject bytes after a fixed-layout APDU body: the `length_field` declared
26/// more than the object's fixed shape holds, so the extra bytes would be
27/// silently dropped and the parse -> serialize round trip would not be
28/// byte-identical (audit r10-O-1).
29pub(crate) fn reject_trailing_body(body: &[u8], expected: usize, what: &'static str) -> Result<()> {
30    if body.len() > expected {
31        return Err(Error::InvalidObject {
32            what,
33            reason: "trailing bytes after the fixed body",
34        });
35    }
36    Ok(())
37}
38
39/// Parse an APDU header: verify the 3-byte `apdu_tag` matches `expected`, decode
40/// the `length_field`, and return the body slice (exactly `length_value` bytes).
41pub(crate) fn parse_apdu_header<'a>(
42    bytes: &'a [u8],
43    expected: ApduTag,
44    what: &'static str,
45) -> Result<&'a [u8]> {
46    if bytes.len() < 3 {
47        return Err(Error::BufferTooShort {
48            need: 3,
49            have: bytes.len(),
50            what,
51        });
52    }
53    let got = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
54    if got != expected {
55        return Err(Error::UnexpectedApduTag {
56            got: got.as_u24(),
57            expected: expected.as_u24(),
58            what,
59        });
60    }
61    let (len_value, len_hdr) = length::decode(&bytes[3..])?;
62    let body_start = 3 + len_hdr;
63    let body_end = body_start + len_value;
64    if bytes.len() < body_end {
65        return Err(Error::LengthMismatch {
66            what,
67            declared: len_value,
68            actual: bytes.len().saturating_sub(body_start),
69        });
70    }
71    Ok(&bytes[body_start..body_end])
72}
73
74/// Parse a header-only (empty-body) APDU, erroring if the body is non-empty.
75pub(crate) fn parse_empty_apdu(bytes: &[u8], expected: ApduTag, what: &'static str) -> Result<()> {
76    let body = parse_apdu_header(bytes, expected, what)?;
77    if !body.is_empty() {
78        return Err(Error::InvalidObject {
79            what,
80            reason: "expected empty body",
81        });
82    }
83    Ok(())
84}
85
86/// Serialized length of an APDU with a `body_len`-byte body.
87pub(crate) fn apdu_len(body_len: usize) -> usize {
88    3 + length::encoded_len(body_len) + body_len
89}
90
91/// Serialized length of a header-only (empty-body) APDU.
92pub(crate) fn empty_apdu_len() -> usize {
93    apdu_len(0)
94}
95
96/// Write an APDU header (tag + `length_field`) into `buf`, returning the number
97/// of header bytes written (the body starts at that offset). Checks the buffer
98/// can hold the whole APDU (`apdu_len(body_len)`) up front.
99pub(crate) fn write_apdu_header(tag: ApduTag, body_len: usize, buf: &mut [u8]) -> Result<usize> {
100    let total = apdu_len(body_len);
101    if buf.len() < total {
102        return Err(Error::OutputBufferTooSmall {
103            need: total,
104            have: buf.len(),
105        });
106    }
107    buf[..3].copy_from_slice(&tag.to_bytes());
108    let n = length::encode_into(body_len, &mut buf[3..])?;
109    Ok(3 + n)
110}
111
112/// Serialize a header-only (empty-body) APDU into `buf`.
113pub(crate) fn serialize_empty_apdu(tag: ApduTag, buf: &mut [u8]) -> Result<usize> {
114    write_apdu_header(tag, 0, buf)
115}
116
117/// Range-check `value` against a `bits`-wide wire field before it is narrowed
118/// and written, mapping the shared [`broadcast_common::len::FieldOverflow`]
119/// (which compares before narrowing) into this crate's [`Error::InvalidObject`]
120/// so a length/count/PID that does not fit is rejected instead of silently
121/// wrapped. `what` names the object and field (e.g. `"ca_pmt ES elementary_PID"`)
122/// for both the `FieldOverflow` and the resulting error.
123pub(crate) fn fit_bits(value: u64, bits: u32, what: &'static str) -> Result<u64> {
124    broadcast_common::len::fit_bits(value, bits, what).map_err(|_| Error::InvalidObject {
125        what,
126        reason: overflow_reason(bits),
127    })
128}
129
130/// Static "why" text for [`fit_bits`], keyed by field width — every width this
131/// crate's wire fields actually use.
132const fn overflow_reason(bits: u32) -> &'static str {
133    match bits {
134        4 => "exceeds 4-bit range (0x0F)",
135        5 => "exceeds 5-bit range (0x1F)",
136        7 => "exceeds 7-bit range (0x7F)",
137        8 => "exceeds 8-bit range (0xFF)",
138        12 => "exceeds 12-bit range (0x0FFF)",
139        13 => "exceeds 13-bit PID range (0x1FFF)",
140        14 => "exceeds 14-bit range (0x3FFF)",
141        16 => "exceeds 16-bit range (0xFFFF)",
142        24 => "exceeds 24-bit range (0xFFFFFF)",
143        _ => "exceeds field width",
144    }
145}
146
147/// `Debug` stand-in for secret bytes: prints only the length, never the
148/// content, so an APDU logged with `{:?}` cannot leak a PIN, licence or DRM
149/// blob (audit #1142). The bytes stay available on the field itself.
150pub(crate) struct Redacted(pub(crate) usize);
151
152impl core::fmt::Debug for Redacted {
153    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
154        write!(f, "<{} bytes redacted>", self.0)
155    }
156}
157
158/// serde helper: serialize a borrowed `&[u8]` field as a byte sequence.
159#[cfg(feature = "serde")]
160pub(crate) mod bytes_serde {
161    pub fn serialize<S: serde::Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
162        s.serialize_bytes(bytes)
163    }
164}