Skip to main content

dvb_ci/ci_plus/
low_speed_comms_v4.rs

1//! Low Speed Communication resource version 4 extensions — ETSI TS 103 205
2//! V1.4.1 §10, Tables 76-89 (PDF pp. 100-110). See
3//! `docs/ts_103_205/low-speed-comms-v4.md`.
4//!
5//! LSC v4 extends LSC v3 (CI Plus V1.3 \[3\] §14.1) to add source-specific
6//! multicast, hybrid connections (response data across the TS interface), a new
7//! `comms_info()` APDU, a new `comms_IP_config()` APDU, and a `source_port` in the
8//! connection_descriptor. The new APDU tags live in the CI Plus `0x9F8Cxx`
9//! namespace.
10//!
11//! - `comms_info_req` (`0x9F8C07`, Table 76) — CICAM → Host, header-only.
12//! - `comms_info_reply` (`0x9F8C08`, Table 77) — Host → CICAM.
13//! - `comms_IP_config_req` (`0x9F8C09`, Table 78) — CICAM → Host, header-only.
14//! - `comms_IP_config_reply` (`0x9F8C0A`, Table 79) — Host → CICAM.
15//! - the Comms Cmd `hybrid_descriptor` (Table 83) and `multicast_descriptor`
16//!   (Table 85) descriptor bodies.
17//!
18//! ## Not wired into `CiPlusApdu` resource dispatch
19//!
20//! TS 103 205 §10 prints **no** LSC v4 resource-summary table with a single
21//! `resource_identifier`. The LSC resource_id and the base `comms_cmd` /
22//! `comms_reply` / `comms_send` / `comms_rcv` APDUs are defined in CI Plus V1.3
23//! \[3\] §14.1 (proprietary, not reproduced) and are **deferred**. These v4
24//! extension APDUs are therefore provided as standalone, directly-constructible /
25//! parseable typed structs with a [`LscV4Apdu::parse`] tag-dispatch helper — they
26//! are **not** wired into [`crate::ci_plus::CiPlusApdu`] (no invented resource_id),
27//! mirroring [`crate::ci_plus::ca_support`].
28//!
29//! ## Table 80 reserved-range typo
30//!
31//! `connection_state` is a **2-bit** field; Table 80 prints the Reserved range as
32//! `0x10-0x11`, an evident typo. We treat it strictly as 2 bits — values 0..3 —
33//! with `0x00` = Disconnected, `0x01` = Connected, and `0x02`/`0x03` Reserved. The
34//! literal `0x10-0x11` range is not encoded.
35
36use crate::error::{Error, Result};
37use crate::objects;
38use crate::tag::ApduTag;
39use alloc::vec::Vec;
40use broadcast_common::{Parse, Serialize};
41
42/// New LSC v4 `apdu_tag`s (§10), in the `0x9F8Cxx` namespace.
43pub mod tag {
44    use crate::tag::ApduTag;
45    /// `comms_info_req_tag` = `0x9F8C07` (Table 76).
46    pub const COMMS_INFO_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x8C, 0x07);
47    /// `comms_info_reply_tag` = `0x9F8C08` (Table 77).
48    pub const COMMS_INFO_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x8C, 0x08);
49    /// `comms_IP_config_req_tag` = `0x9F8C09` (Table 78).
50    pub const COMMS_IP_CONFIG_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x8C, 0x09);
51    /// `comms_IP_config_reply_tag` = `0x9F8C0A` (Table 79).
52    pub const COMMS_IP_CONFIG_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x8C, 0x0A);
53}
54
55/// A 128-bit IPv6-format address (IPv4 prefixed `::ffff:0:0/96` or `::0:0/96`).
56pub const IP_ADDR_LEN: usize = 16;
57/// A 48-bit MAC `physical_address`.
58pub const MAC_LEN: usize = 6;
59
60// --- connection_state (Table 80) ---
61
62/// `connection_state` values (Table 80). The field is 2 bits; see the module doc
63/// for the `0x10-0x11` reserved-range typo note.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66#[non_exhaustive]
67pub enum ConnectionState {
68    /// `0x00` — Disconnected (interface inactive or disconnected).
69    Disconnected,
70    /// `0x01` — Connected (interface active with a valid IP address).
71    Connected,
72    /// Reserved 2-bit value (`0x02`–`0x03`).
73    Reserved(u8),
74}
75impl ConnectionState {
76    /// Decode a 2-bit `connection_state` value (low 2 bits of `v`).
77    #[must_use]
78    pub fn from_u8(v: u8) -> Self {
79        match v & 0x03 {
80            0x00 => Self::Disconnected,
81            0x01 => Self::Connected,
82            other => Self::Reserved(other),
83        }
84    }
85    /// The 2-bit wire value.
86    #[must_use]
87    pub const fn to_u8(self) -> u8 {
88        match self {
89            Self::Disconnected => 0x00,
90            Self::Connected => 0x01,
91            Self::Reserved(v) => v & 0x03,
92        }
93    }
94    /// Spec token, or `"reserved"`.
95    #[must_use]
96    pub fn name(&self) -> &'static str {
97        match self {
98            Self::Disconnected => "disconnected",
99            Self::Connected => "connected",
100            Self::Reserved(_) => "reserved",
101        }
102    }
103}
104broadcast_common::impl_spec_display!(ConnectionState, Reserved);
105
106// --- IP_protocol_version (Table 86) ---
107
108/// `IP_protocol_version` values (Table 86), used in [`MulticastDescriptor`].
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize))]
111#[non_exhaustive]
112pub enum IpProtocolVersion {
113    /// `0x01` — IPv4.
114    Ipv4,
115    /// `0x02` — IPv6.
116    Ipv6,
117    /// Reserved (`0x00`, `0x03`–`0xFF`).
118    Reserved(u8),
119}
120impl IpProtocolVersion {
121    /// Decode an `IP_protocol_version` byte.
122    #[must_use]
123    pub fn from_u8(v: u8) -> Self {
124        match v {
125            0x01 => Self::Ipv4,
126            0x02 => Self::Ipv6,
127            other => Self::Reserved(other),
128        }
129    }
130    /// Wire byte.
131    #[must_use]
132    pub const fn to_u8(self) -> u8 {
133        match self {
134            Self::Ipv4 => 0x01,
135            Self::Ipv6 => 0x02,
136            Self::Reserved(v) => v,
137        }
138    }
139    /// Spec token, or `"reserved"`.
140    #[must_use]
141    pub fn name(&self) -> &'static str {
142        match self {
143            Self::Ipv4 => "ipv4",
144            Self::Ipv6 => "ipv6",
145            Self::Reserved(_) => "reserved",
146        }
147    }
148}
149broadcast_common::impl_spec_display!(IpProtocolVersion, Reserved);
150
151// ---------------------------------------------------------------------------
152// comms_info_req (Table 76)
153// ---------------------------------------------------------------------------
154
155/// `comms_info_req()` (Table 76): CICAM → Host. Header-only.
156///
157/// NB: Table 76 prints `length_field() = 1`, but the APDU carries no payload
158/// fields, so the on-wire body length is 0.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize))]
161pub struct CommsInfoReq;
162
163impl<'a> Parse<'a> for CommsInfoReq {
164    type Error = Error;
165    fn parse(bytes: &'a [u8]) -> Result<Self> {
166        objects::parse_empty_apdu(bytes, tag::COMMS_INFO_REQ, "comms_info_req")?;
167        Ok(Self)
168    }
169}
170impl Serialize for CommsInfoReq {
171    type Error = Error;
172    fn serialized_len(&self) -> usize {
173        objects::empty_apdu_len()
174    }
175    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
176        objects::serialize_empty_apdu(tag::COMMS_INFO_REQ, buf)
177    }
178}
179
180// ---------------------------------------------------------------------------
181// comms_info_reply (Table 77)
182// ---------------------------------------------------------------------------
183
184/// `comms_info_reply()` (Table 77): Host → CICAM.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187pub struct CommsInfoReply {
188    /// `LTS_id` (8) — identifier of the Local TS.
189    pub lts_id: u8,
190    /// `status` (1) — `true` = a connection has been established.
191    pub status: bool,
192    /// `source_IPaddress` (128) — IPv6-format source address; all-zero if unknown.
193    pub source_ip_address: [u8; IP_ADDR_LEN],
194    /// `source_port` (16) — source port; `0x0000` if not aware.
195    pub source_port: u16,
196    /// `inputDeliveryPID` (13) — TS-interface delivery PID for hybrid connections
197    /// (`0x0020`–`0x1FFE`); `0x0000` if not a hybrid connection.
198    pub input_delivery_pid: u16,
199}
200
201// LTS_id(1) + reserved/status(1) + source_IPaddress(16) + source_port(2) +
202// reserved/inputDeliveryPID(2) = 22.
203const INFO_REPLY_BODY: usize = 1 + 1 + IP_ADDR_LEN + 2 + 2;
204const STATUS_BIT: u8 = 0x01;
205const INPUT_DELIVERY_PID_MASK: u16 = 0x1FFF;
206
207impl<'a> Parse<'a> for CommsInfoReply {
208    type Error = Error;
209    fn parse(bytes: &'a [u8]) -> Result<Self> {
210        let body = objects::parse_apdu_header(bytes, tag::COMMS_INFO_REPLY, "comms_info_reply")?;
211        if body.len() < INFO_REPLY_BODY {
212            return Err(Error::BufferTooShort {
213                need: INFO_REPLY_BODY,
214                have: body.len(),
215                what: "comms_info_reply",
216            });
217        }
218        let lts_id = body[0];
219        let status = body[1] & STATUS_BIT != 0;
220        let mut source_ip_address = [0u8; IP_ADDR_LEN];
221        source_ip_address.copy_from_slice(&body[2..2 + IP_ADDR_LEN]);
222        let p = 2 + IP_ADDR_LEN;
223        let source_port = u16::from_be_bytes([body[p], body[p + 1]]);
224        let input_delivery_pid =
225            u16::from_be_bytes([body[p + 2], body[p + 3]]) & INPUT_DELIVERY_PID_MASK;
226        Ok(Self {
227            lts_id,
228            status,
229            source_ip_address,
230            source_port,
231            input_delivery_pid,
232        })
233    }
234}
235impl Serialize for CommsInfoReply {
236    type Error = Error;
237    fn serialized_len(&self) -> usize {
238        objects::apdu_len(INFO_REPLY_BODY)
239    }
240    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
241        let pos = objects::write_apdu_header(tag::COMMS_INFO_REPLY, INFO_REPLY_BODY, buf)?;
242        buf[pos] = self.lts_id;
243        // reserved(7)='0000000' + status(1).
244        buf[pos + 1] = u8::from(self.status);
245        buf[pos + 2..pos + 2 + IP_ADDR_LEN].copy_from_slice(&self.source_ip_address);
246        let p = pos + 2 + IP_ADDR_LEN;
247        buf[p..p + 2].copy_from_slice(&self.source_port.to_be_bytes());
248        // reserved(3)='000' + inputDeliveryPID(13).
249        buf[p + 2..p + 4]
250            .copy_from_slice(&(self.input_delivery_pid & INPUT_DELIVERY_PID_MASK).to_be_bytes());
251        Ok(pos + INFO_REPLY_BODY)
252    }
253}
254
255// ---------------------------------------------------------------------------
256// comms_IP_config_req (Table 78)
257// ---------------------------------------------------------------------------
258
259/// `comms_IP_config_req()` (Table 78): CICAM → Host. Header-only.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize))]
262pub struct CommsIpConfigReq;
263
264impl<'a> Parse<'a> for CommsIpConfigReq {
265    type Error = Error;
266    fn parse(bytes: &'a [u8]) -> Result<Self> {
267        objects::parse_empty_apdu(bytes, tag::COMMS_IP_CONFIG_REQ, "comms_IP_config_req")?;
268        Ok(Self)
269    }
270}
271impl Serialize for CommsIpConfigReq {
272    type Error = Error;
273    fn serialized_len(&self) -> usize {
274        objects::empty_apdu_len()
275    }
276    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
277        objects::serialize_empty_apdu(tag::COMMS_IP_CONFIG_REQ, buf)
278    }
279}
280
281// ---------------------------------------------------------------------------
282// comms_IP_config_reply (Table 79)
283// ---------------------------------------------------------------------------
284
285/// The connected-state IP configuration carried when `connection_state == 0x01`
286/// (Table 79).
287#[derive(Debug, Clone, PartialEq, Eq)]
288#[cfg_attr(feature = "serde", derive(serde::Serialize))]
289pub struct IpConfig {
290    /// `IP_address` (128) — IPv6 format.
291    pub ip_address: [u8; IP_ADDR_LEN],
292    /// `network_mask` (128).
293    pub network_mask: [u8; IP_ADDR_LEN],
294    /// `default_gateway` (128).
295    pub default_gateway: [u8; IP_ADDR_LEN],
296    /// `DHCP_server_address` (128) — all-zero if no DHCP server.
297    pub dhcp_server_address: [u8; IP_ADDR_LEN],
298    /// `DNS_server_address` list (loop count `num_DNS_servers`).
299    pub dns_server_addresses: Vec<[u8; IP_ADDR_LEN]>,
300}
301
302/// `comms_IP_config_reply()` (Table 79): Host → CICAM.
303#[derive(Debug, Clone, PartialEq, Eq)]
304#[cfg_attr(feature = "serde", derive(serde::Serialize))]
305pub struct CommsIpConfigReply {
306    /// `connection_state` (2) — Table 80.
307    pub connection_state: ConnectionState,
308    /// `physical_address` (48) — MAC of this IP network adapter.
309    pub physical_address: [u8; MAC_LEN],
310    /// IP configuration, present iff `connection_state == Connected` (`0x01`).
311    pub ip_config: Option<IpConfig>,
312}
313
314// connection_state/reserved(1) + physical_address(6).
315const IP_CONFIG_PREFIX: usize = 1 + MAC_LEN;
316// IP_address+network_mask+default_gateway+DHCP_server_address(4*16) + num_DNS_servers(1).
317const IP_CONFIG_FIXED: usize = 4 * IP_ADDR_LEN + 1;
318const CONNECTION_STATE_CONNECTED: u8 = 0x01;
319
320impl CommsIpConfigReply {
321    fn body_len(&self) -> usize {
322        IP_CONFIG_PREFIX
323            + match &self.ip_config {
324                Some(c) => IP_CONFIG_FIXED + c.dns_server_addresses.len() * IP_ADDR_LEN,
325                None => 0,
326            }
327    }
328}
329
330fn read_addr(body: &[u8], pos: usize) -> [u8; IP_ADDR_LEN] {
331    let mut a = [0u8; IP_ADDR_LEN];
332    a.copy_from_slice(&body[pos..pos + IP_ADDR_LEN]);
333    a
334}
335
336impl<'a> Parse<'a> for CommsIpConfigReply {
337    type Error = Error;
338    fn parse(bytes: &'a [u8]) -> Result<Self> {
339        let body =
340            objects::parse_apdu_header(bytes, tag::COMMS_IP_CONFIG_REPLY, "comms_IP_config_reply")?;
341        if body.len() < IP_CONFIG_PREFIX {
342            return Err(Error::BufferTooShort {
343                need: IP_CONFIG_PREFIX,
344                have: body.len(),
345                what: "comms_IP_config_reply",
346            });
347        }
348        // connection_state(2) + reserved(6); take the high 2 bits.
349        let connection_state = ConnectionState::from_u8(body[0] >> 6);
350        let mut physical_address = [0u8; MAC_LEN];
351        physical_address.copy_from_slice(&body[1..1 + MAC_LEN]);
352        let ip_config = if connection_state.to_u8() == CONNECTION_STATE_CONNECTED {
353            if body.len() < IP_CONFIG_PREFIX + IP_CONFIG_FIXED {
354                return Err(Error::BufferTooShort {
355                    need: IP_CONFIG_PREFIX + IP_CONFIG_FIXED,
356                    have: body.len(),
357                    what: "comms_IP_config_reply ip_config",
358                });
359            }
360            let mut p = IP_CONFIG_PREFIX;
361            let ip_address = read_addr(body, p);
362            p += IP_ADDR_LEN;
363            let network_mask = read_addr(body, p);
364            p += IP_ADDR_LEN;
365            let default_gateway = read_addr(body, p);
366            p += IP_ADDR_LEN;
367            let dhcp_server_address = read_addr(body, p);
368            p += IP_ADDR_LEN;
369            let n = body[p] as usize;
370            p += 1;
371            if body.len() < p + n * IP_ADDR_LEN {
372                return Err(Error::BufferTooShort {
373                    need: p + n * IP_ADDR_LEN,
374                    have: body.len(),
375                    what: "comms_IP_config_reply dns_servers",
376                });
377            }
378            let mut dns_server_addresses = Vec::with_capacity(n);
379            for _ in 0..n {
380                dns_server_addresses.push(read_addr(body, p));
381                p += IP_ADDR_LEN;
382            }
383            Some(IpConfig {
384                ip_address,
385                network_mask,
386                default_gateway,
387                dhcp_server_address,
388                dns_server_addresses,
389            })
390        } else {
391            None
392        };
393        Ok(Self {
394            connection_state,
395            physical_address,
396            ip_config,
397        })
398    }
399}
400impl Serialize for CommsIpConfigReply {
401    type Error = Error;
402    fn serialized_len(&self) -> usize {
403        objects::apdu_len(self.body_len())
404    }
405    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
406        let body_len = self.body_len();
407        let mut pos = objects::write_apdu_header(tag::COMMS_IP_CONFIG_REPLY, body_len, buf)?;
408        // connection_state(2) << 6 + reserved(6)='000000'.
409        buf[pos] = self.connection_state.to_u8() << 6;
410        buf[pos + 1..pos + 1 + MAC_LEN].copy_from_slice(&self.physical_address);
411        pos += IP_CONFIG_PREFIX;
412        if let Some(c) = &self.ip_config {
413            buf[pos..pos + IP_ADDR_LEN].copy_from_slice(&c.ip_address);
414            pos += IP_ADDR_LEN;
415            buf[pos..pos + IP_ADDR_LEN].copy_from_slice(&c.network_mask);
416            pos += IP_ADDR_LEN;
417            buf[pos..pos + IP_ADDR_LEN].copy_from_slice(&c.default_gateway);
418            pos += IP_ADDR_LEN;
419            buf[pos..pos + IP_ADDR_LEN].copy_from_slice(&c.dhcp_server_address);
420            pos += IP_ADDR_LEN;
421            buf[pos] = c.dns_server_addresses.len() as u8;
422            pos += 1;
423            for a in &c.dns_server_addresses {
424                buf[pos..pos + IP_ADDR_LEN].copy_from_slice(a);
425                pos += IP_ADDR_LEN;
426            }
427        }
428        Ok(pos)
429    }
430}
431
432// ---------------------------------------------------------------------------
433// Comms Cmd hybrid_descriptor (Table 83) — descriptor_tag 0x05
434// ---------------------------------------------------------------------------
435
436/// `descriptor_tag` of the hybrid_descriptor (Table 83).
437pub const HYBRID_DESCRIPTOR_TAG: u8 = 0x05;
438/// `descriptor_tag` of the multicast_descriptor (Table 85).
439pub const MULTICAST_DESCRIPTOR_TAG: u8 = 0x06;
440
441/// `IP_connection_type` values (Table 84), selecting the hybrid_descriptor's
442/// inner connection descriptor.
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444#[cfg_attr(feature = "serde", derive(serde::Serialize))]
445#[non_exhaustive]
446pub enum IpConnectionType {
447    /// `0x03` — IP_descriptor.
448    IpDescriptor,
449    /// `0x04` — hostname_descriptor.
450    HostnameDescriptor,
451    /// `0x06` — multicast_descriptor.
452    MulticastDescriptor,
453    /// Reserved/unknown (`0x00`–`0x02`, `0x05`, `0x07`–`0xFF`).
454    Reserved(u8),
455}
456impl IpConnectionType {
457    /// Decode an `IP_connection_type` byte.
458    #[must_use]
459    pub fn from_u8(v: u8) -> Self {
460        match v {
461            0x03 => Self::IpDescriptor,
462            0x04 => Self::HostnameDescriptor,
463            0x06 => Self::MulticastDescriptor,
464            other => Self::Reserved(other),
465        }
466    }
467    /// Wire byte.
468    #[must_use]
469    pub const fn to_u8(self) -> u8 {
470        match self {
471            Self::IpDescriptor => 0x03,
472            Self::HostnameDescriptor => 0x04,
473            Self::MulticastDescriptor => 0x06,
474            Self::Reserved(v) => v,
475        }
476    }
477    /// Spec token, or `"reserved"`.
478    #[must_use]
479    pub fn name(&self) -> &'static str {
480        match self {
481            Self::IpDescriptor => "ip_descriptor",
482            Self::HostnameDescriptor => "hostname_descriptor",
483            Self::MulticastDescriptor => "multicast_descriptor",
484            Self::Reserved(_) => "reserved",
485        }
486    }
487}
488broadcast_common::impl_spec_display!(IpConnectionType, Reserved);
489
490/// `hybrid_descriptor()` (Table 83). The inner `IP_descriptor()` /
491/// `hostname_descriptor()` / `multicast_descriptor()` body is carried verbatim as
492/// opaque borrowed bytes — `IP_descriptor`/`hostname_descriptor` syntaxes are
493/// deferred to CI Plus V1.3 §14.2.1 (not reproduced); `multicast_descriptor` can
494/// be re-parsed with [`MulticastDescriptor::parse`].
495#[derive(Debug, Clone, PartialEq, Eq)]
496#[cfg_attr(feature = "serde", derive(serde::Serialize))]
497pub struct HybridDescriptor<'a> {
498    /// `LTS_id` (8) — Local TS for the TCP/UDP payload delivery.
499    pub lts_id: u8,
500    /// `IP_connection_type` (8) — Table 84.
501    pub ip_connection_type: IpConnectionType,
502    /// The inner connection-descriptor body (verbatim, after `IP_connection_type`).
503    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
504    pub inner: &'a [u8],
505}
506
507// data portion (after descriptor_length): LTS_id(1) + IP_connection_type(1) + inner.
508const HYBRID_FIXED: usize = 1 + 1;
509
510impl<'a> HybridDescriptor<'a> {
511    /// Parse a `hybrid_descriptor` (`descriptor_tag` `0x05` + `descriptor_length` +
512    /// body) from the start of `bytes`.
513    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
514        let data = parse_descriptor_header(bytes, HYBRID_DESCRIPTOR_TAG, "hybrid_descriptor")?;
515        if data.len() < HYBRID_FIXED {
516            return Err(Error::BufferTooShort {
517                need: HYBRID_FIXED,
518                have: data.len(),
519                what: "hybrid_descriptor",
520            });
521        }
522        Ok(Self {
523            lts_id: data[0],
524            ip_connection_type: IpConnectionType::from_u8(data[1]),
525            inner: &data[HYBRID_FIXED..],
526        })
527    }
528    fn data_len(&self) -> usize {
529        HYBRID_FIXED + self.inner.len()
530    }
531}
532
533impl Serialize for HybridDescriptor<'_> {
534    type Error = Error;
535    fn serialized_len(&self) -> usize {
536        descriptor_len(self.data_len())
537    }
538    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
539        let pos = write_descriptor_header(HYBRID_DESCRIPTOR_TAG, self.data_len(), buf)?;
540        buf[pos] = self.lts_id;
541        buf[pos + 1] = self.ip_connection_type.to_u8();
542        buf[pos + HYBRID_FIXED..pos + self.data_len()].copy_from_slice(self.inner);
543        Ok(pos + self.data_len())
544    }
545}
546
547// ---------------------------------------------------------------------------
548// Comms Cmd multicast_descriptor (Table 85) — descriptor_tag 0x06
549// ---------------------------------------------------------------------------
550
551/// `multicast_descriptor()` (Table 85).
552#[derive(Debug, Clone, PartialEq, Eq)]
553#[cfg_attr(feature = "serde", derive(serde::Serialize))]
554pub struct MulticastDescriptor {
555    /// `IP_protocol_version` (8) — Table 86.
556    pub ip_protocol_version: IpProtocolVersion,
557    /// `IP_address` (128) — multicast service address (IPv4: first 12 bytes `0x00`).
558    pub ip_address: [u8; IP_ADDR_LEN],
559    /// `multicast_port` (16).
560    pub multicast_port: u16,
561    /// `include_sources` (1) — `true` = receive only from listed sources; `false`
562    /// = receive from all sources except those listed (only relevant when sources
563    /// are present).
564    pub include_sources: bool,
565    /// `source_address` list (loop count `num_source_addresses`); empty = any source.
566    pub source_addresses: Vec<[u8; IP_ADDR_LEN]>,
567}
568
569// data portion: IP_protocol_version(1) + IP_address(16) + multicast_port(2) +
570// reserved/include_sources(1) + num_source_addresses(1).
571const MULTICAST_FIXED: usize = 1 + IP_ADDR_LEN + 2 + 1 + 1;
572const INCLUDE_SOURCES_BIT: u8 = 0x01;
573
574impl MulticastDescriptor {
575    /// Parse a `multicast_descriptor` (`descriptor_tag` `0x06` + `descriptor_length`
576    /// + body) from the start of `bytes`.
577    pub fn parse(bytes: &[u8]) -> Result<Self> {
578        let data =
579            parse_descriptor_header(bytes, MULTICAST_DESCRIPTOR_TAG, "multicast_descriptor")?;
580        if data.len() < MULTICAST_FIXED {
581            return Err(Error::BufferTooShort {
582                need: MULTICAST_FIXED,
583                have: data.len(),
584                what: "multicast_descriptor",
585            });
586        }
587        let ip_protocol_version = IpProtocolVersion::from_u8(data[0]);
588        let ip_address = read_addr(data, 1);
589        let multicast_port = u16::from_be_bytes([data[1 + IP_ADDR_LEN], data[2 + IP_ADDR_LEN]]);
590        let flags_pos = 3 + IP_ADDR_LEN;
591        let include_sources = data[flags_pos] & INCLUDE_SOURCES_BIT != 0;
592        let n = data[flags_pos + 1] as usize;
593        let mut pos = MULTICAST_FIXED;
594        if data.len() < pos + n * IP_ADDR_LEN {
595            return Err(Error::BufferTooShort {
596                need: pos + n * IP_ADDR_LEN,
597                have: data.len(),
598                what: "multicast_descriptor sources",
599            });
600        }
601        let mut source_addresses = Vec::with_capacity(n);
602        for _ in 0..n {
603            source_addresses.push(read_addr(data, pos));
604            pos += IP_ADDR_LEN;
605        }
606        Ok(Self {
607            ip_protocol_version,
608            ip_address,
609            multicast_port,
610            include_sources,
611            source_addresses,
612        })
613    }
614    fn data_len(&self) -> usize {
615        MULTICAST_FIXED + self.source_addresses.len() * IP_ADDR_LEN
616    }
617}
618
619impl Serialize for MulticastDescriptor {
620    type Error = Error;
621    fn serialized_len(&self) -> usize {
622        descriptor_len(self.data_len())
623    }
624    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
625        let mut pos = write_descriptor_header(MULTICAST_DESCRIPTOR_TAG, self.data_len(), buf)?;
626        buf[pos] = self.ip_protocol_version.to_u8();
627        buf[pos + 1..pos + 1 + IP_ADDR_LEN].copy_from_slice(&self.ip_address);
628        let p = pos + 1 + IP_ADDR_LEN;
629        buf[p..p + 2].copy_from_slice(&self.multicast_port.to_be_bytes());
630        // reserved(7)='0000000' + include_sources(1).
631        buf[p + 2] = u8::from(self.include_sources);
632        buf[p + 3] = self.source_addresses.len() as u8;
633        pos += MULTICAST_FIXED;
634        for a in &self.source_addresses {
635            buf[pos..pos + IP_ADDR_LEN].copy_from_slice(a);
636            pos += IP_ADDR_LEN;
637        }
638        Ok(pos)
639    }
640}
641
642// --- Shared 2-byte (tag + length) descriptor header helpers (Tables 83/85) ---
643
644// descriptor_tag(1) + descriptor_length(1).
645const DESCRIPTOR_HEADER: usize = 2;
646
647fn parse_descriptor_header<'a>(
648    bytes: &'a [u8],
649    expected_tag: u8,
650    what: &'static str,
651) -> Result<&'a [u8]> {
652    if bytes.len() < DESCRIPTOR_HEADER {
653        return Err(Error::BufferTooShort {
654            need: DESCRIPTOR_HEADER,
655            have: bytes.len(),
656            what,
657        });
658    }
659    if bytes[0] != expected_tag {
660        return Err(Error::InvalidObject {
661            what,
662            reason: "unexpected descriptor_tag",
663        });
664    }
665    let len = bytes[1] as usize;
666    let end = DESCRIPTOR_HEADER + len;
667    if bytes.len() < end {
668        return Err(Error::LengthMismatch {
669            what,
670            declared: len,
671            actual: bytes.len().saturating_sub(DESCRIPTOR_HEADER),
672        });
673    }
674    Ok(&bytes[DESCRIPTOR_HEADER..end])
675}
676
677fn descriptor_len(data_len: usize) -> usize {
678    DESCRIPTOR_HEADER + data_len
679}
680
681fn write_descriptor_header(tag: u8, data_len: usize, buf: &mut [u8]) -> Result<usize> {
682    let total = descriptor_len(data_len);
683    if buf.len() < total {
684        return Err(Error::OutputBufferTooSmall {
685            need: total,
686            have: buf.len(),
687        });
688    }
689    if data_len > u8::MAX as usize {
690        return Err(Error::LengthTooLarge(data_len));
691    }
692    buf[0] = tag;
693    buf[1] = data_len as u8;
694    Ok(DESCRIPTOR_HEADER)
695}
696
697// ---------------------------------------------------------------------------
698// Tag-dispatch helper (no resource_id — see module doc)
699// ---------------------------------------------------------------------------
700
701/// A parsed LSC v4 extension APDU.
702///
703/// There is intentionally **no** `resource_id`-keyed entry point: TS 103 205 does
704/// not print an LSC v4 resource_id (see the module doc). Dispatch is on the
705/// apdu_tag alone, for callers already in an LSC session.
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707#[cfg_attr(feature = "serde", derive(serde::Serialize))]
708#[non_exhaustive]
709pub enum LscV4Apdu {
710    /// `comms_info_req` (`0x9F8C07`).
711    CommsInfoReq(CommsInfoReq),
712    /// `comms_info_reply` (`0x9F8C08`).
713    CommsInfoReply(CommsInfoReply),
714    /// `comms_IP_config_req` (`0x9F8C09`).
715    CommsIpConfigReq(CommsIpConfigReq),
716}
717
718/// A parsed LSC v4 extension APDU that may carry an allocation
719/// (`comms_IP_config_reply` has a DNS-server `Vec`).
720#[derive(Debug, Clone, PartialEq, Eq)]
721#[cfg_attr(feature = "serde", derive(serde::Serialize))]
722#[non_exhaustive]
723pub enum LscV4ReplyApdu {
724    /// `comms_IP_config_reply` (`0x9F8C0A`).
725    CommsIpConfigReply(CommsIpConfigReply),
726}
727
728impl LscV4Apdu {
729    /// Parse a fixed-size LSC v4 extension APDU by its apdu_tag. Returns
730    /// `Ok(None)` for `comms_IP_config_reply`, which allocates and is returned by
731    /// [`parse_ip_config_reply`].
732    pub fn parse(body: &[u8]) -> Result<Self> {
733        if body.len() < 3 {
734            return Err(Error::BufferTooShort {
735                need: 3,
736                have: body.len(),
737                what: "lsc_v4 apdu_tag",
738            });
739        }
740        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
741        match t {
742            tag::COMMS_INFO_REQ => Ok(Self::CommsInfoReq(CommsInfoReq::parse(body)?)),
743            tag::COMMS_INFO_REPLY => Ok(Self::CommsInfoReply(CommsInfoReply::parse(body)?)),
744            tag::COMMS_IP_CONFIG_REQ => Ok(Self::CommsIpConfigReq(CommsIpConfigReq::parse(body)?)),
745            _ => Err(Error::UnexpectedApduTag {
746                got: t.as_u24(),
747                expected: tag::COMMS_INFO_REQ.as_u24(),
748                what: "lsc_v4",
749            }),
750        }
751    }
752}
753
754/// Parse a `comms_IP_config_reply` (`0x9F8C0A`) APDU.
755pub fn parse_ip_config_reply(body: &[u8]) -> Result<CommsIpConfigReply> {
756    CommsIpConfigReply::parse(body)
757}
758
759impl Serialize for LscV4Apdu {
760    type Error = Error;
761    fn serialized_len(&self) -> usize {
762        match self {
763            Self::CommsInfoReq(o) => o.serialized_len(),
764            Self::CommsInfoReply(o) => o.serialized_len(),
765            Self::CommsIpConfigReq(o) => o.serialized_len(),
766        }
767    }
768    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
769        match self {
770            Self::CommsInfoReq(o) => o.serialize_into(buf),
771            Self::CommsInfoReply(o) => o.serialize_into(buf),
772            Self::CommsIpConfigReq(o) => o.serialize_into(buf),
773        }
774    }
775}
776
777impl Serialize for LscV4ReplyApdu {
778    type Error = Error;
779    fn serialized_len(&self) -> usize {
780        match self {
781            Self::CommsIpConfigReply(o) => o.serialized_len(),
782        }
783    }
784    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
785        match self {
786            Self::CommsIpConfigReply(o) => o.serialize_into(buf),
787        }
788    }
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    const IP_A: [u8; IP_ADDR_LEN] = [
796        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0xA8, 0x01,
797        0x0A,
798    ];
799    const IP_B: [u8; IP_ADDR_LEN] = [
800        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x08, 0x08, 0x08,
801        0x08,
802    ];
803
804    #[test]
805    fn info_req_round_trips() {
806        let bytes = CommsInfoReq.to_bytes();
807        assert_eq!(bytes, [0x9F, 0x8C, 0x07, 0x00]);
808        assert_eq!(CommsInfoReq::parse(&bytes).unwrap(), CommsInfoReq);
809    }
810
811    #[test]
812    fn info_reply_round_trips_and_bites() {
813        let r = CommsInfoReply {
814            lts_id: 0x07,
815            status: true,
816            source_ip_address: IP_A,
817            source_port: 0x1234,
818            input_delivery_pid: 0x0100,
819        };
820        let bytes = r.to_bytes();
821        // body=22=0x16. LTS(07) status(01) IP(16) port(12 34) pid(01 00).
822        assert_eq!(bytes[0..4], [0x9F, 0x8C, 0x08, 0x16]);
823        assert_eq!(bytes[4], 0x07);
824        assert_eq!(bytes[5], 0x01); // status
825        assert_eq!(&bytes[6..22], &IP_A);
826        assert_eq!(&bytes[22..24], &[0x12, 0x34]);
827        assert_eq!(&bytes[24..26], &[0x01, 0x00]);
828        assert_eq!(CommsInfoReply::parse(&bytes).unwrap(), r);
829        let mut other = r;
830        other.status = false;
831        assert_eq!(other.to_bytes()[5], 0x00);
832        assert_ne!(bytes, other.to_bytes());
833    }
834
835    #[test]
836    fn info_reply_pid_is_13_bit_masked() {
837        // Top 3 bits of the PID word are reserved.
838        let bytes = [
839            0x9F, 0x8C, 0x08, 0x16, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
840            0x00, 0x00, 0xFF, 0xFE,
841        ];
842        let r = CommsInfoReply::parse(&bytes).unwrap();
843        assert_eq!(r.input_delivery_pid, 0x1FFE);
844    }
845
846    #[test]
847    fn ip_config_req_round_trips() {
848        let bytes = CommsIpConfigReq.to_bytes();
849        assert_eq!(bytes, [0x9F, 0x8C, 0x09, 0x00]);
850        assert_eq!(CommsIpConfigReq::parse(&bytes).unwrap(), CommsIpConfigReq);
851    }
852
853    #[test]
854    fn ip_config_reply_disconnected_round_trips() {
855        let r = CommsIpConfigReply {
856            connection_state: ConnectionState::Disconnected,
857            physical_address: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
858            ip_config: None,
859        };
860        let bytes = r.to_bytes();
861        // body=7: conn_state(00) MAC(6). connection_state 0 in high 2 bits => 0x00.
862        assert_eq!(
863            bytes,
864            [
865                0x9F, 0x8C, 0x0A, 0x07, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55
866            ]
867        );
868        assert_eq!(CommsIpConfigReply::parse(&bytes).unwrap(), r);
869    }
870
871    #[test]
872    fn ip_config_reply_connected_two_dns_round_trips_and_bites() {
873        let r = CommsIpConfigReply {
874            connection_state: ConnectionState::Connected,
875            physical_address: [0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
876            ip_config: Some(IpConfig {
877                ip_address: IP_A,
878                network_mask: IP_B,
879                default_gateway: IP_A,
880                dhcp_server_address: IP_B,
881                dns_server_addresses: alloc::vec![IP_A, IP_B],
882            }),
883        };
884        let bytes = r.to_bytes();
885        // connection_state Connected(01) in high 2 bits => 0x40.
886        assert_eq!(bytes[0..4], [0x9F, 0x8C, 0x0A, (7 + 65 + 32) as u8]);
887        assert_eq!(bytes[4], 0x40);
888        assert_eq!(&bytes[5..11], &[0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]);
889        // num_DNS_servers at offset 11 + 4*16 = 75.
890        assert_eq!(bytes[4 + 7 + 4 * IP_ADDR_LEN], 0x02);
891        assert_eq!(CommsIpConfigReply::parse(&bytes).unwrap(), r);
892        let mut other = r.clone();
893        if let Some(c) = &mut other.ip_config {
894            c.dns_server_addresses.pop();
895        }
896        assert_ne!(bytes, other.to_bytes());
897    }
898
899    #[test]
900    fn hybrid_descriptor_round_trips_and_bites() {
901        let h = HybridDescriptor {
902            lts_id: 0x05,
903            ip_connection_type: IpConnectionType::IpDescriptor,
904            inner: &[0xDE, 0xAD],
905        };
906        let bytes = h.to_bytes();
907        // tag(05) len(04) LTS(05) conn_type(03) inner(DE AD).
908        assert_eq!(bytes, [0x05, 0x04, 0x05, 0x03, 0xDE, 0xAD]);
909        assert_eq!(HybridDescriptor::parse(&bytes).unwrap(), h);
910        let mut other = h;
911        other.lts_id = 0x06;
912        assert_ne!(bytes, other.to_bytes());
913    }
914
915    #[test]
916    fn multicast_descriptor_two_sources_round_trips_and_bites() {
917        let m = MulticastDescriptor {
918            ip_protocol_version: IpProtocolVersion::Ipv4,
919            ip_address: IP_A,
920            multicast_port: 0x1389,
921            include_sources: true,
922            source_addresses: alloc::vec![IP_A, IP_B],
923        };
924        let bytes = m.to_bytes();
925        // data = 1 + 16 + 2 + 1 + 1 + 2*16 = 53. total = 55.
926        assert_eq!(bytes[0], MULTICAST_DESCRIPTOR_TAG);
927        assert_eq!(bytes[1], 53);
928        assert_eq!(bytes[2], 0x01); // IPv4
929        assert_eq!(&bytes[3..19], &IP_A);
930        assert_eq!(&bytes[19..21], &[0x13, 0x89]);
931        assert_eq!(bytes[21], 0x01); // include_sources
932        assert_eq!(bytes[22], 0x02); // num_source_addresses
933        assert_eq!(MulticastDescriptor::parse(&bytes).unwrap(), m);
934        let mut other = m.clone();
935        other.include_sources = false;
936        assert_eq!(other.to_bytes()[21], 0x00);
937        assert_ne!(bytes, other.to_bytes());
938    }
939
940    #[test]
941    fn multicast_descriptor_any_source() {
942        let m = MulticastDescriptor {
943            ip_protocol_version: IpProtocolVersion::Ipv6,
944            ip_address: IP_B,
945            multicast_port: 5004,
946            include_sources: false,
947            source_addresses: Vec::new(),
948        };
949        let bytes = m.to_bytes();
950        assert_eq!(bytes[1], 21); // data = 1+16+2+1+1 = 21
951        assert_eq!(MulticastDescriptor::parse(&bytes).unwrap(), m);
952    }
953
954    #[test]
955    fn dispatch_routes_fixed_tags() {
956        assert!(matches!(
957            LscV4Apdu::parse(&CommsInfoReq.to_bytes()).unwrap(),
958            LscV4Apdu::CommsInfoReq(_)
959        ));
960        let reply = CommsInfoReply {
961            lts_id: 0,
962            status: false,
963            source_ip_address: [0; IP_ADDR_LEN],
964            source_port: 0,
965            input_delivery_pid: 0,
966        };
967        assert!(matches!(
968            LscV4Apdu::parse(&reply.to_bytes()).unwrap(),
969            LscV4Apdu::CommsInfoReply(_)
970        ));
971        assert!(matches!(
972            LscV4Apdu::parse(&CommsIpConfigReq.to_bytes()).unwrap(),
973            LscV4Apdu::CommsIpConfigReq(_)
974        ));
975        // comms_IP_config_reply routes via the allocating helper, not LscV4Apdu.
976        let cfg = CommsIpConfigReply {
977            connection_state: ConnectionState::Disconnected,
978            physical_address: [0; MAC_LEN],
979            ip_config: None,
980        };
981        let cb = cfg.to_bytes();
982        assert_eq!(parse_ip_config_reply(&cb).unwrap(), cfg);
983        // It is NOT a member of the fixed-size dispatch set.
984        assert!(matches!(
985            LscV4Apdu::parse(&cb),
986            Err(Error::UnexpectedApduTag { .. })
987        ));
988    }
989}