Skip to main content

dvb_ci/
tpdu.rs

1//! Transport Protocol Data Unit (TPDU) framing — ETSI EN 50221 Annex A §A.4.1,
2//! Tables A.1-A.16 (PDF pp. 63-70).
3//!
4//! The transport layer is a command-response protocol: the host sends a Command
5//! TPDU (C_TPDU), the module replies with a Response TPDU (R_TPDU) that always
6//! ends in a Status Byte. The `length_field` uses the Table 1 coding (see
7//! [`crate::length`]). This module covers the wire framing only — the PC-Card
8//! hardware transport itself is out of scope.
9
10use crate::error::{Error, Result};
11use crate::length;
12use alloc::vec::Vec;
13use dvb_common::{Parse, Serialize};
14
15/// `tpdu_tag` values — Table A.16 (p. 70). One byte each.
16pub mod tags {
17    /// `TSB` (status byte) = `80`.
18    pub const SB: u8 = 0x80;
19    /// `TRCV` (receive data) = `81`.
20    pub const RCV: u8 = 0x81;
21    /// `Tcreate_t_c` = `82`.
22    pub const CREATE_T_C: u8 = 0x82;
23    /// `Tc_t_c_reply` = `83`.
24    pub const C_T_C_REPLY: u8 = 0x83;
25    /// `Tdelete_t_c` = `84`.
26    pub const DELETE_T_C: u8 = 0x84;
27    /// `Td_t_c_reply` = `85`.
28    pub const D_T_C_REPLY: u8 = 0x85;
29    /// `Trequest_t_c` = `86`.
30    pub const REQUEST_T_C: u8 = 0x86;
31    /// `Tnew_t_c` = `87`.
32    pub const NEW_T_C: u8 = 0x87;
33    /// `Tt_c_error` = `88`.
34    pub const T_C_ERROR: u8 = 0x88;
35    /// `Tdata_last` (last data block) = `A0`.
36    pub const DATA_LAST: u8 = 0xA0;
37    /// `Tdata_more` (more data follows) = `A1`.
38    pub const DATA_MORE: u8 = 0xA1;
39}
40
41/// Status Byte value (`SB_value`, Figure A.6 + Table A.3, p. 64). The single
42/// 1-bit DA (Data Available) flag is bit 8; the remaining bits are reserved
43/// (shall be zero) and preserved here for fidelity.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize))]
46pub struct SbValue(pub u8);
47
48impl SbValue {
49    /// DA (Data Available): true when the module has a message for the host.
50    #[must_use]
51    pub const fn data_available(self) -> bool {
52        self.0 & 0x80 != 0
53    }
54    /// Build an `SB_value` with the DA bit set/clear and reserved bits zero.
55    #[must_use]
56    pub const fn new(data_available: bool) -> Self {
57        Self(if data_available { 0x80 } else { 0x00 })
58    }
59}
60
61/// `c_TPDU_tag` for the data-carrying C_TPDU (chaining): last vs. more (§A.4.1).
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize))]
64#[non_exhaustive]
65pub enum DataBlock {
66    /// `Tdata_last` (`A0`): the final (or only) data block.
67    Last,
68    /// `Tdata_more` (`A1`): more blocks follow.
69    More,
70}
71
72impl DataBlock {
73    fn from_tag(tag: u8) -> Option<Self> {
74        match tag {
75            tags::DATA_LAST => Some(Self::Last),
76            tags::DATA_MORE => Some(Self::More),
77            _ => None,
78        }
79    }
80    /// The `tpdu_tag` byte for this data block (`A0` last / `A1` more).
81    #[must_use]
82    pub fn to_tag(self) -> u8 {
83        match self {
84            Self::Last => tags::DATA_LAST,
85            Self::More => tags::DATA_MORE,
86        }
87    }
88    /// Spec token.
89    #[must_use]
90    pub fn name(&self) -> &'static str {
91        match self {
92            Self::Last => "data_last",
93            Self::More => "data_more",
94        }
95    }
96}
97dvb_common::impl_spec_display!(DataBlock);
98
99// --- single-field connection-management objects (tag + length + t_c_id) ---
100
101/// A connection-management object carrying just `t_c_id`: `Create_T_C`
102/// (Table A.4), `C_T_C_Reply`, `Delete_T_C` (A.6), `D_T_C_Reply`, `Request_T_C`
103/// (A.8). The `tag` distinguishes which (`length_field = 1`).
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106pub struct TcObject {
107    /// The `tpdu_tag` (one of `CREATE_T_C`/`C_T_C_REPLY`/`DELETE_T_C`/
108    /// `D_T_C_REPLY`/`REQUEST_T_C`).
109    pub tag: u8,
110    /// `t_c_id` — the transport connection identifier.
111    pub t_c_id: u8,
112}
113
114impl<'a> Parse<'a> for TcObject {
115    type Error = Error;
116    fn parse(bytes: &'a [u8]) -> Result<Self> {
117        if bytes.is_empty() {
118            return Err(Error::BufferTooShort {
119                need: 1,
120                have: 0,
121                what: "TcObject",
122            });
123        }
124        let tag = bytes[0];
125        let (len, hdr) = length::decode(&bytes[1..])?;
126        if len != 1 {
127            return Err(Error::InvalidObject {
128                what: "TcObject",
129                reason: "length_field must be 1",
130            });
131        }
132        let t_c_id = *bytes.get(1 + hdr).ok_or(Error::BufferTooShort {
133            need: 1 + hdr + 1,
134            have: bytes.len(),
135            what: "TcObject t_c_id",
136        })?;
137        Ok(Self { tag, t_c_id })
138    }
139}
140impl Serialize for TcObject {
141    type Error = Error;
142    fn serialized_len(&self) -> usize {
143        3 // tag + length_field(1) + t_c_id
144    }
145    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
146        if buf.len() < 3 {
147            return Err(Error::OutputBufferTooSmall {
148                need: 3,
149                have: buf.len(),
150            });
151        }
152        buf[0] = self.tag;
153        buf[1] = 1;
154        buf[2] = self.t_c_id;
155        Ok(3)
156    }
157}
158
159/// `New_T_C` (Table A.9, `length=2`): a new connection identifier to establish.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize))]
162pub struct NewTc {
163    /// `t_c_id` of the connection issuing this.
164    pub t_c_id: u8,
165    /// `new_t_c_id` — the identifier for the new connection.
166    pub new_t_c_id: u8,
167}
168
169impl<'a> Parse<'a> for NewTc {
170    type Error = Error;
171    fn parse(bytes: &'a [u8]) -> Result<Self> {
172        let body = parse_fixed(bytes, tags::NEW_T_C, 2, "New_T_C")?;
173        Ok(Self {
174            t_c_id: body[0],
175            new_t_c_id: body[1],
176        })
177    }
178}
179impl Serialize for NewTc {
180    type Error = Error;
181    fn serialized_len(&self) -> usize {
182        4
183    }
184    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
185        write_fixed(tags::NEW_T_C, &[self.t_c_id, self.new_t_c_id], buf)
186    }
187}
188
189/// `T_C_Error` (Table A.10, `length=2`): an error on a transport connection.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize))]
192pub struct TcError {
193    /// `t_c_id`.
194    pub t_c_id: u8,
195    /// `error_code` (Table A.11: `1` = no transport connections available).
196    pub error_code: u8,
197}
198
199impl<'a> Parse<'a> for TcError {
200    type Error = Error;
201    fn parse(bytes: &'a [u8]) -> Result<Self> {
202        let body = parse_fixed(bytes, tags::T_C_ERROR, 2, "T_C_Error")?;
203        Ok(Self {
204            t_c_id: body[0],
205            error_code: body[1],
206        })
207    }
208}
209impl Serialize for TcError {
210    type Error = Error;
211    fn serialized_len(&self) -> usize {
212        4
213    }
214    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
215        write_fixed(tags::T_C_ERROR, &[self.t_c_id, self.error_code], buf)
216    }
217}
218
219/// `C_TPDU` (Table A.1): a Command TPDU, host → module. `length_field` covers
220/// `t_c_id` + data.
221#[derive(Debug, Clone, PartialEq, Eq)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize))]
223pub struct CommandTpdu<'a> {
224    /// The `c_tpdu_tag` (e.g. `RCV`, or `DATA_LAST`/`DATA_MORE` for Send Data).
225    pub tag: u8,
226    /// `t_c_id`.
227    pub t_c_id: u8,
228    /// Data field (`length_value - 1` bytes).
229    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
230    pub data: &'a [u8],
231}
232
233impl<'a> Parse<'a> for CommandTpdu<'a> {
234    type Error = Error;
235    fn parse(bytes: &'a [u8]) -> Result<Self> {
236        if bytes.is_empty() {
237            return Err(Error::BufferTooShort {
238                need: 1,
239                have: 0,
240                what: "C_TPDU",
241            });
242        }
243        let tag = bytes[0];
244        let (len, hdr) = length::decode(&bytes[1..])?;
245        if len == 0 {
246            return Err(Error::InvalidObject {
247                what: "C_TPDU",
248                reason: "length_field must include t_c_id (>=1)",
249            });
250        }
251        let start = 1 + hdr;
252        let end = start + len;
253        if bytes.len() < end {
254            return Err(Error::LengthMismatch {
255                what: "C_TPDU",
256                declared: len,
257                actual: bytes.len().saturating_sub(start),
258            });
259        }
260        Ok(Self {
261            tag,
262            t_c_id: bytes[start],
263            data: &bytes[start + 1..end],
264        })
265    }
266}
267impl Serialize for CommandTpdu<'_> {
268    type Error = Error;
269    fn serialized_len(&self) -> usize {
270        let len_value = 1 + self.data.len();
271        1 + length::encoded_len(len_value) + len_value
272    }
273    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
274        let len_value = 1 + self.data.len();
275        let total = 1 + length::encoded_len(len_value) + len_value;
276        if buf.len() < total {
277            return Err(Error::OutputBufferTooSmall {
278                need: total,
279                have: buf.len(),
280            });
281        }
282        buf[0] = self.tag;
283        let mut pos = 1 + length::encode_into(len_value, &mut buf[1..])?;
284        buf[pos] = self.t_c_id;
285        pos += 1;
286        buf[pos..pos + self.data.len()].copy_from_slice(self.data);
287        Ok(pos + self.data.len())
288    }
289}
290
291/// `R_TPDU` (Table A.2): a Response TPDU, module → host. The mandatory trailing
292/// Status (SB) is NOT included in the `length_field` and is modelled separately.
293#[derive(Debug, Clone, PartialEq, Eq)]
294#[cfg_attr(feature = "serde", derive(serde::Serialize))]
295pub struct ResponseTpdu<'a> {
296    /// The `r_tpdu_tag` (e.g. `DATA_LAST`/`DATA_MORE`).
297    pub tag: u8,
298    /// `t_c_id`.
299    pub t_c_id: u8,
300    /// Data field (`length_value - 1` bytes).
301    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
302    pub data: &'a [u8],
303    /// `SB_value` from the mandatory trailing Status.
304    pub sb_value: SbValue,
305    /// Whether the data block was the last (`Tdata_last`) — convenience view of
306    /// `tag` for chaining, `None` if `tag` is not a data block tag.
307    pub block: Option<DataBlock>,
308}
309
310impl<'a> Parse<'a> for ResponseTpdu<'a> {
311    type Error = Error;
312    fn parse(bytes: &'a [u8]) -> Result<Self> {
313        if bytes.is_empty() {
314            return Err(Error::BufferTooShort {
315                need: 1,
316                have: 0,
317                what: "R_TPDU",
318            });
319        }
320        let tag = bytes[0];
321        let (len, hdr) = length::decode(&bytes[1..])?;
322        if len == 0 {
323            return Err(Error::InvalidObject {
324                what: "R_TPDU",
325                reason: "length_field must include t_c_id (>=1)",
326            });
327        }
328        let start = 1 + hdr;
329        let data_end = start + len;
330        // Status trailer: SB_tag + length_field(=2) + t_c_id + SB_value = 4 bytes.
331        let status_end = data_end + 4;
332        if bytes.len() < status_end {
333            return Err(Error::BufferTooShort {
334                need: status_end,
335                have: bytes.len(),
336                what: "R_TPDU status",
337            });
338        }
339        if bytes[data_end] != tags::SB {
340            return Err(Error::UnexpectedTpduTag {
341                got: bytes[data_end],
342                expected: tags::SB,
343                what: "R_TPDU SB_tag",
344            });
345        }
346        if bytes[data_end + 1] != 2 {
347            return Err(Error::InvalidObject {
348                what: "R_TPDU status",
349                reason: "SB length_field must be 2",
350            });
351        }
352        // bytes[data_end+2] = t_c_id (status), bytes[data_end+3] = SB_value.
353        Ok(Self {
354            tag,
355            t_c_id: bytes[start],
356            data: &bytes[start + 1..data_end],
357            sb_value: SbValue(bytes[data_end + 3]),
358            block: DataBlock::from_tag(tag),
359        })
360    }
361}
362impl Serialize for ResponseTpdu<'_> {
363    type Error = Error;
364    fn serialized_len(&self) -> usize {
365        let len_value = 1 + self.data.len();
366        1 + length::encoded_len(len_value) + len_value + 4
367    }
368    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
369        let total = self.serialized_len();
370        if buf.len() < total {
371            return Err(Error::OutputBufferTooSmall {
372                need: total,
373                have: buf.len(),
374            });
375        }
376        let len_value = 1 + self.data.len();
377        buf[0] = self.tag;
378        let mut pos = 1 + length::encode_into(len_value, &mut buf[1..])?;
379        buf[pos] = self.t_c_id;
380        pos += 1;
381        buf[pos..pos + self.data.len()].copy_from_slice(self.data);
382        pos += self.data.len();
383        // Status trailer.
384        buf[pos] = tags::SB;
385        buf[pos + 1] = 2;
386        buf[pos + 2] = self.t_c_id;
387        buf[pos + 3] = self.sb_value.0;
388        Ok(pos + 4)
389    }
390}
391
392// --- helpers for the fixed-length tag+length+body objects ---
393
394fn parse_fixed<'a>(
395    bytes: &'a [u8],
396    expected: u8,
397    body_len: usize,
398    what: &'static str,
399) -> Result<&'a [u8]> {
400    if bytes.is_empty() {
401        return Err(Error::BufferTooShort {
402            need: 1,
403            have: 0,
404            what,
405        });
406    }
407    if bytes[0] != expected {
408        return Err(Error::UnexpectedTpduTag {
409            got: bytes[0],
410            expected,
411            what,
412        });
413    }
414    let (len, hdr) = length::decode(&bytes[1..])?;
415    if len != body_len {
416        return Err(Error::InvalidObject {
417            what,
418            reason: "unexpected length_field",
419        });
420    }
421    let start = 1 + hdr;
422    let end = start + body_len;
423    if bytes.len() < end {
424        return Err(Error::BufferTooShort {
425            need: end,
426            have: bytes.len(),
427            what,
428        });
429    }
430    Ok(&bytes[start..end])
431}
432
433fn write_fixed(tag: u8, body: &[u8], buf: &mut [u8]) -> Result<usize> {
434    let total = 2 + body.len();
435    if buf.len() < total {
436        return Err(Error::OutputBufferTooSmall {
437            need: total,
438            have: buf.len(),
439        });
440    }
441    buf[0] = tag;
442    buf[1] = body.len() as u8;
443    buf[2..2 + body.len()].copy_from_slice(body);
444    Ok(total)
445}
446
447/// Build a `Create_T_C` object (`tag = CREATE_T_C`, `t_c_id`).
448#[must_use]
449pub fn create_t_c(t_c_id: u8) -> TcObject {
450    TcObject {
451        tag: tags::CREATE_T_C,
452        t_c_id,
453    }
454}
455
456/// Collect a `Vec` of the wire bytes for a [`TcObject`] (convenience).
457#[must_use]
458pub fn tc_object_bytes(o: &TcObject) -> Vec<u8> {
459    o.to_bytes()
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn tc_object_round_trip() {
468        let o = create_t_c(0x01);
469        let bytes = o.to_bytes();
470        assert_eq!(bytes, [0x82, 0x01, 0x01]);
471        assert_eq!(TcObject::parse(&bytes).unwrap(), o);
472    }
473
474    #[test]
475    fn new_tc_round_trip() {
476        let n = NewTc {
477            t_c_id: 1,
478            new_t_c_id: 2,
479        };
480        let bytes = n.to_bytes();
481        assert_eq!(bytes, [0x87, 0x02, 0x01, 0x02]);
482        assert_eq!(NewTc::parse(&bytes).unwrap(), n);
483    }
484
485    #[test]
486    fn tc_error_round_trip() {
487        let e = TcError {
488            t_c_id: 3,
489            error_code: 1,
490        };
491        let bytes = e.to_bytes();
492        assert_eq!(bytes, [0x88, 0x02, 0x03, 0x01]);
493        assert_eq!(TcError::parse(&bytes).unwrap(), e);
494    }
495
496    #[test]
497    fn command_tpdu_round_trip() {
498        let c = CommandTpdu {
499            tag: tags::DATA_LAST,
500            t_c_id: 1,
501            data: &[0xAA, 0xBB, 0xCC],
502        };
503        let bytes = c.to_bytes();
504        // tag A0, length 4 (t_c_id + 3 data), t_c_id, data.
505        assert_eq!(bytes, [0xA0, 0x04, 0x01, 0xAA, 0xBB, 0xCC]);
506        assert_eq!(CommandTpdu::parse(&bytes).unwrap(), c);
507    }
508
509    #[test]
510    fn receive_data_command_no_payload() {
511        let c = CommandTpdu {
512            tag: tags::RCV,
513            t_c_id: 1,
514            data: &[],
515        };
516        let bytes = c.to_bytes();
517        assert_eq!(bytes, [0x81, 0x01, 0x01]);
518        assert_eq!(CommandTpdu::parse(&bytes).unwrap(), c);
519    }
520
521    #[test]
522    fn response_tpdu_round_trip_with_status() {
523        let r = ResponseTpdu {
524            tag: tags::DATA_LAST,
525            t_c_id: 1,
526            data: &[0x9F, 0x80, 0x30, 0x00],
527            sb_value: SbValue::new(true),
528            block: Some(DataBlock::Last),
529        };
530        let bytes = r.to_bytes();
531        // header: A0 05 01 + 4 data ; status: 80 02 01 80.
532        assert_eq!(
533            bytes,
534            [0xA0, 0x05, 0x01, 0x9F, 0x80, 0x30, 0x00, 0x80, 0x02, 0x01, 0x80]
535        );
536        let parsed = ResponseTpdu::parse(&bytes).unwrap();
537        assert_eq!(parsed, r);
538        assert!(parsed.sb_value.data_available());
539        assert_eq!(parsed.block, Some(DataBlock::Last));
540    }
541
542    #[test]
543    fn mutating_data_changes_bytes() {
544        let c = CommandTpdu {
545            tag: tags::DATA_LAST,
546            t_c_id: 1,
547            data: &[0xAA],
548        };
549        let a = c.to_bytes();
550        let b = CommandTpdu {
551            tag: tags::DATA_LAST,
552            t_c_id: 1,
553            data: &[0xBB],
554        }
555        .to_bytes();
556        assert_ne!(a, b);
557    }
558
559    #[test]
560    fn sb_value_da() {
561        assert!(SbValue::new(true).data_available());
562        assert!(!SbValue::new(false).data_available());
563    }
564}