Skip to main content

dvb_ci/
spdu.rs

1//! Session Protocol Data Unit (SPDU) framing — ETSI EN 50221 §7.2.4-7.2.7,
2//! Tables 4-14 (PDF pp. 19-23).
3//!
4//! An SPDU is a one-byte `spdu_tag`, a [`length_field`](crate::length) coding
5//! the length of the *session object value* (NOT any following APDUs), and that
6//! value. Only `session_number` (tag `90`) is followed by an SPDU body of
7//! APDUs; this module models the session-management objects and the
8//! `session_number` header (the trailing APDU body is the caller's, parsed with
9//! [`AnyApdu`](crate::AnyApdu)).
10//!
11//! Each object's `Parse`/`Serialize` covers the whole SPDU header
12//! (`spdu_tag` + `length_field` + value); lengths are computed from content.
13
14use crate::error::{Error, Result};
15use crate::length;
16use crate::resource::ResourceId;
17use dvb_common::{Parse, Serialize};
18
19/// `spdu_tag` values — Table 14 (p. 23). One byte each.
20pub mod tags {
21    /// `Tsession_number` = `90`.
22    pub const SESSION_NUMBER: u8 = 0x90;
23    /// `Topen_session_request` = `91`.
24    pub const OPEN_SESSION_REQUEST: u8 = 0x91;
25    /// `Topen_session_response` = `92`.
26    pub const OPEN_SESSION_RESPONSE: u8 = 0x92;
27    /// `Tcreate_session` = `93`.
28    pub const CREATE_SESSION: u8 = 0x93;
29    /// `Tcreate_session_response` = `94`.
30    pub const CREATE_SESSION_RESPONSE: u8 = 0x94;
31    /// `Tclose_session_request` = `95`.
32    pub const CLOSE_SESSION_REQUEST: u8 = 0x95;
33    /// `Tclose_session_response` = `96`.
34    pub const CLOSE_SESSION_RESPONSE: u8 = 0x96;
35}
36
37/// `session_status` values — Tables 7 (open/create) and 12 (close), pp. 20-22.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40#[non_exhaustive]
41pub enum SessionStatus {
42    /// `00` — session opened / closed as required.
43    Ok,
44    /// `F0` — resource non-existent (open/create), or session_nb not allocated
45    /// (close).
46    ResourceNonExistent,
47    /// `F1` — resource exists but unavailable.
48    ResourceUnavailable,
49    /// `F2` — resource exists but version lower than requested.
50    ResourceVersionTooLow,
51    /// `F3` — resource busy.
52    ResourceBusy,
53    /// Any other value (reserved).
54    Reserved(u8),
55}
56
57impl SessionStatus {
58    /// Decode a `session_status` byte.
59    #[must_use]
60    pub fn from_u8(v: u8) -> Self {
61        match v {
62            0x00 => Self::Ok,
63            0xF0 => Self::ResourceNonExistent,
64            0xF1 => Self::ResourceUnavailable,
65            0xF2 => Self::ResourceVersionTooLow,
66            0xF3 => Self::ResourceBusy,
67            other => Self::Reserved(other),
68        }
69    }
70    /// Wire byte.
71    #[must_use]
72    pub const fn to_u8(self) -> u8 {
73        match self {
74            Self::Ok => 0x00,
75            Self::ResourceNonExistent => 0xF0,
76            Self::ResourceUnavailable => 0xF1,
77            Self::ResourceVersionTooLow => 0xF2,
78            Self::ResourceBusy => 0xF3,
79            Self::Reserved(v) => v,
80        }
81    }
82    /// Spec token, or `"reserved"`.
83    #[must_use]
84    pub fn name(&self) -> &'static str {
85        match self {
86            Self::Ok => "ok",
87            Self::ResourceNonExistent => "resource_non_existent",
88            Self::ResourceUnavailable => "resource_unavailable",
89            Self::ResourceVersionTooLow => "resource_version_too_low",
90            Self::ResourceBusy => "resource_busy",
91            Self::Reserved(_) => "reserved",
92        }
93    }
94}
95dvb_common::impl_spec_display!(SessionStatus, Reserved);
96
97// --- shared SPDU header helpers ---
98
99fn parse_spdu_header<'a>(bytes: &'a [u8], expected: u8, what: &'static str) -> Result<&'a [u8]> {
100    let first = *bytes.first().ok_or(Error::BufferTooShort {
101        need: 1,
102        have: 0,
103        what,
104    })?;
105    if first != expected {
106        return Err(Error::UnexpectedSpduTag {
107            got: first,
108            expected,
109            what,
110        });
111    }
112    let (len, hdr) = length::decode(&bytes[1..])?;
113    let start = 1 + hdr;
114    let end = start + len;
115    if bytes.len() < end {
116        return Err(Error::LengthMismatch {
117            what,
118            declared: len,
119            actual: bytes.len().saturating_sub(start),
120        });
121    }
122    Ok(&bytes[start..end])
123}
124
125fn spdu_len(value_len: usize) -> usize {
126    1 + length::encoded_len(value_len) + value_len
127}
128
129fn write_spdu_header(tag: u8, value_len: usize, buf: &mut [u8]) -> Result<usize> {
130    let total = spdu_len(value_len);
131    if buf.len() < total {
132        return Err(Error::OutputBufferTooSmall {
133            need: total,
134            have: buf.len(),
135        });
136    }
137    buf[0] = tag;
138    let n = length::encode_into(value_len, &mut buf[1..])?;
139    Ok(1 + n)
140}
141
142/// `open_session_request()` (Table 5) — module → host, `length=4`.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144#[cfg_attr(feature = "serde", derive(serde::Serialize))]
145pub struct OpenSessionRequest {
146    /// The requested `resource_identifier()`.
147    pub resource: ResourceId,
148}
149
150impl<'a> Parse<'a> for OpenSessionRequest {
151    type Error = Error;
152    fn parse(bytes: &'a [u8]) -> Result<Self> {
153        let v = parse_spdu_header(bytes, tags::OPEN_SESSION_REQUEST, "open_session_request")?;
154        Ok(Self {
155            resource: ResourceId::parse(v)?,
156        })
157    }
158}
159impl Serialize for OpenSessionRequest {
160    type Error = Error;
161    fn serialized_len(&self) -> usize {
162        spdu_len(ResourceId::LEN)
163    }
164    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
165        let pos = write_spdu_header(tags::OPEN_SESSION_REQUEST, ResourceId::LEN, buf)?;
166        let n = self.resource.serialize_into(&mut buf[pos..])?;
167        Ok(pos + n)
168    }
169}
170
171/// `open_session_response()` (Table 6) — host → module, `length=7`.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize))]
174pub struct OpenSessionResponse {
175    /// `session_status`.
176    pub status: SessionStatus,
177    /// The `resource_identifier()`.
178    pub resource: ResourceId,
179    /// `session_nb` (0 reserved; meaningless when status != ok).
180    pub session_nb: u16,
181}
182
183impl<'a> Parse<'a> for OpenSessionResponse {
184    type Error = Error;
185    fn parse(bytes: &'a [u8]) -> Result<Self> {
186        let v = parse_spdu_header(bytes, tags::OPEN_SESSION_RESPONSE, "open_session_response")?;
187        if v.len() != 7 {
188            return Err(Error::InvalidObject {
189                what: "open_session_response",
190                reason: "value must be 7 bytes",
191            });
192        }
193        Ok(Self {
194            status: SessionStatus::from_u8(v[0]),
195            resource: ResourceId::parse(&v[1..5])?,
196            session_nb: u16::from_be_bytes([v[5], v[6]]),
197        })
198    }
199}
200impl Serialize for OpenSessionResponse {
201    type Error = Error;
202    fn serialized_len(&self) -> usize {
203        spdu_len(7)
204    }
205    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
206        let mut pos = write_spdu_header(tags::OPEN_SESSION_RESPONSE, 7, buf)?;
207        buf[pos] = self.status.to_u8();
208        pos += 1;
209        pos += self.resource.serialize_into(&mut buf[pos..])?;
210        buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
211        Ok(pos + 2)
212    }
213}
214
215/// `create_session()` (Table 8) — host → module, `length=6`.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize))]
218pub struct CreateSession {
219    /// The `resource_identifier()`.
220    pub resource: ResourceId,
221    /// `session_nb` allocated for the session.
222    pub session_nb: u16,
223}
224
225impl<'a> Parse<'a> for CreateSession {
226    type Error = Error;
227    fn parse(bytes: &'a [u8]) -> Result<Self> {
228        let v = parse_spdu_header(bytes, tags::CREATE_SESSION, "create_session")?;
229        if v.len() != 6 {
230            return Err(Error::InvalidObject {
231                what: "create_session",
232                reason: "value must be 6 bytes",
233            });
234        }
235        Ok(Self {
236            resource: ResourceId::parse(&v[0..4])?,
237            session_nb: u16::from_be_bytes([v[4], v[5]]),
238        })
239    }
240}
241impl Serialize for CreateSession {
242    type Error = Error;
243    fn serialized_len(&self) -> usize {
244        spdu_len(6)
245    }
246    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
247        let mut pos = write_spdu_header(tags::CREATE_SESSION, 6, buf)?;
248        pos += self.resource.serialize_into(&mut buf[pos..])?;
249        buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
250        Ok(pos + 2)
251    }
252}
253
254/// `create_session_response()` (Table 9) — module → host, `length=7`.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize))]
257pub struct CreateSessionResponse {
258    /// `session_status`.
259    pub status: SessionStatus,
260    /// The `resource_identifier()`.
261    pub resource: ResourceId,
262    /// `session_nb` (equals the create_session it replies to).
263    pub session_nb: u16,
264}
265
266impl<'a> Parse<'a> for CreateSessionResponse {
267    type Error = Error;
268    fn parse(bytes: &'a [u8]) -> Result<Self> {
269        let v = parse_spdu_header(
270            bytes,
271            tags::CREATE_SESSION_RESPONSE,
272            "create_session_response",
273        )?;
274        if v.len() != 7 {
275            return Err(Error::InvalidObject {
276                what: "create_session_response",
277                reason: "value must be 7 bytes",
278            });
279        }
280        Ok(Self {
281            status: SessionStatus::from_u8(v[0]),
282            resource: ResourceId::parse(&v[1..5])?,
283            session_nb: u16::from_be_bytes([v[5], v[6]]),
284        })
285    }
286}
287impl Serialize for CreateSessionResponse {
288    type Error = Error;
289    fn serialized_len(&self) -> usize {
290        spdu_len(7)
291    }
292    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
293        let mut pos = write_spdu_header(tags::CREATE_SESSION_RESPONSE, 7, buf)?;
294        buf[pos] = self.status.to_u8();
295        pos += 1;
296        pos += self.resource.serialize_into(&mut buf[pos..])?;
297        buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
298        Ok(pos + 2)
299    }
300}
301
302/// `close_session_request()` (Table 10) — `length=2`.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304#[cfg_attr(feature = "serde", derive(serde::Serialize))]
305pub struct CloseSessionRequest {
306    /// `session_nb` to close.
307    pub session_nb: u16,
308}
309
310impl<'a> Parse<'a> for CloseSessionRequest {
311    type Error = Error;
312    fn parse(bytes: &'a [u8]) -> Result<Self> {
313        let v = parse_spdu_header(bytes, tags::CLOSE_SESSION_REQUEST, "close_session_request")?;
314        if v.len() != 2 {
315            return Err(Error::InvalidObject {
316                what: "close_session_request",
317                reason: "value must be 2 bytes",
318            });
319        }
320        Ok(Self {
321            session_nb: u16::from_be_bytes([v[0], v[1]]),
322        })
323    }
324}
325impl Serialize for CloseSessionRequest {
326    type Error = Error;
327    fn serialized_len(&self) -> usize {
328        spdu_len(2)
329    }
330    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
331        let pos = write_spdu_header(tags::CLOSE_SESSION_REQUEST, 2, buf)?;
332        buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
333        Ok(pos + 2)
334    }
335}
336
337/// `close_session_response()` (Table 11) — `length=3`.
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339#[cfg_attr(feature = "serde", derive(serde::Serialize))]
340pub struct CloseSessionResponse {
341    /// `session_status` (Table 12 values).
342    pub status: SessionStatus,
343    /// `session_nb` that was closed.
344    pub session_nb: u16,
345}
346
347impl<'a> Parse<'a> for CloseSessionResponse {
348    type Error = Error;
349    fn parse(bytes: &'a [u8]) -> Result<Self> {
350        let v = parse_spdu_header(
351            bytes,
352            tags::CLOSE_SESSION_RESPONSE,
353            "close_session_response",
354        )?;
355        if v.len() != 3 {
356            return Err(Error::InvalidObject {
357                what: "close_session_response",
358                reason: "value must be 3 bytes",
359            });
360        }
361        Ok(Self {
362            status: SessionStatus::from_u8(v[0]),
363            session_nb: u16::from_be_bytes([v[1], v[2]]),
364        })
365    }
366}
367impl Serialize for CloseSessionResponse {
368    type Error = Error;
369    fn serialized_len(&self) -> usize {
370        spdu_len(3)
371    }
372    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
373        let mut pos = write_spdu_header(tags::CLOSE_SESSION_RESPONSE, 3, buf)?;
374        buf[pos] = self.status.to_u8();
375        pos += 1;
376        buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
377        Ok(pos + 2)
378    }
379}
380
381/// `session_number()` (Table 13) — `length=2`. Precedes an SPDU body of APDUs;
382/// this models only the header (the trailing APDU body is parsed separately by
383/// the caller with [`AnyApdu`](crate::AnyApdu)).
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385#[cfg_attr(feature = "serde", derive(serde::Serialize))]
386pub struct SessionNumber {
387    /// `session_nb` the following APDUs belong to.
388    pub session_nb: u16,
389}
390
391impl SessionNumber {
392    /// Header length in bytes (`spdu_tag` + `length_field=2` + `session_nb`) =
393    /// the offset at which the trailing APDU body begins.
394    pub const HEADER_LEN: usize = 4;
395}
396
397impl<'a> Parse<'a> for SessionNumber {
398    type Error = Error;
399    fn parse(bytes: &'a [u8]) -> Result<Self> {
400        // session_number is followed by an APDU body; only the value (length=2)
401        // belongs to the SPDU header, so we read exactly that and ignore any
402        // trailing APDU bytes.
403        let v = parse_spdu_header(bytes, tags::SESSION_NUMBER, "session_number")?;
404        if v.len() != 2 {
405            return Err(Error::InvalidObject {
406                what: "session_number",
407                reason: "value must be 2 bytes",
408            });
409        }
410        Ok(Self {
411            session_nb: u16::from_be_bytes([v[0], v[1]]),
412        })
413    }
414}
415impl Serialize for SessionNumber {
416    type Error = Error;
417    fn serialized_len(&self) -> usize {
418        spdu_len(2)
419    }
420    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
421        let pos = write_spdu_header(tags::SESSION_NUMBER, 2, buf)?;
422        buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
423        Ok(pos + 2)
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use crate::resource::CONDITIONAL_ACCESS_SUPPORT;
431
432    #[test]
433    fn open_session_request_round_trip() {
434        let o = OpenSessionRequest {
435            resource: CONDITIONAL_ACCESS_SUPPORT,
436        };
437        let bytes = o.to_bytes();
438        assert_eq!(bytes, [0x91, 0x04, 0x00, 0x03, 0x00, 0x41]);
439        assert_eq!(OpenSessionRequest::parse(&bytes).unwrap(), o);
440    }
441
442    #[test]
443    fn open_session_response_round_trip() {
444        let o = OpenSessionResponse {
445            status: SessionStatus::Ok,
446            resource: CONDITIONAL_ACCESS_SUPPORT,
447            session_nb: 0x0102,
448        };
449        let bytes = o.to_bytes();
450        assert_eq!(bytes[0], tags::OPEN_SESSION_RESPONSE);
451        assert_eq!(bytes[1], 0x07);
452        let parsed = OpenSessionResponse::parse(&bytes).unwrap();
453        assert_eq!(parsed, o);
454        assert_eq!(parsed.status.name(), "ok");
455    }
456
457    #[test]
458    fn create_session_round_trip() {
459        let c = CreateSession {
460            resource: CONDITIONAL_ACCESS_SUPPORT,
461            session_nb: 5,
462        };
463        let bytes = c.to_bytes();
464        assert_eq!(bytes[1], 0x06);
465        assert_eq!(CreateSession::parse(&bytes).unwrap(), c);
466    }
467
468    #[test]
469    fn create_session_response_round_trip() {
470        let c = CreateSessionResponse {
471            status: SessionStatus::ResourceBusy,
472            resource: CONDITIONAL_ACCESS_SUPPORT,
473            session_nb: 5,
474        };
475        let bytes = c.to_bytes();
476        let parsed = CreateSessionResponse::parse(&bytes).unwrap();
477        assert_eq!(parsed, c);
478        assert_eq!(parsed.status.name(), "resource_busy");
479    }
480
481    #[test]
482    fn close_session_round_trips() {
483        let req = CloseSessionRequest { session_nb: 0x00FF };
484        assert_eq!(req.to_bytes(), [0x95, 0x02, 0x00, 0xFF]);
485        assert_eq!(CloseSessionRequest::parse(&req.to_bytes()).unwrap(), req);
486
487        let resp = CloseSessionResponse {
488            status: SessionStatus::Ok,
489            session_nb: 0x00FF,
490        };
491        assert_eq!(resp.to_bytes(), [0x96, 0x03, 0x00, 0x00, 0xFF]);
492        assert_eq!(CloseSessionResponse::parse(&resp.to_bytes()).unwrap(), resp);
493    }
494
495    #[test]
496    fn session_number_round_trip_and_header_len() {
497        let sn = SessionNumber { session_nb: 0x1234 };
498        let bytes = sn.to_bytes();
499        assert_eq!(bytes, [0x90, 0x02, 0x12, 0x34]);
500        assert_eq!(bytes.len(), SessionNumber::HEADER_LEN);
501        // Parses fine even with a trailing APDU body.
502        let mut with_body = bytes.to_vec();
503        with_body.extend_from_slice(&[0x9F, 0x80, 0x30, 0x00]);
504        assert_eq!(SessionNumber::parse(&with_body).unwrap(), sn);
505    }
506
507    #[test]
508    fn mutating_session_nb_changes_bytes() {
509        let req = CloseSessionRequest { session_nb: 1 };
510        let a = req.to_bytes();
511        let b = CloseSessionRequest { session_nb: 2 }.to_bytes();
512        assert_ne!(a, b);
513    }
514
515    #[test]
516    fn rejects_wrong_tag() {
517        assert!(matches!(
518            OpenSessionRequest::parse(&[0x92, 0x04, 0, 0, 0, 0]),
519            Err(Error::UnexpectedSpduTag { .. })
520        ));
521    }
522}