Skip to main content

dvb_ci_runtime/
transport.rs

1//! TPDU transport layer — a sans-IO state machine for the single transport
2//! connection per CI slot (ETSI EN 50221 §A.4).
3//!
4//! Connection lifecycle (Figures 6/7): `Idle → Creating → Active`. The host
5//! sends `Create_T_C`; the module answers `C_T_C_Reply` (→ `Active`) or the
6//! host times out back to `Idle`. In `Active` the host **polls regularly** —
7//! per §A.4, a poll is a `T_Data_Last` with an empty data field — and, whenever
8//! a Status Byte reports Data-Available (DA), sends `T_RCV` to receive the
9//! queued message. Chained module messages arrive as `T_Data_More*` then a
10//! final `T_Data_Last` and are reassembled into one SPDU payload.
11//!
12//! Timing: EN 50221 mandates regular polling and a reply-timeout arc but does
13//! not fix the interval, so [`DEFAULT_POLL_INTERVAL`] / [`DEFAULT_REPLY_TIMEOUT`]
14//! are implementation-chosen defaults. All timing is expressed via the sans-IO
15//! [`Tick`](crate::event::Event::Tick)/timer model so it is deterministic and
16//! testable without a clock.
17
18use std::collections::VecDeque;
19use std::time::Duration;
20
21use broadcast_common::{Parse, Serialize};
22use dvb_ci::tpdu::{CommandTpdu, DataBlock, ResponseTpdu, SbValue, TcObject, create_t_c, tags};
23
24/// Length of a standalone/appended `T_SB` object: `tag · 0x02 · t_c_id · SB`.
25const SB_OBJECT_LEN: usize = 4;
26
27/// Parse a `T_SB` object (`0x80 0x02 t_c_id sb_value`) at the start of `bytes`.
28fn parse_sb(bytes: &[u8]) -> Option<(u8, SbValue)> {
29    if bytes.len() >= SB_OBJECT_LEN && bytes[0] == tags::SB && bytes[1] == 0x02 {
30        Some((bytes[2], SbValue(bytes[3])))
31    } else {
32        None
33    }
34}
35
36/// Conventional host poll interval (implementation-chosen; §A.4 mandates only
37/// "poll regularly").
38pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(100);
39/// Conventional reply timeout for an expected `R_TPDU` (the §A.4 Figure 6/7
40/// "Timeout" arc; value implementation-chosen).
41pub const DEFAULT_REPLY_TIMEOUT: Duration = Duration::from_millis(1000);
42
43/// Transport connection state (EN 50221 §A.4, Figures 6/7).
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum TcState {
47    /// No connection; nothing sent.
48    Idle,
49    /// `Create_T_C` sent, awaiting `C_T_C_Reply`.
50    Creating,
51    /// Connection up; polling/exchanging data.
52    Active,
53}
54
55/// What the transport layer wants done after handling an input.
56#[derive(Debug, Default, Clone, PartialEq, Eq)]
57pub struct Out {
58    /// Link-layer TPDU frames to write to the device, in order.
59    pub writes: Vec<Vec<u8>>,
60    /// Fully-reassembled SPDU payloads to pass up to the session layer.
61    pub spdus: Vec<Vec<u8>>,
62    /// Requested delay until the next [`Tick`](crate::event::Event::Tick).
63    pub timer: Option<Duration>,
64    /// A transport error (e.g. reply timeout, unexpected tag).
65    pub error: Option<TransportError>,
66}
67
68/// Transport-layer errors.
69#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
70#[non_exhaustive]
71pub enum TransportError {
72    /// No `C_T_C_Reply` within the reply timeout (§A.4 timeout arc).
73    #[error("transport connection setup timed out")]
74    SetupTimeout,
75    /// A C_TPDU (poll or data block) sent while `Active` went unanswered
76    /// past the reply timeout (§A.4 timeout arc, applied to the `Active`
77    /// state rather than only connection setup).
78    #[error("no reply to an in-flight C_TPDU within the reply timeout")]
79    ReplyTimeout,
80    /// A reply arrived for a different `t_c_id` than ours.
81    #[error("unexpected t_c_id {got} (expected {expected})")]
82    WrongTcId {
83        /// The `t_c_id` received.
84        got: u8,
85        /// Our connection's `t_c_id`.
86        expected: u8,
87    },
88    /// The module reported a `T_C_Error`.
89    #[error("module reported T_C_Error")]
90    ModuleError,
91    /// A frame could not be parsed as an `R_TPDU`.
92    #[error("malformed R_TPDU")]
93    Malformed,
94    /// A chained `T_Data_More*` reassembly exceeded [`MAX_REASSEMBLY_LEN`]
95    /// without a terminating `T_Data_Last` — the partial reassembly is
96    /// dropped rather than retained (a well-behaved module chains a bounded
97    /// number of fragments; this guards a stuck or hostile one from growing
98    /// host memory without limit).
99    #[error("chained TPDU reassembly exceeded {MAX_REASSEMBLY_LEN} bytes without a T_Data_Last")]
100    ReassemblyOverflow,
101    /// [`Transport::send_spdu`] was called with [`MAX_OUTBOUND_QUEUE`] SPDUs
102    /// already queued — the caller is producing faster than the half-duplex
103    /// link can drain (one block per module turn); queueing further would
104    /// grow unboundedly. The new SPDU is dropped; the caller should retry
105    /// once earlier sends have drained.
106    #[error("outbound queue is full ({MAX_OUTBOUND_QUEUE} SPDUs already queued)")]
107    OutboundQueueFull,
108    /// [`Transport::send_spdu`] was called with an SPDU longer than
109    /// [`MAX_SPDU_LEN`] — it cannot be wrapped in a single `T_Data_Last`
110    /// `CommandTpdu` (`length_field`'s own max encodable value). Previously
111    /// this was only discovered when the resulting `CommandTpdu` failed to
112    /// serialize, which `Transport::cmd` reported by panicking.
113    #[error(
114        "SPDU of {len} bytes exceeds the {MAX_SPDU_LEN}-byte maximum a single T_Data_Last can carry"
115    )]
116    SpduTooLarge {
117        /// The SPDU's actual length.
118        len: usize,
119    },
120}
121
122/// Defensive cap on a single chained-fragment reassembly (`Transport::on_data`).
123/// Not a spec limit — EN 50221 places no bound on how many `T_Data_More`
124/// fragments may precede a `T_Data_Last` — but an unbounded accumulator lets
125/// a stuck or hostile module grow host memory without limit; 2 MiB is far
126/// beyond any legitimate CI application object this transport carries
127/// (`dvb-ci`'s own largest length-prefixed objects top out at 65535 bytes).
128pub const MAX_REASSEMBLY_LEN: usize = 2 * 1024 * 1024;
129
130/// Defensive cap on [`Transport::send_spdu`]'s queue depth. The link sends
131/// one queued block per module turn (§A.4 half-duplex, issue #337), so a
132/// caller enqueueing faster than that drains would otherwise grow the queue
133/// without limit.
134pub const MAX_OUTBOUND_QUEUE: usize = 64;
135
136/// Largest SPDU [`Transport::send_spdu`] can wrap in one `T_Data_Last`
137/// `CommandTpdu`: `length_field`'s own max encodable value (65535, EN 50221
138/// Table 1) minus the 1-byte `t_c_id` that shares the length-prefixed span.
139pub const MAX_SPDU_LEN: usize = 65_535 - 1;
140
141/// The single transport connection for a slot.
142#[derive(Debug)]
143pub struct Transport {
144    tcid: u8,
145    state: TcState,
146    reassembly: Vec<u8>,
147    poll_interval: Duration,
148    reply_timeout: Duration,
149    /// Time accumulated since the last poll (drives the poll cadence).
150    since_poll: Duration,
151    /// Time accumulated since a command that expects a reply (drives the
152    /// reply timeout); `None` when not awaiting a reply. While `Some`, a host
153    /// C_TPDU is *in flight* (sent, module not yet answered) — the link is
154    /// half-duplex, so no further data block may be sent until it clears.
155    awaiting: Option<Duration>,
156    /// SPDUs queued to send, one `T_Data_Last` per module turn. EN 50221's link
157    /// is polled half-duplex: the host sends a single data block, then must wait
158    /// for the module's `T_SB` before sending the next. Sending two back-to-back
159    /// makes a real CAM drop the second (issue #337).
160    outbound: VecDeque<Vec<u8>>,
161}
162
163impl Default for Transport {
164    fn default() -> Self {
165        Self::new(1)
166    }
167}
168
169impl Transport {
170    /// New transport for `tcid` with default timings.
171    #[must_use]
172    pub fn new(tcid: u8) -> Self {
173        Self {
174            tcid,
175            state: TcState::Idle,
176            reassembly: Vec::new(),
177            poll_interval: DEFAULT_POLL_INTERVAL,
178            reply_timeout: DEFAULT_REPLY_TIMEOUT,
179            since_poll: Duration::ZERO,
180            awaiting: None,
181            outbound: VecDeque::new(),
182        }
183    }
184
185    /// Override the poll interval / reply timeout.
186    #[must_use]
187    pub fn with_timing(mut self, poll: Duration, reply: Duration) -> Self {
188        self.poll_interval = poll;
189        self.reply_timeout = reply;
190        self
191    }
192
193    /// Current connection state.
194    #[must_use]
195    pub fn state(&self) -> TcState {
196        self.state
197    }
198
199    /// `data` must be at most [`MAX_SPDU_LEN`] bytes — `send_spdu` enforces
200    /// this before ever queueing, and every other caller passes empty data,
201    /// so `serialize_into` failing here would mean that invariant broke,
202    /// not a normal runtime condition (r10-W-20: this used to be able to
203    /// panic on caller-controlled data that reached here unchecked).
204    fn cmd(&self, tag: u8, data: &[u8]) -> Vec<u8> {
205        let c = CommandTpdu {
206            tag,
207            t_c_id: self.tcid,
208            data,
209        };
210        let mut buf = vec![0u8; c.serialized_len()];
211        let n = c
212            .serialize_into(&mut buf)
213            .expect("data.len() <= MAX_SPDU_LEN, enforced by every caller");
214        buf.truncate(n);
215        buf
216    }
217
218    fn poll_frame(&self) -> Vec<u8> {
219        // §A.4: poll == T_Data_Last with empty data.
220        self.cmd(tags::DATA_LAST, &[])
221    }
222
223    /// Open the connection: emit `Create_T_C` and arm the reply timeout.
224    pub fn init(&mut self) -> Out {
225        self.state = TcState::Creating;
226        self.awaiting = Some(Duration::ZERO);
227        let obj: TcObject = create_t_c(self.tcid);
228        Out {
229            writes: vec![obj.to_bytes()],
230            timer: Some(self.reply_timeout),
231            ..Out::default()
232        }
233    }
234
235    /// Queue an upper-layer SPDU to send (wrapped in a `T_Data_Last`). The block
236    /// is transmitted now if the link is free, else held until the in-flight
237    /// C_TPDU is answered — one data block per module turn (§A.4 half-duplex).
238    pub fn send_spdu(&mut self, spdu: &[u8]) -> Out {
239        if self.state != TcState::Active {
240            return Out::default();
241        }
242        // r10-W-20: reject up front rather than queueing an SPDU that would
243        // later fail to serialize (previously discovered by `cmd`
244        // panicking deep inside `flush`/`tick`).
245        if spdu.len() > MAX_SPDU_LEN {
246            return Out {
247                error: Some(TransportError::SpduTooLarge { len: spdu.len() }),
248                ..Out::default()
249            };
250        }
251        // r10-W-17: cap queue depth so a caller producing faster than the
252        // half-duplex link drains cannot grow it without limit.
253        if self.outbound.len() >= MAX_OUTBOUND_QUEUE {
254            return Out {
255                error: Some(TransportError::OutboundQueueFull),
256                ..Out::default()
257            };
258        }
259        self.outbound.push_back(spdu.to_vec());
260        self.flush()
261    }
262
263    /// Emit the next queued data block if the link is free (Active and no
264    /// C_TPDU in flight); otherwise nothing (it waits for the module's `T_SB`).
265    fn flush(&mut self) -> Out {
266        if self.state != TcState::Active || self.awaiting.is_some() {
267            return Out::default();
268        }
269        match self.outbound.pop_front() {
270            Some(spdu) => {
271                self.awaiting = Some(Duration::ZERO);
272                self.since_poll = Duration::ZERO;
273                Out {
274                    writes: vec![self.cmd(tags::DATA_LAST, &spdu)],
275                    timer: Some(self.poll_interval),
276                    ..Out::default()
277                }
278            }
279            None => Out::default(),
280        }
281    }
282
283    /// Advance logical time by `elapsed`: poll if due, or time out a pending
284    /// reply.
285    pub fn tick(&mut self, elapsed: Duration) -> Out {
286        match self.state {
287            TcState::Idle => Out::default(),
288            TcState::Creating => {
289                if let Some(w) = self.awaiting.as_mut() {
290                    *w += elapsed;
291                    if *w >= self.reply_timeout {
292                        self.state = TcState::Idle;
293                        self.awaiting = None;
294                        // r10-W-17: SPDUs queued against the failed
295                        // connection attempt must not linger and get sent
296                        // against a later, unrelated re-established
297                        // connection.
298                        self.outbound.clear();
299                        return Out {
300                            error: Some(TransportError::SetupTimeout),
301                            ..Out::default()
302                        };
303                    }
304                }
305                Out {
306                    timer: Some(self.reply_timeout),
307                    ..Out::default()
308                }
309            }
310            TcState::Active => {
311                // r10-W-16: a C_TPDU (poll or data block) we already sent is
312                // half-duplex — the link cannot carry another until the
313                // module answers it. Track how long it's been outstanding
314                // and time it out, exactly as `Creating` does for
315                // `Create_T_C`; only once it clears (cleared by `on_frame`,
316                // or here on timeout) does the poll cadence below apply.
317                // Previously this branch never consulted `awaiting` at all
318                // once `since_poll` crossed `poll_interval`, so it just kept
319                // sending a fresh poll on top of a still-outstanding one.
320                if let Some(w) = self.awaiting.as_mut() {
321                    *w += elapsed;
322                    if *w >= self.reply_timeout {
323                        self.awaiting = None;
324                        return Out {
325                            error: Some(TransportError::ReplyTimeout),
326                            timer: Some(self.poll_interval),
327                            ..Out::default()
328                        };
329                    }
330                    return Out {
331                        timer: Some(self.reply_timeout - *w),
332                        ..Out::default()
333                    };
334                }
335                self.since_poll += elapsed;
336                if self.since_poll >= self.poll_interval {
337                    self.since_poll = Duration::ZERO;
338                    // A queued data block goes out in preference to an empty poll.
339                    if !self.outbound.is_empty() {
340                        return self.flush();
341                    }
342                    self.awaiting = Some(Duration::ZERO);
343                    Out {
344                        writes: vec![self.poll_frame()],
345                        timer: Some(self.poll_interval),
346                        ..Out::default()
347                    }
348                } else {
349                    Out {
350                        timer: Some(self.poll_interval - self.since_poll),
351                        ..Out::default()
352                    }
353                }
354            }
355        }
356    }
357
358    /// Handle one link-layer frame read from the device.
359    ///
360    /// A module frame is a leading object (`C_T_C_Reply` / `T_Data_*` / …)
361    /// followed by an appended `T_SB`, or a standalone `T_SB` (the reply to a
362    /// poll with nothing queued). The `T_SB`'s DA bit drives whether the host
363    /// must `T_RCV` next.
364    pub fn on_frame(&mut self, frame: &[u8]) -> Out {
365        self.awaiting = None;
366        match frame.first().copied() {
367            // C_T_C_Reply (+ appended T_SB): connection becomes Active.
368            Some(tags::C_T_C_REPLY) => match TcObject::parse(frame) {
369                Ok(o) if o.t_c_id == self.tcid => {
370                    self.state = TcState::Active;
371                    self.since_poll = Duration::ZERO;
372                    let da = parse_sb(&frame[3..]).is_some_and(|(_, sb)| sb.data_available());
373                    self.after_status(da)
374                }
375                Ok(o) => self.wrong_tcid(o.t_c_id),
376                Err(_) => self.malformed(),
377            },
378            // Standalone T_SB — the reply to a poll.
379            Some(tags::SB) => match parse_sb(frame) {
380                Some((tcid, _)) if tcid != self.tcid => self.wrong_tcid(tcid),
381                Some((_, sb)) => self.after_status(sb.data_available()),
382                None => self.malformed(),
383            },
384            Some(tags::T_C_ERROR) => Out {
385                error: Some(TransportError::ModuleError),
386                ..Out::default()
387            },
388            Some(tags::DATA_LAST | tags::DATA_MORE) => self.on_data(frame),
389            _ => self.malformed(),
390        }
391    }
392
393    /// r10-W-17: also clears any in-progress chained-fragment reassembly —
394    /// a malformed frame invalidates it; leaving it in place would silently
395    /// prepend stale bytes to the NEXT chain's data.
396    fn malformed(&mut self) -> Out {
397        self.reassembly.clear();
398        Out {
399            error: Some(TransportError::Malformed),
400            ..Out::default()
401        }
402    }
403
404    /// r10-W-17: also clears reassembly, for the same reason as [`Self::malformed`].
405    fn wrong_tcid(&mut self, got: u8) -> Out {
406        self.reassembly.clear();
407        Out {
408            error: Some(TransportError::WrongTcId {
409                got,
410                expected: self.tcid,
411            }),
412            ..Out::default()
413        }
414    }
415
416    /// React to a Status Byte: if DA, solicit the queued message with `T_RCV`;
417    /// otherwise resume the idle poll cadence.
418    fn after_status(&mut self, data_available: bool) -> Out {
419        if data_available {
420            self.awaiting = Some(Duration::ZERO);
421            Out {
422                writes: vec![self.cmd(tags::RCV, &[])],
423                ..Out::default()
424            }
425        } else {
426            // Module idle: its `T_SB` freed the link, so send the next queued
427            // data block if any (the #337 fix); otherwise resume polling.
428            if !self.outbound.is_empty() {
429                return self.flush();
430            }
431            self.since_poll = Duration::ZERO;
432            Out {
433                timer: Some(self.poll_interval),
434                ..Out::default()
435            }
436        }
437    }
438
439    fn on_data(&mut self, frame: &[u8]) -> Out {
440        let r = match ResponseTpdu::parse(frame) {
441            Ok(r) => r,
442            Err(_) => return self.malformed(),
443        };
444        if r.t_c_id != self.tcid {
445            return self.wrong_tcid(r.t_c_id);
446        }
447        self.reassembly.extend_from_slice(r.data);
448        if self.reassembly.len() > MAX_REASSEMBLY_LEN {
449            // r10-W-17: EN 50221 places no bound on the number of chained
450            // `T_Data_More` fragments — without this cap a stuck or hostile
451            // module could grow host memory without limit.
452            self.reassembly.clear();
453            return Out {
454                error: Some(TransportError::ReassemblyOverflow),
455                ..Out::default()
456            };
457        }
458        match r.block {
459            // More chained fragments: each waits for another T_RCV (§A.4 item 10).
460            Some(DataBlock::More) => {
461                self.awaiting = Some(Duration::ZERO);
462                Out {
463                    writes: vec![self.cmd(tags::RCV, &[])],
464                    ..Out::default()
465                }
466            }
467            // Last (or only) fragment: emit the reassembled SPDU, then let the
468            // appended Status Byte decide whether to receive another message.
469            _ => {
470                let mut out = self.after_status(r.sb_value.data_available());
471                if !self.reassembly.is_empty() {
472                    out.spdus.push(core::mem::take(&mut self.reassembly));
473                }
474                out
475            }
476        }
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use dvb_ci::tpdu::SbValue;
484
485    /// Build an R_TPDU frame (module→host) for tests.
486    fn r_tpdu(tag: u8, tcid: u8, data: &[u8], da: bool) -> Vec<u8> {
487        // tag, length_field(=1+data), tcid, data..., SB(0x80), len=2, tcid, sb_value
488        let mut v = vec![tag];
489        v.push((1 + data.len()) as u8);
490        v.push(tcid);
491        v.extend_from_slice(data);
492        v.extend_from_slice(&[tags::SB, 0x02, tcid, SbValue::new(da).0]);
493        v
494    }
495
496    #[test]
497    fn init_sends_create_tc_and_arms_timeout() {
498        let mut t = Transport::new(1);
499        let out = t.init();
500        assert_eq!(out.writes, vec![vec![tags::CREATE_T_C, 0x01, 0x01]]);
501        assert_eq!(t.state(), TcState::Creating);
502        assert_eq!(out.timer, Some(DEFAULT_REPLY_TIMEOUT));
503    }
504
505    #[test]
506    fn setup_times_out_to_idle() {
507        let mut t = Transport::new(1);
508        t.init();
509        let out = t.tick(DEFAULT_REPLY_TIMEOUT);
510        assert_eq!(out.error, Some(TransportError::SetupTimeout));
511        assert_eq!(t.state(), TcState::Idle);
512    }
513
514    #[test]
515    fn reply_activates_then_polls_on_interval() {
516        let mut t = Transport::new(1);
517        t.init();
518        let out = t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
519        assert_eq!(t.state(), TcState::Active);
520        assert!(out.error.is_none());
521        // Before the interval: no poll.
522        let early = t.tick(DEFAULT_POLL_INTERVAL / 2);
523        assert!(early.writes.is_empty());
524        // Crossing the interval: an empty T_Data_Last poll.
525        let due = t.tick(DEFAULT_POLL_INTERVAL);
526        assert_eq!(due.writes, vec![vec![tags::DATA_LAST, 0x01, 0x01]]);
527    }
528
529    #[test]
530    fn reassembles_more_then_last_into_one_spdu() {
531        let mut t = Transport::new(1);
532        t.init();
533        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
534        // MORE: partial data, solicits RCV
535        let o1 = t.on_frame(&r_tpdu(tags::DATA_MORE, 1, &[0xAA, 0xBB], false));
536        assert!(o1.spdus.is_empty());
537        assert_eq!(o1.writes, vec![vec![tags::RCV, 0x01, 0x01]]);
538        // LAST: completes the SPDU
539        let o2 = t.on_frame(&r_tpdu(tags::DATA_LAST, 1, &[0xCC], false));
540        assert_eq!(o2.spdus, vec![vec![0xAA, 0xBB, 0xCC]]);
541    }
542
543    #[test]
544    fn data_available_triggers_rcv() {
545        let mut t = Transport::new(1);
546        t.init();
547        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
548        // LAST with DA set → host must RCV the next queued message.
549        let o = t.on_frame(&r_tpdu(tags::DATA_LAST, 1, &[0x01], true));
550        assert_eq!(o.spdus, vec![vec![0x01]]);
551        assert_eq!(o.writes, vec![vec![tags::RCV, 0x01, 0x01]]);
552    }
553
554    #[test]
555    fn two_sends_serialize_one_block_per_module_turn() {
556        // #337: a real CAM drops a second T_Data_Last sent before it answers the
557        // first. Two send_spdu in one turn must emit only ONE write; the second
558        // goes out after the module's T_SB.
559        let mut t = Transport::new(1);
560        t.init();
561        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
562
563        let first = t.send_spdu(&[0x92, 0x07]); // e.g. open_session_response
564        assert_eq!(first.writes.len(), 1);
565        assert_eq!(first.writes[0][0], tags::DATA_LAST);
566
567        // Queued while the first is in flight → no write yet.
568        let second = t.send_spdu(&[0x9F, 0x80, 0x10, 0x00]); // profile_enq
569        assert!(
570            second.writes.is_empty(),
571            "second block must wait for the SB"
572        );
573
574        // Module acknowledges with a standalone T_SB (data_available = 0).
575        let after_sb = t.on_frame(&[tags::SB, 0x02, 0x01, SbValue::new(false).0]);
576        assert_eq!(
577            after_sb.writes.len(),
578            1,
579            "second block flushes after the SB"
580        );
581        assert_eq!(after_sb.writes[0][0], tags::DATA_LAST);
582        // It carries the profile_enq payload.
583        assert!(
584            after_sb.writes[0]
585                .windows(4)
586                .any(|w| w == [0x9F, 0x80, 0x10, 0x00])
587        );
588    }
589
590    /// r10-W-16: while a C_TPDU sent in `Active` is still awaiting its
591    /// reply, `tick` must not send another one (the link is half-duplex) —
592    /// pre-fix, once `since_poll` crossed `poll_interval` again, `tick` sent
593    /// a fresh poll regardless of `awaiting`.
594    #[test]
595    fn active_tick_does_not_poll_while_a_c_tpdu_is_in_flight() {
596        let mut t = Transport::new(1);
597        t.init();
598        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
599        // Send a data block: awaiting becomes Some, no reply yet.
600        let sent = t.send_spdu(&[0xAA]);
601        assert_eq!(sent.writes.len(), 1, "the data block itself goes out");
602
603        // Advance well past one poll_interval, twice over, with no reply.
604        let tick1 = t.tick(DEFAULT_POLL_INTERVAL);
605        assert!(
606            tick1.writes.is_empty(),
607            "must not poll while a C_TPDU is in flight, got {:?}",
608            tick1.writes
609        );
610        let tick2 = t.tick(DEFAULT_POLL_INTERVAL);
611        assert!(
612            tick2.writes.is_empty(),
613            "must not poll while a C_TPDU is in flight, got {:?}",
614            tick2.writes
615        );
616    }
617
618    /// r10-W-16: an in-flight C_TPDU that the module never answers must
619    /// eventually time out (there was previously no timeout at all for the
620    /// `Active` state's `awaiting` — only `Creating`'s `Create_T_C` had
621    /// one).
622    #[test]
623    fn active_reply_timeout_surfaces_error_and_clears_awaiting() {
624        let mut t = Transport::new(1);
625        t.init();
626        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
627        t.send_spdu(&[0xAA]);
628
629        let out = t.tick(DEFAULT_REPLY_TIMEOUT);
630        assert_eq!(out.error, Some(TransportError::ReplyTimeout));
631        assert_eq!(t.state(), TcState::Active, "stays Active, just resets");
632
633        // Awaiting cleared: a later poll can now go out again.
634        let due = t.tick(DEFAULT_POLL_INTERVAL);
635        assert_eq!(due.writes, vec![vec![tags::DATA_LAST, 0x01, 0x01]]);
636    }
637
638    #[test]
639    fn wrong_tcid_is_flagged() {
640        let mut t = Transport::new(1);
641        t.init();
642        let o = t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x09]);
643        assert_eq!(
644            o.error,
645            Some(TransportError::WrongTcId {
646                got: 9,
647                expected: 1
648            })
649        );
650    }
651
652    /// r10-W-20: an over-long SPDU must be rejected with `SpduTooLarge`
653    /// instead of `send_spdu` propagating it through `flush` into `cmd`,
654    /// where it used to reach a `.expect()` and panic.
655    #[test]
656    fn send_spdu_rejects_an_oversized_spdu_instead_of_panicking() {
657        let mut t = Transport::new(1);
658        t.init();
659        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
660
661        let oversized = vec![0u8; MAX_SPDU_LEN + 1];
662        let out = t.send_spdu(&oversized);
663        assert_eq!(
664            out.error,
665            Some(TransportError::SpduTooLarge {
666                len: oversized.len()
667            })
668        );
669        assert!(out.writes.is_empty());
670        assert!(
671            t.outbound.is_empty(),
672            "the oversized SPDU must not be queued either"
673        );
674    }
675
676    /// r10-W-17: a malformed frame arriving mid-chain must clear whatever
677    /// partial reassembly was in progress, so it does not silently
678    /// contaminate the NEXT chain's data with stale bytes.
679    #[test]
680    fn reassembly_is_cleared_on_a_malformed_frame_mid_chain() {
681        let mut t = Transport::new(1);
682        t.init();
683        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
684
685        // Start a chain: one MORE fragment with stale bytes.
686        let o1 = t.on_frame(&r_tpdu(tags::DATA_MORE, 1, &[0xDE, 0xAD], false));
687        assert_eq!(o1.writes, vec![vec![tags::RCV, 0x01, 0x01]]);
688        assert_eq!(t.reassembly, vec![0xDE, 0xAD]);
689
690        // A malformed frame interrupts the chain.
691        let o2 = t.on_frame(&[0xFF]);
692        assert_eq!(o2.error, Some(TransportError::Malformed));
693        assert!(
694            t.reassembly.is_empty(),
695            "stale bytes must not survive a malformed frame"
696        );
697
698        // A fresh, unrelated chain must reassemble cleanly, with no leftover
699        // 0xDE 0xAD prefix from before.
700        let o3 = t.on_frame(&r_tpdu(tags::DATA_LAST, 1, &[0x01, 0x02], false));
701        assert_eq!(o3.spdus, vec![vec![0x01, 0x02]]);
702    }
703
704    /// r10-W-17: chained `T_Data_More` reassembly must not grow without
705    /// bound — EN 50221 places no cap on fragment count, so a stuck or
706    /// hostile module could otherwise exhaust host memory.
707    #[test]
708    fn reassembly_overflow_is_rejected_and_cleared() {
709        let mut t = Transport::new(1);
710        t.init();
711        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
712
713        // Feed MORE fragments large enough to cross MAX_REASSEMBLY_LEN.
714        // Each R_TPDU's `data` field is capped at 65534 bytes (length_field
715        // max 65535 minus the 1-byte t_c_id), so a handful of maximal
716        // fragments comfortably crosses the 2 MiB cap. Built via
717        // `ResponseTpdu`'s own `Serialize` (not the small-payload `r_tpdu`
718        // helper, whose length byte can't represent more than 127 bytes).
719        let chunk = vec![0xAAu8; 65_534];
720        let frame = ResponseTpdu {
721            tag: tags::DATA_MORE,
722            t_c_id: 1,
723            data: &chunk,
724            sb_value: SbValue::new(false),
725            block: Some(DataBlock::More),
726        }
727        .to_bytes();
728        let mut overflowed = false;
729        for _ in 0..40 {
730            let out = t.on_frame(&frame);
731            if let Some(err) = out.error {
732                assert_eq!(err, TransportError::ReassemblyOverflow);
733                overflowed = true;
734                break;
735            }
736        }
737        assert!(overflowed, "40 * 65534 bytes must cross MAX_REASSEMBLY_LEN");
738        assert!(
739            t.reassembly.is_empty(),
740            "overflowed reassembly must be cleared, not retained"
741        );
742    }
743
744    /// r10-W-17: `send_spdu` must not grow the outbound queue without
745    /// bound when the caller produces faster than the half-duplex link
746    /// drains.
747    #[test]
748    fn send_spdu_rejects_once_the_outbound_queue_is_full() {
749        let mut t = Transport::new(1);
750        t.init();
751        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
752
753        // The first send goes out immediately and leaves `awaiting` set, so
754        // every subsequent send just queues (nothing drains it here).
755        t.send_spdu(&[0x00]);
756        for _ in 0..MAX_OUTBOUND_QUEUE {
757            let out = t.send_spdu(&[0x00]);
758            assert!(out.error.is_none());
759        }
760        assert_eq!(t.outbound.len(), MAX_OUTBOUND_QUEUE);
761
762        let full = t.send_spdu(&[0x00]);
763        assert_eq!(full.error, Some(TransportError::OutboundQueueFull));
764        assert_eq!(
765            t.outbound.len(),
766            MAX_OUTBOUND_QUEUE,
767            "the rejected SPDU must not be queued"
768        );
769    }
770
771    /// r10-W-17: SPDUs queued before a connection attempt fails (setup
772    /// timeout) must not linger and get sent against a later, unrelated
773    /// re-established connection.
774    #[test]
775    fn outbound_queue_is_cleared_on_setup_timeout() {
776        let mut t = Transport::new(1);
777        t.init();
778        t.on_frame(&[tags::C_T_C_REPLY, 0x01, 0x01]);
779        t.send_spdu(&[0xAA]); // in flight
780        t.send_spdu(&[0xBB]); // queued behind it
781        assert_eq!(t.outbound.len(), 1);
782
783        // Simulate an external re-init (e.g. the session layer resetting
784        // the connection) while the old queue is still populated, then let
785        // the new attempt time out.
786        t.init();
787        assert_eq!(t.state(), TcState::Creating);
788        let out = t.tick(DEFAULT_REPLY_TIMEOUT);
789        assert_eq!(out.error, Some(TransportError::SetupTimeout));
790        assert!(
791            t.outbound.is_empty(),
792            "stale queued SPDUs must not survive into the next connection"
793        );
794    }
795}