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