Skip to main content

dvb_ci_runtime/
session.rs

1//! SPDU session layer — a sans-IO mechanism over the transport layer
2//! (ETSI EN 50221 §7.2).
3//!
4//! Multiplexes logical sessions (one per resource in use) over the transport
5//! connection: allocates/tracks `session_nb`s, answers `open_session_request`
6//! for resources the host advertises, opens `create_session` on demand, and
7//! routes `session_number`+APDU to/from the resource bound to a session. It is
8//! mechanism only — *which* resources the host provides is the caller's policy,
9//! supplied as the `provides` predicate to [`SessionLayer::on_spdu`].
10
11use std::collections::BTreeMap;
12
13use broadcast_common::{Parse, Serialize};
14use dvb_ci::resource::ResourceId;
15use dvb_ci::spdu::{
16    CloseSessionRequest, CloseSessionResponse, CreateSessionResponse, OpenSessionRequest,
17    OpenSessionResponse, SessionNumber, SessionStatus, tags,
18};
19
20fn ser<S: Serialize>(s: &S) -> Vec<u8> {
21    let mut b = vec![0u8; s.serialized_len()];
22    // The buffer is sized to `serialized_len()`, so serialization cannot fail;
23    // matched (not `expect`ed) to avoid a `Debug` bound on `S::Error`.
24    match s.serialize_into(&mut b) {
25        Ok(n) => b.truncate(n),
26        Err(_) => b.clear(),
27    }
28    b
29}
30
31/// What the session layer wants done after handling one SPDU.
32#[derive(Debug, Default, Clone, PartialEq, Eq)]
33pub struct SessionOut {
34    /// SPDUs to hand down to the transport layer (each becomes a `T_Data_Last`).
35    pub spdus: Vec<Vec<u8>>,
36    /// `(session_nb, apdu_bytes)` to pass up to the resource layer.
37    pub apdus: Vec<(u16, Vec<u8>)>,
38    /// Sessions newly opened (`session_nb`, bound resource).
39    pub opened: Vec<(u16, ResourceId)>,
40    /// `session_nb`s that closed.
41    pub closed: Vec<u16>,
42}
43
44/// The session table + `session_nb` allocator.
45#[derive(Debug, Default)]
46pub struct SessionLayer {
47    sessions: BTreeMap<u16, ResourceId>,
48    next: u16,
49}
50
51impl SessionLayer {
52    /// New, empty session layer.
53    #[must_use]
54    pub fn new() -> Self {
55        Self {
56            sessions: BTreeMap::new(),
57            next: 1, // session_nb 0 is reserved
58        }
59    }
60
61    /// Resource bound to `session_nb`, if open.
62    #[must_use]
63    pub fn resource_of(&self, session_nb: u16) -> Option<ResourceId> {
64        self.sessions.get(&session_nb).copied()
65    }
66
67    /// All open `(session_nb, resource)` pairs, ascending by `session_nb`.
68    #[must_use]
69    pub fn sessions(&self) -> Vec<(u16, ResourceId)> {
70        self.sessions.iter().map(|(&n, &r)| (n, r)).collect()
71    }
72
73    /// Number of open sessions.
74    #[must_use]
75    pub fn len(&self) -> usize {
76        self.sessions.len()
77    }
78
79    /// Whether there are no open sessions.
80    #[must_use]
81    pub fn is_empty(&self) -> bool {
82        self.sessions.is_empty()
83    }
84
85    fn alloc(&mut self) -> u16 {
86        let nb = self.next;
87        self.next = self.next.checked_add(1).filter(|&n| n != 0).unwrap_or(1);
88        nb
89    }
90
91    /// Open a session to a **module-provided** resource (host-initiated):
92    /// returns the `open_session_request` SPDU to send. Sessions are opened the
93    /// same way in both directions (§8.4.1) — the host sends
94    /// `open_session_request`, and the module (the resource provider) assigns the
95    /// `session_nb` in its `open_session_response`. (`create_session`/0x93 is a
96    /// resource-manager-internal primitive; a real CAM rejects it with
97    /// `status=0xF0` — verified live against an AlphaCrypt.) The session is
98    /// recorded once the module's `open_session_response(ok)` arrives.
99    pub fn create_session(&mut self, resource: ResourceId) -> Vec<u8> {
100        ser(&OpenSessionRequest { resource })
101    }
102
103    /// Wrap an APDU for sending on `session_nb` (`session_number` + body).
104    #[must_use]
105    pub fn send_apdu(&self, session_nb: u16, apdu: &[u8]) -> Vec<u8> {
106        let mut v = ser(&SessionNumber { session_nb });
107        v.extend_from_slice(apdu);
108        v
109    }
110
111    /// Begin closing `session_nb`: returns the `close_session_request` SPDU.
112    pub fn close(&mut self, session_nb: u16) -> Vec<u8> {
113        self.sessions.remove(&session_nb);
114        ser(&CloseSessionRequest { session_nb })
115    }
116
117    /// Handle one inbound SPDU. `provides` answers "does the host provide this
118    /// resource?" for an incoming `open_session_request`.
119    pub fn on_spdu(&mut self, spdu: &[u8], provides: impl Fn(ResourceId) -> bool) -> SessionOut {
120        let mut out = SessionOut::default();
121        match spdu.first().copied() {
122            // Module wants a host-provided resource.
123            Some(tags::OPEN_SESSION_REQUEST) if let Ok(req) = OpenSessionRequest::parse(spdu) => {
124                if provides(req.resource) {
125                    let session_nb = self.alloc();
126                    self.sessions.insert(session_nb, req.resource);
127                    out.spdus.push(ser(&OpenSessionResponse {
128                        status: SessionStatus::Ok,
129                        resource: req.resource,
130                        session_nb,
131                    }));
132                    out.opened.push((session_nb, req.resource));
133                } else {
134                    out.spdus.push(ser(&OpenSessionResponse {
135                        status: SessionStatus::ResourceNonExistent,
136                        resource: req.resource,
137                        session_nb: 0,
138                    }));
139                }
140            }
141            // Module's reply to our open_session_request (host opened a
142            // module-provided resource); the module assigns the session_nb.
143            Some(tags::OPEN_SESSION_RESPONSE)
144                if let Ok(resp) = OpenSessionResponse::parse(spdu)
145                    && resp.status == SessionStatus::Ok =>
146            {
147                self.sessions.insert(resp.session_nb, resp.resource);
148                out.opened.push((resp.session_nb, resp.resource));
149            }
150            // (Legacy) module's reply to a create_session, if any module uses it.
151            Some(tags::CREATE_SESSION_RESPONSE)
152                if let Ok(resp) = CreateSessionResponse::parse(spdu)
153                    && resp.status == SessionStatus::Ok =>
154            {
155                self.sessions.insert(resp.session_nb, resp.resource);
156                out.opened.push((resp.session_nb, resp.resource));
157            }
158            // Peer closes a session.
159            Some(tags::CLOSE_SESSION_REQUEST) if let Ok(req) = CloseSessionRequest::parse(spdu) => {
160                self.sessions.remove(&req.session_nb);
161                out.spdus.push(ser(&CloseSessionResponse {
162                    status: SessionStatus::Ok,
163                    session_nb: req.session_nb,
164                }));
165                out.closed.push(req.session_nb);
166            }
167            // Ack of a close we initiated.
168            Some(tags::CLOSE_SESSION_RESPONSE)
169                if let Ok(resp) = CloseSessionResponse::parse(spdu) =>
170            {
171                self.sessions.remove(&resp.session_nb);
172                out.closed.push(resp.session_nb);
173            }
174            // Data: session_number(nb) + APDU body.
175            Some(tags::SESSION_NUMBER)
176                if let Ok(sn) = SessionNumber::parse(spdu)
177                    && spdu.len() > SessionNumber::HEADER_LEN =>
178            {
179                out.apdus
180                    .push((sn.session_nb, spdu[SessionNumber::HEADER_LEN..].to_vec()));
181            }
182            _ => {}
183        }
184        out
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use dvb_ci::resource::{APPLICATION_INFORMATION, RESOURCE_MANAGER};
192
193    fn provides_rm(r: ResourceId) -> bool {
194        r == RESOURCE_MANAGER
195    }
196
197    #[test]
198    fn open_request_for_provided_resource_grants_and_tracks() {
199        let mut s = SessionLayer::new();
200        let req = ser(&OpenSessionRequest {
201            resource: RESOURCE_MANAGER,
202        });
203        let out = s.on_spdu(&req, provides_rm);
204        assert_eq!(out.opened.len(), 1);
205        let (nb, res) = out.opened[0];
206        assert_eq!(res, RESOURCE_MANAGER);
207        assert_eq!(s.resource_of(nb), Some(RESOURCE_MANAGER));
208        // reply is an open_session_response with status ok
209        let resp = OpenSessionResponse::parse(&out.spdus[0]).unwrap();
210        assert_eq!(resp.status, SessionStatus::Ok);
211        assert_eq!(resp.session_nb, nb);
212    }
213
214    #[test]
215    fn open_request_for_absent_resource_denied() {
216        let mut s = SessionLayer::new();
217        let req = ser(&OpenSessionRequest {
218            resource: APPLICATION_INFORMATION,
219        });
220        let out = s.on_spdu(&req, provides_rm);
221        assert!(out.opened.is_empty());
222        let resp = OpenSessionResponse::parse(&out.spdus[0]).unwrap();
223        assert_eq!(resp.status, SessionStatus::ResourceNonExistent);
224        assert!(s.is_empty());
225    }
226
227    #[test]
228    fn create_session_tracked_on_ok_response() {
229        let mut s = SessionLayer::new();
230        let _spdu = s.create_session(APPLICATION_INFORMATION);
231        // module replies ok for session 1
232        let resp = ser(&CreateSessionResponse {
233            status: SessionStatus::Ok,
234            resource: APPLICATION_INFORMATION,
235            session_nb: 1,
236        });
237        let out = s.on_spdu(&resp, |_| false);
238        assert_eq!(out.opened, vec![(1, APPLICATION_INFORMATION)]);
239        assert_eq!(s.resource_of(1), Some(APPLICATION_INFORMATION));
240    }
241
242    #[test]
243    fn session_number_routes_apdu_up() {
244        let mut s = SessionLayer::new();
245        let apdu = [0x9F, 0x80, 0x21, 0x00];
246        let mut spdu = ser(&SessionNumber { session_nb: 7 });
247        spdu.extend_from_slice(&apdu);
248        let out = s.on_spdu(&spdu, |_| false);
249        assert_eq!(out.apdus, vec![(7, apdu.to_vec())]);
250    }
251
252    #[test]
253    fn close_request_acks_and_removes() {
254        let mut s = SessionLayer::new();
255        // open one first
256        let req = ser(&OpenSessionRequest {
257            resource: RESOURCE_MANAGER,
258        });
259        let nb = s.on_spdu(&req, provides_rm).opened[0].0;
260        // peer closes it
261        let close = ser(&CloseSessionRequest { session_nb: nb });
262        let out = s.on_spdu(&close, |_| false);
263        assert_eq!(out.closed, vec![nb]);
264        assert!(s.is_empty());
265        // reply is a close_session_response
266        assert_eq!(out.spdus[0][0], tags::CLOSE_SESSION_RESPONSE);
267    }
268
269    #[test]
270    fn send_apdu_prefixes_session_number() {
271        let s = SessionLayer::new();
272        let wire = s.send_apdu(3, &[0xAA, 0xBB]);
273        let sn = SessionNumber::parse(&wire).unwrap();
274        assert_eq!(sn.session_nb, 3);
275        assert_eq!(&wire[SessionNumber::HEADER_LEN..], &[0xAA, 0xBB]);
276    }
277}