Skip to main content

dvb_ci/objects/
ca_pmt_reply.rs

1//! CA PMT Reply object (`ca_pmt_reply`) — ETSI EN 50221 §8.4.3.5, Table 26
2//! (PDF p. 32).
3//!
4//! `ca_pmt_reply` (`9F 80 33`, app → host) reports descrambling capability. It
5//! has one optional programme-level `CA_enable` (gated by a `CA_enable_flag`
6//! bit) followed by a per-ES list, each ES with its own `CA_enable_flag` +
7//! optional 7-bit `CA_enable`.
8
9use crate::error::{Error, Result};
10use crate::tag::{self, ApduTag};
11use crate::traits::ApduDef;
12use alloc::vec::Vec;
13use dvb_common::{Parse, Serialize};
14
15/// `CA_enable` 7-bit value (Table, p. 32).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18#[non_exhaustive]
19pub enum CaEnable {
20    /// `01` — descrambling possible.
21    Possible,
22    /// `02` — possible under conditions (purchase dialogue).
23    PossiblePurchaseDialogue,
24    /// `03` — possible under conditions (technical dialogue).
25    PossibleTechnicalDialogue,
26    /// `71` — not possible (no entitlement).
27    NotPossibleNoEntitlement,
28    /// `73` — not possible (technical reasons).
29    NotPossibleTechnical,
30    /// Any other 7-bit value (RFU).
31    Rfu(u8),
32}
33
34impl CaEnable {
35    /// Decode the low 7 bits of a `CA_enable` byte.
36    #[must_use]
37    pub fn from_u8(v: u8) -> Self {
38        match v & 0x7F {
39            0x01 => Self::Possible,
40            0x02 => Self::PossiblePurchaseDialogue,
41            0x03 => Self::PossibleTechnicalDialogue,
42            0x71 => Self::NotPossibleNoEntitlement,
43            0x73 => Self::NotPossibleTechnical,
44            other => Self::Rfu(other),
45        }
46    }
47    /// The 7-bit wire value.
48    #[must_use]
49    pub const fn to_u8(self) -> u8 {
50        match self {
51            Self::Possible => 0x01,
52            Self::PossiblePurchaseDialogue => 0x02,
53            Self::PossibleTechnicalDialogue => 0x03,
54            Self::NotPossibleNoEntitlement => 0x71,
55            Self::NotPossibleTechnical => 0x73,
56            Self::Rfu(v) => v & 0x7F,
57        }
58    }
59    /// Spec token, or `"reserved"`.
60    #[must_use]
61    pub fn name(&self) -> &'static str {
62        match self {
63            Self::Possible => "descrambling_possible",
64            Self::PossiblePurchaseDialogue => "possible_purchase_dialogue",
65            Self::PossibleTechnicalDialogue => "possible_technical_dialogue",
66            Self::NotPossibleNoEntitlement => "not_possible_no_entitlement",
67            Self::NotPossibleTechnical => "not_possible_technical",
68            Self::Rfu(_) => "reserved",
69        }
70    }
71}
72dvb_common::impl_spec_display!(CaEnable, Rfu);
73
74/// One ES entry in a `ca_pmt_reply`.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77pub struct CaPmtReplyStream {
78    /// 13-bit `elementary_PID`.
79    pub elementary_pid: u16,
80    /// ES-level `CA_enable` — `Some` iff the `CA_enable_flag` bit was set.
81    pub ca_enable: Option<CaEnable>,
82}
83
84/// `ca_pmt_reply()` object (Table 26).
85#[derive(Debug, Clone, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize))]
87pub struct CaPmtReply {
88    /// `program_number`.
89    pub program_number: u16,
90    /// 5-bit `version_number`.
91    pub version_number: u8,
92    /// `current_next_indicator`.
93    pub current_next_indicator: bool,
94    /// Programme-level `CA_enable` — `Some` iff the programme `CA_enable_flag` bit
95    /// was set.
96    pub ca_enable: Option<CaEnable>,
97    /// Per-ES entries in wire order.
98    pub streams: Vec<CaPmtReplyStream>,
99}
100
101const REPLY_PREFIX: usize = 4; // program_number(2) + version/cni/flag/enable(2)
102const ES_LEN: usize = 3; // reserved/elem_pid(2) + flag/enable(1)
103
104impl<'a> Parse<'a> for CaPmtReply {
105    type Error = Error;
106    fn parse(bytes: &'a [u8]) -> Result<Self> {
107        let body = super::parse_apdu_header(bytes, tag::CA_PMT_REPLY, "ca_pmt_reply")?;
108        if body.len() < REPLY_PREFIX {
109            return Err(Error::BufferTooShort {
110                need: REPLY_PREFIX,
111                have: body.len(),
112                what: "ca_pmt_reply prefix",
113            });
114        }
115        let program_number = u16::from_be_bytes([body[0], body[1]]);
116        let version_number = (body[2] >> 1) & 0x1F;
117        let current_next_indicator = (body[2] & 0x01) != 0;
118        let ca_enable_flag = (body[3] & 0x80) != 0;
119        let ca_enable = if ca_enable_flag {
120            Some(CaEnable::from_u8(body[3] & 0x7F))
121        } else {
122            None
123        };
124
125        let mut pos = REPLY_PREFIX;
126        let mut streams = Vec::new();
127        while pos < body.len() {
128            if pos + ES_LEN > body.len() {
129                return Err(Error::BufferTooShort {
130                    need: pos + ES_LEN,
131                    have: body.len(),
132                    what: "ca_pmt_reply ES",
133                });
134            }
135            let elementary_pid = (((body[pos] & 0x1F) as u16) << 8) | body[pos + 1] as u16;
136            let es_flag = (body[pos + 2] & 0x80) != 0;
137            let es_enable = if es_flag {
138                Some(CaEnable::from_u8(body[pos + 2] & 0x7F))
139            } else {
140                None
141            };
142            streams.push(CaPmtReplyStream {
143                elementary_pid,
144                ca_enable: es_enable,
145            });
146            pos += ES_LEN;
147        }
148
149        Ok(Self {
150            program_number,
151            version_number,
152            current_next_indicator,
153            ca_enable,
154            streams,
155        })
156    }
157}
158
159impl Serialize for CaPmtReply {
160    type Error = Error;
161    fn serialized_len(&self) -> usize {
162        super::apdu_len(REPLY_PREFIX + self.streams.len() * ES_LEN)
163    }
164    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
165        let body = REPLY_PREFIX + self.streams.len() * ES_LEN;
166        let mut pos = super::write_apdu_header(tag::CA_PMT_REPLY, body, buf)?;
167        buf[pos..pos + 2].copy_from_slice(&self.program_number.to_be_bytes());
168        // reserved(2)='11', version(5), current_next(1).
169        buf[pos + 2] =
170            0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
171        buf[pos + 3] = encode_enable_byte(self.ca_enable);
172        pos += REPLY_PREFIX;
173        for s in &self.streams {
174            // reserved(3)='111', elementary_PID(13).
175            buf[pos] = 0xE0 | ((s.elementary_pid >> 8) as u8 & 0x1F);
176            buf[pos + 1] = s.elementary_pid as u8;
177            buf[pos + 2] = encode_enable_byte(s.ca_enable);
178            pos += ES_LEN;
179        }
180        Ok(pos)
181    }
182}
183
184/// Encode a `CA_enable_flag` + 7-bit `CA_enable`/reserved byte. When absent the
185/// flag is 0 and the 7 reserved bits are set (`0x7F`) per the reserved-bit
186/// convention.
187fn encode_enable_byte(enable: Option<CaEnable>) -> u8 {
188    match enable {
189        Some(e) => 0x80 | (e.to_u8() & 0x7F),
190        None => 0x7F,
191    }
192}
193
194impl<'a> ApduDef<'a> for CaPmtReply {
195    const TAG: ApduTag = tag::CA_PMT_REPLY;
196    const NAME: &'static str = "CA_PMT_REPLY";
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn reply_with_prog_and_es_enable_round_trips() {
205        let r = CaPmtReply {
206            program_number: 0x0001,
207            version_number: 1,
208            current_next_indicator: true,
209            ca_enable: Some(CaEnable::Possible),
210            streams: alloc::vec![
211                CaPmtReplyStream {
212                    elementary_pid: 0x0200,
213                    ca_enable: Some(CaEnable::Possible),
214                },
215                CaPmtReplyStream {
216                    elementary_pid: 0x0201,
217                    ca_enable: Some(CaEnable::NotPossibleNoEntitlement),
218                },
219            ],
220        };
221        let bytes = r.to_bytes();
222        assert_eq!(&bytes[..3], &[0x9F, 0x80, 0x33]);
223        let parsed = CaPmtReply::parse(&bytes).unwrap();
224        assert_eq!(parsed, r);
225        assert_eq!(parsed.streams.len(), 2);
226        assert_eq!(parsed.ca_enable.unwrap().name(), "descrambling_possible");
227    }
228
229    #[test]
230    fn reply_without_enable_flags() {
231        let r = CaPmtReply {
232            program_number: 9,
233            version_number: 0,
234            current_next_indicator: true,
235            ca_enable: None,
236            streams: alloc::vec![CaPmtReplyStream {
237                elementary_pid: 0x00FF,
238                ca_enable: None,
239            }],
240        };
241        let bytes = r.to_bytes();
242        let parsed = CaPmtReply::parse(&bytes).unwrap();
243        assert_eq!(parsed, r);
244        assert!(parsed.ca_enable.is_none());
245        assert!(parsed.streams[0].ca_enable.is_none());
246    }
247
248    #[test]
249    fn mutating_enable_changes_bytes() {
250        let r = CaPmtReply {
251            program_number: 1,
252            version_number: 1,
253            current_next_indicator: true,
254            ca_enable: Some(CaEnable::Possible),
255            streams: Vec::new(),
256        };
257        let a = r.to_bytes();
258        let mut other = r.clone();
259        other.ca_enable = Some(CaEnable::NotPossibleNoEntitlement);
260        assert_ne!(a, other.to_bytes());
261    }
262}