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::resource::ResourceId;
13use dvb_ci::spdu::tags as spdu_tags;
14use dvb_ci::tag::ApduTag;
15use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
16
17/// Name of a TPDU tag (§A.4), or `"tpdu(0xXX)"` for an unknown one.
18fn tpdu_name(tag: u8) -> &'static str {
19    match tag {
20        tpdu_tags::SB => "T_SB",
21        tpdu_tags::RCV => "T_RCV",
22        tpdu_tags::CREATE_T_C => "Create_T_C",
23        tpdu_tags::C_T_C_REPLY => "C_T_C_Reply",
24        tpdu_tags::T_C_ERROR => "T_C_Error",
25        tpdu_tags::DATA_LAST => "T_Data_Last",
26        tpdu_tags::DATA_MORE => "T_Data_More",
27        _ => "T_?",
28    }
29}
30
31/// Name of a session SPDU tag (§7), or `"spdu(0xXX)"` for an unknown one.
32fn spdu_name(tag: u8) -> &'static str {
33    match tag {
34        spdu_tags::SESSION_NUMBER => "session_number",
35        spdu_tags::OPEN_SESSION_REQUEST => "open_session_request",
36        spdu_tags::OPEN_SESSION_RESPONSE => "open_session_response",
37        spdu_tags::CREATE_SESSION => "create_session",
38        spdu_tags::CREATE_SESSION_RESPONSE => "create_session_response",
39        spdu_tags::CLOSE_SESSION_REQUEST => "close_session_request",
40        spdu_tags::CLOSE_SESSION_RESPONSE => "close_session_response",
41        _ => "spdu(?)",
42    }
43}
44
45/// Decode the APDU at the start of `apdu` (3-byte tag) into `name (9F80xx)`.
46fn apdu_label(apdu: &[u8]) -> String {
47    match apdu.first_chunk::<3>() {
48        Some(&[a, b, c]) => {
49            let tag = ApduTag::from_bytes(a, b, c);
50            format!("{} ({:02X}{:02X}{:02X})", tag.name(), a, b, c)
51        }
52        None => "apdu(short)".to_string(),
53    }
54}
55
56/// Decode the SPDU payload of a data TPDU into a label, recursing into the APDU
57/// when the SPDU is a `session_number` wrapper.
58fn spdu_label(spdu: &[u8]) -> String {
59    match spdu.first().copied() {
60        Some(spdu_tags::SESSION_NUMBER) if spdu.len() >= 4 => {
61            let nb = u16::from_be_bytes([spdu[2], spdu[3]]);
62            let rest = &spdu[4..];
63            if rest.is_empty() {
64                format!("session {nb}")
65            } else {
66                format!("session {nb} · {}", apdu_label(rest))
67            }
68        }
69        // Session-management SPDUs: surface the resource_id (+ status/nb) so a
70        // live trace shows *which* resource each create/open targets.
71        Some(spdu_tags::OPEN_SESSION_REQUEST) if spdu.len() >= 6 => {
72            let r = ResourceId(u32::from_be_bytes([spdu[2], spdu[3], spdu[4], spdu[5]]));
73            format!("open_session_request {}", r.name())
74        }
75        Some(spdu_tags::CREATE_SESSION) if spdu.len() >= 8 => {
76            let r = ResourceId(u32::from_be_bytes([spdu[2], spdu[3], spdu[4], spdu[5]]));
77            let nb = u16::from_be_bytes([spdu[6], spdu[7]]);
78            format!("create_session {} (nb {nb})", r.name())
79        }
80        Some(t @ (spdu_tags::CREATE_SESSION_RESPONSE | spdu_tags::OPEN_SESSION_RESPONSE))
81            if spdu.len() >= 9 =>
82        {
83            let status = spdu[2];
84            let r = ResourceId(u32::from_be_bytes([spdu[3], spdu[4], spdu[5], spdu[6]]));
85            let nb = u16::from_be_bytes([spdu[7], spdu[8]]);
86            format!("{} {} status={status:#04x} nb={nb}", spdu_name(t), r.name())
87        }
88        Some(t) => spdu_name(t).to_string(),
89        None => "empty".to_string(),
90    }
91}
92
93/// Decode one raw link frame into a one-line annotation.
94///
95/// Handles the leading TPDU, the SPDU it carries (for `T_Data_*`), and the APDU
96/// inside a `session_number` SPDU. Appended `T_SB` data-available bits are noted.
97#[must_use]
98pub fn decode_frame(frame: &[u8]) -> String {
99    let Some(&tag) = frame.first() else {
100        return "empty frame".to_string();
101    };
102    match tag {
103        tpdu_tags::SB => match frame.get(3) {
104            Some(&sb) => format!(
105                "T_SB tcid={} DA={}",
106                frame.get(2).copied().unwrap_or(0),
107                u8::from(SbValue(sb).data_available())
108            ),
109            None => "T_SB (short)".to_string(),
110        },
111        tpdu_tags::CREATE_T_C | tpdu_tags::C_T_C_REPLY | tpdu_tags::RCV | tpdu_tags::T_C_ERROR => {
112            format!(
113                "{} tcid={}",
114                tpdu_name(tag),
115                frame.get(2).copied().unwrap_or(0)
116            )
117        }
118        tpdu_tags::DATA_LAST | tpdu_tags::DATA_MORE => {
119            // tag · length_field · t_c_id · data(=SPDU) · [appended T_SB].
120            // length_field uses the Table-1 codec (long-form for >127 bytes), so
121            // decode it rather than assuming a single byte.
122            let Ok((len, hdr)) = dvb_ci::length::decode(&frame[1..]) else {
123                return format!("{} (bad length_field)", tpdu_name(tag));
124            };
125            let start = 1 + hdr; // t_c_id offset
126            let tcid = frame.get(start).copied().unwrap_or(0);
127            // `length` counts t_c_id + data; the SPDU is the data after t_c_id.
128            let data_end = (start + len).min(frame.len());
129            let spdu = frame.get(start + 1..data_end).unwrap_or(&[]);
130            if spdu.is_empty() {
131                format!("{} tcid={} (poll)", tpdu_name(tag), tcid)
132            } else {
133                format!("{} tcid={} · {}", tpdu_name(tag), tcid, spdu_label(spdu))
134            }
135        }
136        _ => format!("{} {:02X?}", tpdu_name(tag), &frame[..frame.len().min(8)]),
137    }
138}
139
140/// Format a whole captured exchange as a multi-line annotated trace.
141#[must_use]
142pub fn decode_log(log: &[LinkEvent]) -> String {
143    let mut out = String::new();
144    for ev in log {
145        let line = match ev {
146            LinkEvent::Tx(f) => format!("W {}", decode_frame(f)),
147            LinkEvent::Rx(f) => format!("R {}", decode_frame(f)),
148            LinkEvent::Reset => "  reset()".to_string(),
149            LinkEvent::SlotInfo(si) => format!("  slot_info() -> ready={}", si.module_ready),
150        };
151        out.push_str(&line);
152        out.push('\n');
153    }
154    out
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn decodes_the_337_handshake_frames() {
163        // Bytes lifted from issue #337's trace.
164        assert_eq!(decode_frame(&[0x82, 0x01, 0x01]), "Create_T_C tcid=1");
165        assert_eq!(decode_frame(&[0x81, 0x01, 0x01]), "T_RCV tcid=1");
166        assert_eq!(decode_frame(&[0x80, 0x02, 0x01, 0x00]), "T_SB tcid=1 DA=0");
167
168        // open_session_request (module): a0 07 01 | 91 04 00 01 00 41 | SB
169        let osr = [
170            0xA0, 0x07, 0x01, 0x91, 0x04, 0x00, 0x01, 0x00, 0x41, 0x80, 0x02, 0x01, 0x00,
171        ];
172        assert_eq!(
173            decode_frame(&osr),
174            "T_Data_Last tcid=1 · open_session_request resource_manager"
175        );
176
177        // profile_enq (host): a0 09 01 | 90 02 00 01 | 9f 80 10 00
178        let enq = [
179            0xA0, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00,
180        ];
181        assert_eq!(
182            decode_frame(&enq),
183            "T_Data_Last tcid=1 · session 1 · profile_enq (9F8010)"
184        );
185    }
186
187    #[test]
188    fn decodes_long_form_length_profile_reply() {
189        // The module's profile_reply, captured raw on hardware (#337): the
190        // length_field is long-form `82 00 09`. A short-form decoder mis-reads
191        // tcid as 0x00 and garbles the SPDU.
192        let f = [
193            0xA0, 0x82, 0x00, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x11, 0x00, 0x80,
194            0x02, 0x01, 0x00,
195        ];
196        assert_eq!(
197            decode_frame(&f),
198            "T_Data_Last tcid=1 · session 1 · profile (9F8011)"
199        );
200    }
201
202    #[test]
203    fn decodes_log_directions() {
204        let log = [
205            LinkEvent::Reset,
206            LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
207            LinkEvent::Rx(vec![0x80, 0x02, 0x01, 0x00]),
208        ];
209        let s = decode_log(&log);
210        assert!(s.contains("  reset()"));
211        assert!(s.contains("W Create_T_C tcid=1"));
212        assert!(s.contains("R T_SB tcid=1 DA=0"));
213    }
214}