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 dvb_ci::resource::ResourceId;
14use dvb_ci::spdu::{
15    tags, CloseSessionRequest, CloseSessionResponse, CreateSession, CreateSessionResponse,
16    OpenSessionRequest, OpenSessionResponse, SessionNumber, SessionStatus,
17};
18use dvb_common::{Parse, Serialize};
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 `create_session` SPDU to send. The session is recorded once
93    /// the module's `create_session_response(ok)` arrives.
94    pub fn create_session(&mut self, resource: ResourceId) -> Vec<u8> {
95        let session_nb = self.alloc();
96        ser(&CreateSession {
97            resource,
98            session_nb,
99        })
100    }
101
102    /// Wrap an APDU for sending on `session_nb` (`session_number` + body).
103    #[must_use]
104    pub fn send_apdu(&self, session_nb: u16, apdu: &[u8]) -> Vec<u8> {
105        let mut v = ser(&SessionNumber { session_nb });
106        v.extend_from_slice(apdu);
107        v
108    }
109
110    /// Begin closing `session_nb`: returns the `close_session_request` SPDU.
111    pub fn close(&mut self, session_nb: u16) -> Vec<u8> {
112        self.sessions.remove(&session_nb);
113        ser(&CloseSessionRequest { session_nb })
114    }
115
116    /// Handle one inbound SPDU. `provides` answers "does the host provide this
117    /// resource?" for an incoming `open_session_request`.
118    pub fn on_spdu(&mut self, spdu: &[u8], provides: impl Fn(ResourceId) -> bool) -> SessionOut {
119        let mut out = SessionOut::default();
120        match spdu.first().copied() {
121            // Module wants a host-provided resource.
122            Some(tags::OPEN_SESSION_REQUEST) => {
123                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            }
142            // Module's reply to our create_session.
143            Some(tags::CREATE_SESSION_RESPONSE) => {
144                if let Ok(resp) = CreateSessionResponse::parse(spdu) {
145                    if resp.status == SessionStatus::Ok {
146                        self.sessions.insert(resp.session_nb, resp.resource);
147                        out.opened.push((resp.session_nb, resp.resource));
148                    }
149                }
150            }
151            // Peer closes a session.
152            Some(tags::CLOSE_SESSION_REQUEST) => {
153                if let Ok(req) = CloseSessionRequest::parse(spdu) {
154                    self.sessions.remove(&req.session_nb);
155                    out.spdus.push(ser(&CloseSessionResponse {
156                        status: SessionStatus::Ok,
157                        session_nb: req.session_nb,
158                    }));
159                    out.closed.push(req.session_nb);
160                }
161            }
162            // Ack of a close we initiated.
163            Some(tags::CLOSE_SESSION_RESPONSE) => {
164                if let Ok(resp) = CloseSessionResponse::parse(spdu) {
165                    self.sessions.remove(&resp.session_nb);
166                    out.closed.push(resp.session_nb);
167                }
168            }
169            // Data: session_number(nb) + APDU body.
170            Some(tags::SESSION_NUMBER) => {
171                if let Ok(sn) = SessionNumber::parse(spdu) {
172                    if spdu.len() > SessionNumber::HEADER_LEN {
173                        out.apdus
174                            .push((sn.session_nb, spdu[SessionNumber::HEADER_LEN..].to_vec()));
175                    }
176                }
177            }
178            _ => {}
179        }
180        out
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use dvb_ci::resource::{APPLICATION_INFORMATION, RESOURCE_MANAGER};
188
189    fn provides_rm(r: ResourceId) -> bool {
190        r == RESOURCE_MANAGER
191    }
192
193    #[test]
194    fn open_request_for_provided_resource_grants_and_tracks() {
195        let mut s = SessionLayer::new();
196        let req = ser(&OpenSessionRequest {
197            resource: RESOURCE_MANAGER,
198        });
199        let out = s.on_spdu(&req, provides_rm);
200        assert_eq!(out.opened.len(), 1);
201        let (nb, res) = out.opened[0];
202        assert_eq!(res, RESOURCE_MANAGER);
203        assert_eq!(s.resource_of(nb), Some(RESOURCE_MANAGER));
204        // reply is an open_session_response with status ok
205        let resp = OpenSessionResponse::parse(&out.spdus[0]).unwrap();
206        assert_eq!(resp.status, SessionStatus::Ok);
207        assert_eq!(resp.session_nb, nb);
208    }
209
210    #[test]
211    fn open_request_for_absent_resource_denied() {
212        let mut s = SessionLayer::new();
213        let req = ser(&OpenSessionRequest {
214            resource: APPLICATION_INFORMATION,
215        });
216        let out = s.on_spdu(&req, provides_rm);
217        assert!(out.opened.is_empty());
218        let resp = OpenSessionResponse::parse(&out.spdus[0]).unwrap();
219        assert_eq!(resp.status, SessionStatus::ResourceNonExistent);
220        assert!(s.is_empty());
221    }
222
223    #[test]
224    fn create_session_tracked_on_ok_response() {
225        let mut s = SessionLayer::new();
226        let _spdu = s.create_session(APPLICATION_INFORMATION);
227        // module replies ok for session 1
228        let resp = ser(&CreateSessionResponse {
229            status: SessionStatus::Ok,
230            resource: APPLICATION_INFORMATION,
231            session_nb: 1,
232        });
233        let out = s.on_spdu(&resp, |_| false);
234        assert_eq!(out.opened, vec![(1, APPLICATION_INFORMATION)]);
235        assert_eq!(s.resource_of(1), Some(APPLICATION_INFORMATION));
236    }
237
238    #[test]
239    fn session_number_routes_apdu_up() {
240        let mut s = SessionLayer::new();
241        let apdu = [0x9F, 0x80, 0x21, 0x00];
242        let mut spdu = ser(&SessionNumber { session_nb: 7 });
243        spdu.extend_from_slice(&apdu);
244        let out = s.on_spdu(&spdu, |_| false);
245        assert_eq!(out.apdus, vec![(7, apdu.to_vec())]);
246    }
247
248    #[test]
249    fn close_request_acks_and_removes() {
250        let mut s = SessionLayer::new();
251        // open one first
252        let req = ser(&OpenSessionRequest {
253            resource: RESOURCE_MANAGER,
254        });
255        let nb = s.on_spdu(&req, provides_rm).opened[0].0;
256        // peer closes it
257        let close = ser(&CloseSessionRequest { session_nb: nb });
258        let out = s.on_spdu(&close, |_| false);
259        assert_eq!(out.closed, vec![nb]);
260        assert!(s.is_empty());
261        // reply is a close_session_response
262        assert_eq!(out.spdus[0][0], tags::CLOSE_SESSION_RESPONSE);
263    }
264
265    #[test]
266    fn send_apdu_prefixes_session_number() {
267        let s = SessionLayer::new();
268        let wire = s.send_apdu(3, &[0xAA, 0xBB]);
269        let sn = SessionNumber::parse(&wire).unwrap();
270        assert_eq!(sn.session_nb, 3);
271        assert_eq!(&wire[SessionNumber::HEADER_LEN..], &[0xAA, 0xBB]);
272    }
273}