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}