Skip to main content

dvb_ci/ci_ext/
broadcast_service_gateway.rs

1//! Broadcast Service Gateway objects — ETSI TS 101 699 V1.1.1 §6.1.3.3,
2//! Tables 32-34 (PDF pp. 39-40). See `docs/ci_plus/input-modules.md`.
3//!
4//! Resource ID `0x00811ii1` (`ii` = Module ID). A Type 'B' module on a broadcast
5//! network presents this resource. It **inherits all Generic Service Gateway
6//! calls** (Tables 22-31, tags `9F8000`-`9F8008` — see
7//! [`super::service_gateway`]) and adds the broadcast-event (EIT) extension
8//! objects:
9//!
10//! - `EITSectionReq` (`9F 80 10`, Table 32) — app → module: request an EIT section.
11//! - `EITSectionAck` (`9F 80 11`, Table 33) — module → app: response code +
12//!   EIT-modelled event loop.
13//!
14//! Dispatch ([`BroadcastServiceGatewayApdu`]) routes `9F8010`/`9F8011` to the EIT
15//! objects and **delegates every other `9F80xx` tag to the inherited Generic
16//! Service Gateway dispatch**.
17
18use super::service_gateway::ServiceGatewayApdu;
19use crate::error::{Error, Result};
20use crate::objects;
21use crate::tag::ApduTag;
22use alloc::vec::Vec;
23use broadcast_common::{Parse, Serialize};
24
25/// Resource-scoped `apdu_tag`s for the Broadcast Service Gateway EIT extension
26/// (Tables 32-33). The generic-gateway tags live in [`super::service_gateway::tag`].
27pub mod tag {
28    use crate::tag::ApduTag;
29    /// `EITSectionReqTag` = `9F 80 10`.
30    pub const EIT_SECTION_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x10);
31    /// `EITSectionAckTag` = `9F 80 11`.
32    pub const EIT_SECTION_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x11);
33}
34
35/// `ResponseCode` — EIT section response status (Table 34).
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize))]
38#[non_exhaustive]
39pub enum EitResponseCode {
40    /// `0b00` — Section not on the present document (may be on another TS).
41    NotOnPresentDocument,
42    /// `0b01` — Section not available.
43    NotAvailable,
44    /// `0b10` — Section found.
45    SectionFound,
46    /// `0b11` — reserved.
47    Reserved,
48}
49
50impl EitResponseCode {
51    /// Decode the 2-bit `ResponseCode`.
52    #[must_use]
53    pub fn from_u8(v: u8) -> Self {
54        match v & 0x03 {
55            0b00 => Self::NotOnPresentDocument,
56            0b01 => Self::NotAvailable,
57            0b10 => Self::SectionFound,
58            _ => Self::Reserved,
59        }
60    }
61    /// 2-bit wire value.
62    #[must_use]
63    pub const fn to_u8(self) -> u8 {
64        match self {
65            Self::NotOnPresentDocument => 0b00,
66            Self::NotAvailable => 0b01,
67            Self::SectionFound => 0b10,
68            Self::Reserved => 0b11,
69        }
70    }
71    /// Spec token, or `"reserved"`.
72    #[must_use]
73    pub fn name(&self) -> &'static str {
74        match self {
75            Self::NotOnPresentDocument => "Section not on the present document",
76            Self::NotAvailable => "Section not available",
77            Self::SectionFound => "Section found",
78            Self::Reserved => "reserved",
79        }
80    }
81}
82broadcast_common::impl_spec_display!(EitResponseCode);
83
84/// `EITSectionReq()` (Table 32) — app → module: request an EIT section.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize))]
87pub struct EitSectionReq {
88    /// `TableID` — 16-bit (wider than the 8-bit EIT table_id); EIT values
89    /// `0x4E`-`0x6F` (ETS 300 468).
90    pub table_id: u16,
91    /// `ServiceID` — as the EIT in ETS 300 468.
92    pub service_id: u16,
93    /// `SectionNumber` — as the EIT in ETS 300 468.
94    pub section_number: u8,
95    /// `OriginalNetworkID` — as the EIT in ETS 300 468.
96    pub original_network_id: u16,
97    /// `OKToDisruptService` — `1`: a current service may be disrupted to obtain
98    /// the requested event information; `0`: delivery shall not be disrupted.
99    pub ok_to_disrupt_service: bool,
100}
101
102/// One EIT event in an [`EitSectionAck`] loop — modelled on the EIT event loop in
103/// ETS 300 468.
104#[derive(Debug, Clone, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106pub struct EitEvent<'a> {
107    /// `event_id` (16-bit).
108    pub event_id: u16,
109    /// `start_time` — 40-bit MJD+BCD UTC, carried verbatim (5 bytes).
110    pub start_time: [u8; 5],
111    /// `duration` — 24-bit BCD HHMMSS.
112    pub duration: [u8; 3],
113    /// `running_status` — 3-bit EIT running status.
114    pub running_status: u8,
115    /// `free_CA_mode` — EIT meaning.
116    pub free_ca_mode: bool,
117    /// The EIT event descriptor loop (`descriptors_loop_length` bytes), verbatim.
118    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
119    pub descriptors: &'a [u8],
120}
121
122/// `EITSectionAck()` (Table 33) — module → app: response code + an EIT event loop.
123#[derive(Debug, Clone, PartialEq, Eq)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize))]
125pub struct EitSectionAck<'a> {
126    /// `ResponseCode` (Table 34).
127    pub response_code: EitResponseCode,
128    /// The events (`Length` is the byte count of the event loop, may be 0).
129    #[cfg_attr(feature = "serde", serde(borrow))]
130    pub events: Vec<EitEvent<'a>>,
131}
132
133// --- EITSectionReq ---
134
135// TableID(2) + ServiceID(2) + SectionNumber(1) + OriginalNetworkID(2)
136// + Reserved(7)/OKToDisruptService(1).
137const EIT_SECTION_REQ_BODY: usize = 2 + 2 + 1 + 2 + 1;
138
139impl<'a> Parse<'a> for EitSectionReq {
140    type Error = Error;
141    fn parse(bytes: &'a [u8]) -> Result<Self> {
142        let body = objects::parse_apdu_header(bytes, tag::EIT_SECTION_REQ, "EITSectionReq")?;
143        if body.len() < EIT_SECTION_REQ_BODY {
144            return Err(Error::BufferTooShort {
145                need: EIT_SECTION_REQ_BODY,
146                have: body.len(),
147                what: "EITSectionReq",
148            });
149        }
150        Ok(Self {
151            table_id: u16::from_be_bytes([body[0], body[1]]),
152            service_id: u16::from_be_bytes([body[2], body[3]]),
153            section_number: body[4],
154            original_network_id: u16::from_be_bytes([body[5], body[6]]),
155            ok_to_disrupt_service: (body[7] & 0x01) != 0,
156        })
157    }
158}
159impl Serialize for EitSectionReq {
160    type Error = Error;
161    fn serialized_len(&self) -> usize {
162        objects::apdu_len(EIT_SECTION_REQ_BODY)
163    }
164    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
165        let pos = objects::write_apdu_header(tag::EIT_SECTION_REQ, EIT_SECTION_REQ_BODY, buf)?;
166        buf[pos..pos + 2].copy_from_slice(&self.table_id.to_be_bytes());
167        buf[pos + 2..pos + 4].copy_from_slice(&self.service_id.to_be_bytes());
168        buf[pos + 4] = self.section_number;
169        buf[pos + 5..pos + 7].copy_from_slice(&self.original_network_id.to_be_bytes());
170        // Reserved(7)=0, OKToDisruptService(1).
171        buf[pos + 7] = u8::from(self.ok_to_disrupt_service);
172        Ok(pos + EIT_SECTION_REQ_BODY)
173    }
174}
175
176// --- EITSectionAck ---
177
178// Reserved(2)/ResponseCode(2)/Length(12) = 2 header bytes.
179const EIT_SECTION_ACK_PREFIX: usize = 2;
180// event_id(2) + start_time(5) + duration(3) + running_status/free_CA/loop_len(2).
181const EIT_EVENT_FIXED: usize = 2 + 5 + 3 + 2;
182
183impl<'a> Parse<'a> for EitSectionAck<'a> {
184    type Error = Error;
185    fn parse(bytes: &'a [u8]) -> Result<Self> {
186        let body = objects::parse_apdu_header(bytes, tag::EIT_SECTION_ACK, "EITSectionAck")?;
187        if body.len() < EIT_SECTION_ACK_PREFIX {
188            return Err(Error::BufferTooShort {
189                need: EIT_SECTION_ACK_PREFIX,
190                have: body.len(),
191                what: "EITSectionAck",
192            });
193        }
194        // byte0: Reserved(2) + ResponseCode(2) + Length high nibble(4 → top of 12).
195        let response_code = EitResponseCode::from_u8((body[0] >> 4) & 0x03);
196        let length = ((u16::from(body[0] & 0x0F) << 8) | u16::from(body[1])) as usize;
197        let loop_start = EIT_SECTION_ACK_PREFIX;
198        let loop_end = loop_start + length;
199        if body.len() < loop_end {
200            return Err(Error::LengthMismatch {
201                what: "EITSectionAck event loop",
202                declared: length,
203                actual: body.len() - loop_start,
204            });
205        }
206        let mut events = Vec::new();
207        let mut p = loop_start;
208        while p < loop_end {
209            if loop_end - p < EIT_EVENT_FIXED {
210                return Err(Error::InvalidObject {
211                    what: "EITSectionAck event",
212                    reason: "truncated event header",
213                });
214            }
215            let event_id = u16::from_be_bytes([body[p], body[p + 1]]);
216            let mut start_time = [0u8; 5];
217            start_time.copy_from_slice(&body[p + 2..p + 7]);
218            let mut duration = [0u8; 3];
219            duration.copy_from_slice(&body[p + 7..p + 10]);
220            let b10 = body[p + 10];
221            let b11 = body[p + 11];
222            let running_status = (b10 >> 5) & 0x07;
223            let free_ca_mode = (b10 & 0x10) != 0;
224            let dll = ((u16::from(b10 & 0x0F) << 8) | u16::from(b11)) as usize;
225            let desc_start = p + EIT_EVENT_FIXED;
226            let desc_end = desc_start + dll;
227            if desc_end > loop_end {
228                return Err(Error::LengthMismatch {
229                    what: "EITSectionAck event descriptors",
230                    declared: dll,
231                    actual: loop_end - desc_start,
232                });
233            }
234            events.push(EitEvent {
235                event_id,
236                start_time,
237                duration,
238                running_status,
239                free_ca_mode,
240                descriptors: &body[desc_start..desc_end],
241            });
242            p = desc_end;
243        }
244        Ok(Self {
245            response_code,
246            events,
247        })
248    }
249}
250
251impl EitSectionAck<'_> {
252    /// Byte length of the event loop (`Length`).
253    fn loop_len(&self) -> usize {
254        self.events
255            .iter()
256            .map(|e| EIT_EVENT_FIXED + e.descriptors.len())
257            .sum()
258    }
259}
260
261impl Serialize for EitSectionAck<'_> {
262    type Error = Error;
263    fn serialized_len(&self) -> usize {
264        objects::apdu_len(EIT_SECTION_ACK_PREFIX + self.loop_len())
265    }
266    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
267        let length = self.loop_len();
268        if length > 0x0FFF {
269            return Err(Error::InvalidObject {
270                what: "EITSectionAck",
271                reason: "event loop longer than 4095 bytes",
272            });
273        }
274        let body_len = EIT_SECTION_ACK_PREFIX + length;
275        let mut pos = objects::write_apdu_header(tag::EIT_SECTION_ACK, body_len, buf)?;
276        let len16 = length as u16;
277        // Reserved(2)=0, ResponseCode(2), Length(12).
278        buf[pos] = (self.response_code.to_u8() << 4) | ((len16 >> 8) as u8 & 0x0F);
279        buf[pos + 1] = len16 as u8;
280        pos += EIT_SECTION_ACK_PREFIX;
281        for e in &self.events {
282            if e.descriptors.len() > 0x0FFF {
283                return Err(Error::InvalidObject {
284                    what: "EITSectionAck event",
285                    reason: "event descriptors loop longer than 4095 bytes",
286                });
287            }
288            buf[pos..pos + 2].copy_from_slice(&e.event_id.to_be_bytes());
289            buf[pos + 2..pos + 7].copy_from_slice(&e.start_time);
290            buf[pos + 7..pos + 10].copy_from_slice(&e.duration);
291            let dll = e.descriptors.len() as u16;
292            buf[pos + 10] = ((e.running_status & 0x07) << 5)
293                | (u8::from(e.free_ca_mode) << 4)
294                | ((dll >> 8) as u8 & 0x0F);
295            buf[pos + 11] = dll as u8;
296            pos += EIT_EVENT_FIXED;
297            buf[pos..pos + e.descriptors.len()].copy_from_slice(e.descriptors);
298            pos += e.descriptors.len();
299        }
300        Ok(pos)
301    }
302}
303
304/// Resource-scoped dispatch over the Broadcast Service Gateway objects.
305///
306/// `9F8010`/`9F8011` route to the EIT extension objects; every other `9F80xx` tag
307/// is delegated to the inherited [`ServiceGatewayApdu`] (Tables 22-30).
308#[derive(Debug, Clone, PartialEq, Eq)]
309#[cfg_attr(feature = "serde", derive(serde::Serialize))]
310#[non_exhaustive]
311pub enum BroadcastServiceGatewayApdu<'a> {
312    /// An inherited Generic Service Gateway object (`9F8000`-`9F8008`).
313    #[cfg_attr(feature = "serde", serde(borrow))]
314    ServiceGateway(ServiceGatewayApdu<'a>),
315    /// `EITSectionReq` (`9F 80 10`).
316    EitSectionReq(EitSectionReq),
317    /// `EITSectionAck` (`9F 80 11`).
318    EitSectionAck(EitSectionAck<'a>),
319}
320
321impl<'a> BroadcastServiceGatewayApdu<'a> {
322    /// Parse a Broadcast Service Gateway APDU. `9F8010`/`9F8011` route to the EIT
323    /// objects; any other tag is delegated to the inherited Generic Service
324    /// Gateway dispatch.
325    pub fn parse(body: &'a [u8]) -> Result<Self> {
326        if body.len() < 3 {
327            return Err(Error::BufferTooShort {
328                need: 3,
329                have: body.len(),
330                what: "broadcast_service_gateway apdu_tag",
331            });
332        }
333        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
334        match t {
335            tag::EIT_SECTION_REQ => Ok(Self::EitSectionReq(EitSectionReq::parse(body)?)),
336            tag::EIT_SECTION_ACK => Ok(Self::EitSectionAck(EitSectionAck::parse(body)?)),
337            // All other 9F80xx tags are the inherited Generic Service Gateway calls.
338            _ => Ok(Self::ServiceGateway(ServiceGatewayApdu::parse(body)?)),
339        }
340    }
341}
342
343impl Serialize for BroadcastServiceGatewayApdu<'_> {
344    type Error = Error;
345    fn serialized_len(&self) -> usize {
346        match self {
347            Self::ServiceGateway(o) => o.serialized_len(),
348            Self::EitSectionReq(o) => o.serialized_len(),
349            Self::EitSectionAck(o) => o.serialized_len(),
350        }
351    }
352    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
353        match self {
354            Self::ServiceGateway(o) => o.serialize_into(buf),
355            Self::EitSectionReq(o) => o.serialize_into(buf),
356            Self::EitSectionAck(o) => o.serialize_into(buf),
357        }
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::super::service_gateway::{ServiceListReq, ServiceReference};
364    use super::*;
365
366    #[test]
367    fn eit_section_req_round_trips_and_bites() {
368        let req = EitSectionReq {
369            table_id: 0x004E,
370            service_id: 0x0064,
371            section_number: 0x00,
372            original_network_id: 0x0001,
373            ok_to_disrupt_service: true,
374        };
375        let bytes = req.to_bytes();
376        // body 8; tag(3)+len(1)+8 = 12; byte7 = 0x01 (OKToDisrupt).
377        assert_eq!(
378            bytes,
379            [
380                0x9F, 0x80, 0x10, 0x08, 0x00, 0x4E, 0x00, 0x64, 0x00, 0x00, 0x01, 0x01
381            ]
382        );
383        assert_eq!(EitSectionReq::parse(&bytes).unwrap(), req);
384        let mut other = req;
385        other.ok_to_disrupt_service = false;
386        assert_ne!(bytes, other.to_bytes());
387        assert_eq!(other.to_bytes()[11], 0x00);
388    }
389
390    #[test]
391    fn eit_section_ack_multi_event_round_trips_and_bites() {
392        // Two events; first carries a 2-byte descriptor, second is empty.
393        let d0 = [0x4D, 0x00]; // short_event_descriptor tag, empty-ish
394        let e0 = EitEvent {
395            event_id: 0x1001,
396            start_time: [0xC0, 0x79, 0x12, 0x45, 0x00],
397            duration: [0x01, 0x30, 0x00],
398            running_status: 4,
399            free_ca_mode: false,
400            descriptors: &d0,
401        };
402        let e1 = EitEvent {
403            event_id: 0x1002,
404            start_time: [0xC0, 0x79, 0x14, 0x15, 0x00],
405            duration: [0x00, 0x45, 0x00],
406            running_status: 1,
407            free_ca_mode: true,
408            descriptors: &[],
409        };
410        let ack = EitSectionAck {
411            response_code: EitResponseCode::SectionFound,
412            events: alloc::vec![e0, e1],
413        };
414        let bytes = ack.to_bytes();
415        // event0 = 12 fixed + 2 desc = 14; event1 = 12; Length = 26 = 0x1A.
416        // byte4(prefix0) = (0b10<<4) | (26>>8) = 0x20 ; byte5 = 26 = 0x1A.
417        assert_eq!(bytes[0..6], [0x9F, 0x80, 0x11, 0x1C, 0x20, 0x1A]);
418        // event0 running_status=4, free_CA=0, dll=2 → byte = (4<<5)|0|0 = 0x80, next 0x02
419        assert_eq!(bytes[16], 0x80);
420        assert_eq!(bytes[17], 0x02);
421        let parsed = EitSectionAck::parse(&bytes).unwrap();
422        assert_eq!(parsed, ack);
423        assert_eq!(parsed.events.len(), 2);
424        assert_eq!(parsed.events[0].running_status, 4);
425        assert!(parsed.events[1].free_ca_mode);
426        let mut other = ack.clone();
427        other.events[0].event_id = 0x1003;
428        assert_ne!(bytes, other.to_bytes());
429    }
430
431    #[test]
432    fn eit_section_ack_empty_loop() {
433        let ack = EitSectionAck {
434            response_code: EitResponseCode::NotAvailable,
435            events: alloc::vec![],
436        };
437        let bytes = ack.to_bytes();
438        // byte4 = (0b01<<4)|0 = 0x10 ; Length = 0.
439        assert_eq!(bytes, [0x9F, 0x80, 0x11, 0x02, 0x10, 0x00]);
440        assert_eq!(EitSectionAck::parse(&bytes).unwrap(), ack);
441        assert_eq!(ack.response_code.name(), "Section not available");
442    }
443
444    #[test]
445    fn bsg_routes_eit_tags() {
446        let req = EitSectionReq {
447            table_id: 0x4E,
448            service_id: 1,
449            section_number: 0,
450            original_network_id: 1,
451            ok_to_disrupt_service: false,
452        }
453        .to_bytes();
454        assert!(matches!(
455            BroadcastServiceGatewayApdu::parse(&req).unwrap(),
456            BroadcastServiceGatewayApdu::EitSectionReq(_)
457        ));
458    }
459
460    #[test]
461    fn bsg_delegates_generic_gateway_tags() {
462        // 9F8000 (ServiceListReq) is inherited from the Generic Service Gateway.
463        let req = ServiceListReq.to_bytes();
464        let parsed = BroadcastServiceGatewayApdu::parse(&req).unwrap();
465        assert!(matches!(
466            parsed,
467            BroadcastServiceGatewayApdu::ServiceGateway(ServiceGatewayApdu::ServiceListReq(_))
468        ));
469        assert_eq!(parsed.to_bytes(), req);
470
471        // 9F8006 ServiceDescAck inherited too.
472        use super::super::service_gateway::ServiceDescAck;
473        let sda = ServiceDescAck {
474            service: ServiceReference {
475                original_network_id: 1,
476                service_id: 0x64,
477            },
478            eit_schedule_flag: true,
479            eit_present_following_flag: true,
480            running_status: 4,
481            free_ca_mode: false,
482            descriptors: &[0x48, 0x01, 0x01],
483        }
484        .to_bytes();
485        let parsed = BroadcastServiceGatewayApdu::parse(&sda).unwrap();
486        assert!(matches!(
487            parsed,
488            BroadcastServiceGatewayApdu::ServiceGateway(ServiceGatewayApdu::ServiceDescAck(_))
489        ));
490        assert_eq!(parsed.to_bytes(), sda);
491    }
492}