Skip to main content

dvb_ci_runtime/
trace.rs

1//! Human-readable decoding of CI link frames, for diagnosing live-CAM
2//! exchanges (e.g. issue #337).
3//!
4//! [`decode_frame`] turns one raw link frame into a one-line annotation —
5//! TPDU tag → SPDU tag → APDU tag — so a [`RecordingCaDevice`] capture reads
6//! like the byte traces in bug reports without hand-decoding. [`decode_log`]
7//! formats a whole captured exchange.
8//!
9//! [`RecordingCaDevice`]: crate::device::RecordingCaDevice
10
11use crate::device::LinkEvent;
12use dvb_ci::spdu::tags as spdu_tags;
13use dvb_ci::tag::ApduTag;
14use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
15
16/// Name of a TPDU tag (§A.4), or `"tpdu(0xXX)"` for an unknown one.
17fn tpdu_name(tag: u8) -> &'static str {
18    match tag {
19        tpdu_tags::SB => "T_SB",
20        tpdu_tags::RCV => "T_RCV",
21        tpdu_tags::CREATE_T_C => "Create_T_C",
22        tpdu_tags::C_T_C_REPLY => "C_T_C_Reply",
23        tpdu_tags::T_C_ERROR => "T_C_Error",
24        tpdu_tags::DATA_LAST => "T_Data_Last",
25        tpdu_tags::DATA_MORE => "T_Data_More",
26        _ => "T_?",
27    }
28}
29
30/// Name of a session SPDU tag (§7), or `"spdu(0xXX)"` for an unknown one.
31fn spdu_name(tag: u8) -> &'static str {
32    match tag {
33        spdu_tags::SESSION_NUMBER => "session_number",
34        spdu_tags::OPEN_SESSION_REQUEST => "open_session_request",
35        spdu_tags::OPEN_SESSION_RESPONSE => "open_session_response",
36        spdu_tags::CREATE_SESSION => "create_session",
37        spdu_tags::CREATE_SESSION_RESPONSE => "create_session_response",
38        spdu_tags::CLOSE_SESSION_REQUEST => "close_session_request",
39        spdu_tags::CLOSE_SESSION_RESPONSE => "close_session_response",
40        _ => "spdu(?)",
41    }
42}
43
44/// Decode the APDU at the start of `apdu` (3-byte tag) into `name (9F80xx)`.
45fn apdu_label(apdu: &[u8]) -> String {
46    match apdu.first_chunk::<3>() {
47        Some(&[a, b, c]) => {
48            let tag = ApduTag::from_bytes(a, b, c);
49            format!("{} ({:02X}{:02X}{:02X})", tag.name(), a, b, c)
50        }
51        None => "apdu(short)".to_string(),
52    }
53}
54
55/// Decode the SPDU payload of a data TPDU into a label, recursing into the APDU
56/// when the SPDU is a `session_number` wrapper.
57fn spdu_label(spdu: &[u8]) -> String {
58    match spdu.first().copied() {
59        Some(spdu_tags::SESSION_NUMBER) if spdu.len() >= 4 => {
60            let nb = u16::from_be_bytes([spdu[2], spdu[3]]);
61            let rest = &spdu[4..];
62            if rest.is_empty() {
63                format!("session {nb}")
64            } else {
65                format!("session {nb} · {}", apdu_label(rest))
66            }
67        }
68        Some(t) => spdu_name(t).to_string(),
69        None => "empty".to_string(),
70    }
71}
72
73/// Decode one raw link frame into a one-line annotation.
74///
75/// Handles the leading TPDU, the SPDU it carries (for `T_Data_*`), and the APDU
76/// inside a `session_number` SPDU. Appended `T_SB` data-available bits are noted.
77#[must_use]
78pub fn decode_frame(frame: &[u8]) -> String {
79    let Some(&tag) = frame.first() else {
80        return "empty frame".to_string();
81    };
82    match tag {
83        tpdu_tags::SB => match frame.get(3) {
84            Some(&sb) => format!(
85                "T_SB tcid={} DA={}",
86                frame.get(2).copied().unwrap_or(0),
87                u8::from(SbValue(sb).data_available())
88            ),
89            None => "T_SB (short)".to_string(),
90        },
91        tpdu_tags::CREATE_T_C | tpdu_tags::C_T_C_REPLY | tpdu_tags::RCV | tpdu_tags::T_C_ERROR => {
92            format!(
93                "{} tcid={}",
94                tpdu_name(tag),
95                frame.get(2).copied().unwrap_or(0)
96            )
97        }
98        tpdu_tags::DATA_LAST | tpdu_tags::DATA_MORE => {
99            // tag · length · t_c_id · data(=SPDU) · [appended T_SB]
100            let len = frame.get(1).copied().unwrap_or(0) as usize;
101            let tcid = frame.get(2).copied().unwrap_or(0);
102            // `length` counts t_c_id + data; data is the SPDU.
103            let data_end = (2 + len).min(frame.len());
104            let spdu = frame.get(3..data_end).unwrap_or(&[]);
105            if spdu.is_empty() {
106                format!("{} tcid={} (poll)", tpdu_name(tag), tcid)
107            } else {
108                format!("{} tcid={} · {}", tpdu_name(tag), tcid, spdu_label(spdu))
109            }
110        }
111        _ => format!("{} {:02X?}", tpdu_name(tag), &frame[..frame.len().min(8)]),
112    }
113}
114
115/// Format a whole captured exchange as a multi-line annotated trace.
116#[must_use]
117pub fn decode_log(log: &[LinkEvent]) -> String {
118    let mut out = String::new();
119    for ev in log {
120        let line = match ev {
121            LinkEvent::Tx(f) => format!("W {}", decode_frame(f)),
122            LinkEvent::Rx(f) => format!("R {}", decode_frame(f)),
123            LinkEvent::Reset => "  reset()".to_string(),
124            LinkEvent::SlotInfo(si) => format!("  slot_info() -> ready={}", si.module_ready),
125        };
126        out.push_str(&line);
127        out.push('\n');
128    }
129    out
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn decodes_the_337_handshake_frames() {
138        // Bytes lifted from issue #337's trace.
139        assert_eq!(decode_frame(&[0x82, 0x01, 0x01]), "Create_T_C tcid=1");
140        assert_eq!(decode_frame(&[0x81, 0x01, 0x01]), "T_RCV tcid=1");
141        assert_eq!(decode_frame(&[0x80, 0x02, 0x01, 0x00]), "T_SB tcid=1 DA=0");
142
143        // open_session_request (module): a0 07 01 | 91 04 00 01 00 41 | SB
144        let osr = [
145            0xA0, 0x07, 0x01, 0x91, 0x04, 0x00, 0x01, 0x00, 0x41, 0x80, 0x02, 0x01, 0x00,
146        ];
147        assert_eq!(
148            decode_frame(&osr),
149            "T_Data_Last tcid=1 · open_session_request"
150        );
151
152        // profile_enq (host): a0 09 01 | 90 02 00 01 | 9f 80 10 00
153        let enq = [
154            0xA0, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00,
155        ];
156        assert_eq!(
157            decode_frame(&enq),
158            "T_Data_Last tcid=1 · session 1 · profile_enq (9F8010)"
159        );
160    }
161
162    #[test]
163    fn decodes_log_directions() {
164        let log = [
165            LinkEvent::Reset,
166            LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
167            LinkEvent::Rx(vec![0x80, 0x02, 0x01, 0x00]),
168        ];
169        let s = decode_log(&log);
170        assert!(s.contains("  reset()"));
171        assert!(s.contains("W Create_T_C tcid=1"));
172        assert!(s.contains("R T_SB tcid=1 DA=0"));
173    }
174}